A PHP 8 extension that wraps the Advantage Database Server (ADS) Client Engine (ACE) SDK, exposing native ADS connectivity to PHP via six clean OOP classes.
| Component | Version |
|---|---|
| PHP | 8.0.x — ZTS VC16 x64 (Thread Safe) |
| Advantage Database Server | 11.x (ACE SDK 11.10) |
| Visual Studio | 2022 (v17.x, MSVC toolset 14.4x) |
| OS | Windows x64 (Server 2016 / 2019 / 2022 or Windows 10/11) |
| PHP Dev Pack | php-8.0.1-devel-vs16-x64 |
Note on VS2022 vs VS2019: PHP 8.0.1 was compiled with VS2019 (linker 14.28). The build script automatically patches the output DLL's PE linker version field from 14.4x → 14.28 so PHP's extension loader accepts it.
php_advantage/
├── src/
│ ├── php_ads.h # Shared header — structs, macros, class-entry externs
│ ├── php_ads.c # Module entry, MINIT/RINIT, shared helpers, constants
│ ├── ads_connection.c # AdsConnection class
│ ├── ads_statement.c # AdsStatement class
│ ├── ads_table.c # AdsTable class
│ ├── ads_misc.c # AdsTransaction + AdsDictionary classes
│ ├── ads_prepared.c # AdsPreparedStatement class
│ └── ads_arginfo.h # PHP 8 arginfo descriptors for all methods
├── acesdk/ # Advantage Client Engine SDK (ace.h, ace64.lib, DLLs)
├── php-devpack/ # PHP 8.0.1 development headers and import library
├── bin/zts/ # Build output: php_ads.dll
├── Makefile.win # NMake build file
├── build.bat # One-shot build script (sets up VS2022 + NMake + patch)
└── patch_linker_ver.ps1 # PE linker version patcher (14.4x → 14.28)
- Install Visual Studio 2022 (Community or Build Tools) with the Desktop development with C++ workload.
- Install PHP 8.0.x ZTS x64 (e.g.
C:\php\). - Place the PHP 8.0.1 Dev Pack under
php-devpack\php-8.0.1-devel-vs16-x64\(already included). - Place the ACE SDK files under
acesdk\(ace.h + ace64.lib + DLLs — already included).
Open a regular Command Prompt (not a Developer Command Prompt — build.bat sets that up):
cd F:\php_advantage
build.batThe script:
- Initialises the VS2022 x64 toolchain via
vcvars64.bat. - Runs
nmake /f Makefile.win→ producesbin\zts\php_ads.dll. - Patches the PE linker version field from 14.4x to 14.28.
nmake /f Makefile.win installThis copies php_ads.dll to C:\php\ext\php_ads.dll.
Alternatively, copy manually:
copy bin\zts\php_ads.dll C:\php\ext\php_ads.dllThe ACE runtime DLLs must be discoverable at the time PHP loads the extension.
The simplest approach is to copy them alongside php.exe:
copy acesdk\ace64.dll C:\php\
copy acesdk\adsloc64.dll C:\php\
copy acesdk\axcws64.dll C:\php\
copy acesdk\aicu64.dll C:\php\Add one line to C:\php\php.ini:
extension=php_adsC:\php\php.exe -m | findstr adsExpected output: ads
All errors throw AdsException (extends RuntimeException).
Error codes in getCode() are native ACE error numbers.
// Static factory
$conn = AdsConnection::connect([
'path' => '\\\\server:6262\\share\\mydb.add', // required
'user' => 'username', // optional
'password' => 'secret', // optional
'serverType' => ADS_REMOTE_SERVER, // optional, default: LOCAL|REMOTE
'options' => 0, // optional ulOptions bitmask
]);
$conn->close();
$conn->isAlive() : bool
$conn->query(string $sql) : AdsStatement
$conn->execute(string $sql) : bool // for INSERT / UPDATE / DELETE / DDL
$conn->prepare(string $sql) : AdsPreparedStatement // parameterized query
$conn->beginTransaction() : AdsTransactionAdsConnection does not have a constructor — always use AdsConnection::connect().
Returned by AdsConnection::query().
$stmt = $conn->query("SELECT PropertyID, Address FROM properties");
$stmt->fetchAssoc() : array|false // next row as associative array, false at EOF
$stmt->fetchRow() : array|false // next row as indexed array, false at EOF
$stmt->fetchAll() : array // all remaining rows as array of assoc arrays
$stmt->columnCount() : int
$stmt->rowCount() : int // total records in result set
$stmt->close() : voidReturned by AdsConnection::prepare(). Use parameterized queries to safely pass
user-supplied values without string interpolation or manual escaping.
- Write
:nameplaceholders in the SQL string. - Call a
bind*method for each placeholder before callingexecute(). - The leading
:is optional when calling bind methods —"status"and":status"are equivalent. ?positional placeholders are not supported; always use named:namemarkers.
$prep->bindString (string $name, string $value) : void
$prep->bindInt (string $name, int $value) : void
$prep->bindDouble (string $name, float $value) : void
$prep->bindBool (string $name, bool $value) : void
$prep->bindDate (string $name, string $value) : void // "CCYYMMDD"
$prep->bindTimestamp(string $name, string $value) : void // "YYYY-MM-DD HH:MM:SS"
$prep->bindMoney (string $name, int $value) : void // SIGNED64 scaled integer
$prep->bindBinary (string $name, string $data, int $type = ADS_BINARY) : void
$prep->bindNull (string $name) : void
$prep->bind (string $name, mixed $value) : void // auto-detects PHP type
$prep->execute() : AdsStatement|true
$prep->paramCount() : int
$prep->close() : void
bind() type mapping:
| PHP type | ACE call |
|---|---|
null |
AdsSetNull |
bool |
AdsSetLogical |
int |
AdsSetLong |
float |
AdsSetDouble |
string |
AdsSetString |
bindMoney() note: ADS_MONEY fields store values as a 64-bit scaled integer.
The scale factor is set per-field in the table schema (commonly 4 decimal places,
so $10.00 must be passed as 100000). Use bindMoney() rather than bindDouble()
to avoid floating-point rounding errors.
bindString() and memo fields: bindString() works correctly for ADS_MEMO
and ADS_NMEMO parameters. ACE treats memo columns as variable-length text; pass
the full string including content longer than any VARCHAR limit.
bindBinary() note: Writes binary data to an ADS_BINARY (BLOB), ADS_IMAGE,
or ADS_RAW parameter in a single call. The $type constant selects the ACE blob
type: ADS_BINARY (6, default) for arbitrary binary data, ADS_IMAGE (7) for
bitmap/image fields. Pass the raw bytes as a PHP string. Do not use bindBinary()
for memo (ADS_MEMO) columns — use bindString() instead.
| Query type | Returns |
|---|---|
SELECT |
AdsStatement — cursor positioned at first row |
INSERT / UPDATE / DELETE / DDL |
true |
After a
SELECT execute(), the statement handle is transferred to the returnedAdsStatement. The prepared object is marked closed; callprepare()again for a subsequent execution of the same SQL.
Direct (non-SQL) table access.
$tbl = AdsTable::open(
$conn,
'C:\\data\\mytable.adt',
ADS_ADT, // table type
ADS_COMPATIBLE_LOCKING, // lock type
ADS_ANSI, // character set
ADS_SHARED // open mode
);
// Navigation
$tbl->gotoTop();
$tbl->gotoBottom();
$tbl->gotoRecord(int $n);
$tbl->skip(int $n = 1);
$tbl->atEOF() : bool
$tbl->atBOF() : bool
// Reading fields
$tbl->getString(string $field) : string
$tbl->getLong(string $field) : int
$tbl->getDouble(string $field) : float
$tbl->getLogical(string $field) : bool
$tbl->getRecord() : array // all fields as assoc array
// Writing fields (call writeRecord() to flush)
$tbl->setString(string $field, string $value)
$tbl->setLong(string $field, int $value)
$tbl->setDouble(string $field, float $value)
$tbl->setLogical(string $field, bool $value)
// Record operations
$tbl->recordCount() : int
$tbl->recordNum() : int
$tbl->appendRecord() // adds a blank record and locks it
$tbl->writeRecord() // flush pending changes
$tbl->cancelUpdate() // discard pending changes
$tbl->deleteRecord() // mark current record deleted
$tbl->close();$tx = $conn->beginTransaction();
$tx->isActive() : bool
$tx->commit() : void
$tx->rollback() : voidFull CRUD access to the SAP ACE data dictionary. Wraps AdsDDxxx functions directly — no SQL, no ODBC.
// Open independently (creates its own connection to the .add file)
$dd = AdsDictionary::open(
'\\\\server:6262\\share\\mydb.add', // path — required
ADS_REMOTE_SERVER, // server type — optional
'username', // user — optional
'password' // password — optional
);
// Or borrow an existing connection (does not disconnect on close)
$dd = AdsDictionary::fromConnection($conn);
$dd->close();$dd->getDatabaseProperty(int $prop) : string
$dd->setDatabaseProperty(int $prop, string $value) : void$dd->addTable(string $alias, string $path,
int $tableType = ADS_ADT, int $charType = ADS_ANSI,
string $indexPath = '', string $comment = '') : void
$dd->removeTable(string $alias, bool $deleteFiles = false) : void
$dd->getTableProperty(string $table, int $prop) : string
$dd->setTableProperty(string $table, int $prop, string $val): void$dd->getFieldProperty(string $table, string $field, int $prop) : string
$dd->setFieldProperty(string $table, string $field, int $prop, string $val): void$dd->addIndexFile(string $table, string $indexPath, string $comment = '') : void
$dd->removeIndexFile(string $table, string $indexPath, bool $del = false) : void
$dd->getIndexProperty(string $table, string $index, int $prop) : string
$dd->setIndexProperty(string $table, string $index, int $prop, string $val): void$dd->createUser(string $user, string $password = '',
string $group = '', string $desc = '') : void
$dd->deleteUser(string $user) : void
$dd->getUserProperty(string $user, int $prop) : string
$dd->setUserProperty(string $user, int $prop, string $val) : void
$dd->addUserToGroup(string $user, string $group) : void
$dd->removeUserFromGroup(string $user, string $group) : void
$dd->getUserTableRights(string $table, string $user) : int // bitmask
$dd->setUserTableRights(string $table, string $user, int $rights): void // revoke-all then grant$dd->createView(string $name, string $sql, string $comment = '') : void
$dd->dropView(string $name) : void
$dd->getViewProperty(string $view, int $prop) : string
$dd->setViewProperty(string $view, int $prop, string $val) : void$dd->createProcedure(string $name, string $container, string $procedure,
string $input = '', string $output = '',
string $comment = '') : void
$dd->dropProcedure(string $name) : void
$dd->getProcProperty(string $name, int $prop) : string
$dd->setProcProperty(string $name, int $prop, string $val): void// 7 required args; priority, comment, options are optional
$dd->createTrigger(string $name, string $table,
int $triggerType, int $eventTypes, int $containerType,
string $container, string $procedure,
int $priority = 1, string $comment = '', int $options = 0): void
$dd->dropTrigger(string $name) : void
$dd->getTriggerProperty(string $name, int $prop) : string
$dd->setTriggerProperty(...) // throws — not supported by SAP ACE// updateRule and deleteRule are optional (default 0)
$dd->createRefIntegrity(string $name, string $failTable,
string $parent, string $parentTag,
string $child, string $childTag,
int $updateRule = 0, int $deleteRule = 0): void
$dd->removeRefIntegrity(string $name) : void$dd->createLink(string $alias, string $path,
string $user = '', string $password = '') : void
$dd->dropLink(string $alias) : void
$dd->modifyLink(string $alias, string $path = '',
string $user = '', string $password = '') : void<?php
$dd = AdsDictionary::open('\\\\srv\\data\\mydb.add', ADS_REMOTE_SERVER, 'admin', 'secret');
// Register an existing physical table in the dictionary
$dd->addTable('invoices', 'C:\\data\\invoices.adt', ADS_ADT, ADS_ANSI,
'C:\\data\\invoices.cdx', 'Customer invoices');
// Set a display description
$dd->setTableProperty('invoices', ADS_DD_TABLE_DESCRIPTION, 'Customer invoice table');
// Create a view over it
$dd->createView('open_invoices',
"SELECT id, customer_id, amount FROM invoices WHERE paid = FALSE",
'Unpaid invoices');
// Create a stored procedure
$dd->createProcedure('sp_close_invoice', 'procs.dll', 'CloseInvoice',
'@invoice_id INTEGER', '', 'Mark an invoice as paid');
// Add a user and grant read access to the table
$dd->createUser('reports_user', 'rp@ss', 'readers', 'Read-only reporting account');
$dd->setUserTableRights('invoices', 'reports_user', ADS_READ_RIGHT);
// Set up referential integrity: invoices.customer_id → customers.id
$dd->createRefIntegrity('ri_inv_cust', 'ri_errors',
'customers', 'cust_pk', 'invoices', 'inv_fk',
ADS_RI_RESTRICT, ADS_RI_RESTRICT);
$dd->close();try {
$conn = AdsConnection::connect(['path' => '...']);
} catch (AdsException $e) {
echo $e->getMessage(); // human-readable ACE message (+ last-error detail)
echo $e->getCode(); // native ACE error code
}| Category | Constants |
|---|---|
| Server type | ADS_LOCAL_SERVER, ADS_REMOTE_SERVER |
| Table type | ADS_NTX, ADS_CDX, ADS_ADT, ADS_VFP |
| Character set | ADS_ANSI, ADS_OEM |
| Open mode | ADS_SHARED, ADS_EXCLUSIVE |
| Locking | ADS_COMPATIBLE_LOCKING, ADS_PROPRIETARY_LOCKING |
| Rights | ADS_CHECKRIGHTS, ADS_IGNORERIGHTS |
| Filters | ADS_RESPECTFILTERS, ADS_IGNOREFILTERS |
| String trim | ADS_TRIM, ADS_LTRIM, ADS_RTRIM |
| Field types | ADS_LOGICAL, ADS_NUMERIC, ADS_DATE, ADS_STRING, ADS_MEMO, ADS_BINARY, ADS_IMAGE, ADS_VARCHAR, ADS_DOUBLE, ADS_INTEGER, ADS_SHORTINT, ADS_TIME, ADS_TIMESTAMP, ADS_AUTOINC, ADS_RAW, ADS_CURDOUBLE, ADS_MONEY, ADS_ROWVERSION, ADS_MODTIME, ADS_NCHAR, ADS_NMEMO |
<?php
$conn = AdsConnection::connect([
'path' => '\\\\192.168.0.10:6262\\share\\mydb.add',
'user' => 'admin',
'password' => 'secret',
'serverType' => ADS_REMOTE_SERVER,
]);
$stmt = $conn->query(
"SELECT TOP 10 CustomerID, Name, Balance FROM customers ORDER BY Balance DESC"
);
echo $stmt->columnCount() . " columns, " . $stmt->rowCount() . " rows\n";
while (($row = $stmt->fetchAssoc()) !== false) {
printf("%-6d %-30s %10.2f\n",
$row['CustomerID'], trim($row['Name']), $row['Balance']);
}
$stmt->close();
$conn->close();<?php
$conn = AdsConnection::connect(['path' => '...']);
$tx = $conn->beginTransaction();
try {
$conn->execute("UPDATE accounts SET balance = balance - 100 WHERE id = 1");
$conn->execute("UPDATE accounts SET balance = balance + 100 WHERE id = 2");
$tx->commit();
} catch (AdsException $e) {
$tx->rollback();
throw $e;
}
$conn->close();<?php
$conn = AdsConnection::connect(['path' => '...']);
$tbl = AdsTable::open($conn, 'C:\\data\\inventory.adt', ADS_ADT,
ADS_COMPATIBLE_LOCKING, ADS_ANSI, ADS_SHARED);
$tbl->gotoTop();
while (!$tbl->atEOF()) {
$row = $tbl->getRecord();
echo $row['SKU'] . ': ' . $row['Description'] . "\n";
$tbl->skip();
}
$tbl->close();
$conn->close();<?php
$conn = AdsConnection::connect(['path' => '...']);
$tbl = AdsTable::open($conn, 'C:\\data\\inventory.adt', ADS_ADT,
ADS_COMPATIBLE_LOCKING, ADS_ANSI, ADS_EXCLUSIVE);
$tbl->appendRecord();
$tbl->setString('SKU', 'WIDGET-42');
$tbl->setString('Description', 'Blue Widget');
$tbl->setDouble('Price', 9.99);
$tbl->setLong ('Stock', 100);
$tbl->setLogical('Active', true);
$tbl->writeRecord();
$tbl->close();
$conn->close();Use prepare() instead of query() whenever query criteria come from user
input or application variables. Parameters are never interpreted as SQL, so
there is no risk of SQL injection.
<?php
$conn = AdsConnection::connect([
'path' => '\\\\192.168.0.10:6262\\share\\mydb.add',
'user' => 'admin',
'password' => 'secret',
'serverType' => ADS_REMOTE_SERVER,
]);
// Prepare once, bind, execute
$prep = $conn->prepare(
"SELECT CustomerID, Name, Balance
FROM customers
WHERE Status = :status
AND Balance > :minbal
ORDER BY Balance DESC"
);
$prep->bindString('status', 'ACTIVE');
$prep->bindDouble('minbal', 1000.00);
$stmt = $prep->execute(); // returns AdsStatement
echo $stmt->rowCount() . " customers found\n";
while (($row = $stmt->fetchAssoc()) !== false) {
printf("%6d %-30s %10.2f\n",
$row['CustomerID'], trim($row['Name']), $row['Balance']);
}
$stmt->close();
$conn->close();<?php
$conn = AdsConnection::connect(['path' => '...', 'serverType' => ADS_REMOTE_SERVER]);
$prep = $conn->prepare(
"INSERT INTO products (SKU, Description, Price, Stock, Active, CreatedDate)
VALUES (:sku, :desc, :price, :stock, :active, :created)"
);
$prep->bindString ('sku', 'WIDGET-99');
$prep->bindString ('desc', 'Deluxe Widget');
$prep->bindDouble ('price', 24.99);
$prep->bindInt ('stock', 250);
$prep->bindBool ('active', true);
$prep->bindDate ('created', '20260521'); // CCYYMMDD format
$prep->execute(); // INSERT → returns true
echo "Product inserted.\n";
$conn->close();Prepare the statement once, then re-bind and re-execute for each row.
This is significantly faster than issuing individual execute() calls with
concatenated SQL strings.
<?php
$conn = AdsConnection::connect(['path' => '...', 'serverType' => ADS_REMOTE_SERVER]);
$tx = $conn->beginTransaction();
$products = [
['SKU' => 'AAA-01', 'Description' => 'Alpha Widget', 'Price' => 9.99, 'Stock' => 100],
['SKU' => 'BBB-02', 'Description' => 'Beta Widget', 'Price' => 14.99, 'Stock' => 50],
['SKU' => 'CCC-03', 'Description' => 'Gamma Widget', 'Price' => 4.99, 'Stock' => 200],
];
try {
// Prepare the INSERT once outside the loop
$prep = $conn->prepare(
"INSERT INTO products (SKU, Description, Price, Stock)
VALUES (:sku, :desc, :price, :stock)"
);
foreach ($products as $p) {
$prep->bindString('sku', $p['SKU']);
$prep->bindString('desc', $p['Description']);
$prep->bindDouble('price', $p['Price']);
$prep->bindInt ('stock', $p['Stock']);
$prep->execute();
// Re-prepare after each execute() to reset the handle for the next iteration
$prep = $conn->prepare(
"INSERT INTO products (SKU, Description, Price, Stock)
VALUES (:sku, :desc, :price, :stock)"
);
}
$tx->commit();
echo count($products) . " products inserted.\n";
} catch (AdsException $e) {
$tx->rollback();
echo "Batch failed: " . $e->getMessage() . "\n";
}
$conn->close();Note: After
execute()the prepared handle is consumed (transferred to the returnedAdsStatementfor SELECT, or released for DML). Callprepare()again before the next iteration. For INSERT/UPDATE/DELETE loops, wrapping in a transaction also provides a substantial speed improvement over auto-commit.
<?php
$conn = AdsConnection::connect(['path' => '...', 'serverType' => ADS_REMOTE_SERVER]);
$tx = $conn->beginTransaction();
try {
// Deactivate all records in a category
$disable = $conn->prepare(
"UPDATE products SET Active = :flag WHERE CategoryID = :catid"
);
$disable->bindBool('flag', false);
$disable->bindInt ('catid', 7);
$disable->execute();
// Record the change in an audit log
$audit = $conn->prepare(
"INSERT INTO audit_log (Action, TableName, RecordID, ChangedAt)
VALUES (:action, :tbl, :recid, :ts)"
);
$audit->bindString ('action', 'DEACTIVATE');
$audit->bindString ('tbl', 'products');
$audit->bindInt ('recid', 7);
$audit->bindTimestamp('ts', date('Y-m-d H:i:s'));
$audit->execute();
$tx->commit();
echo "Category deactivated and audit entry written.\n";
} catch (AdsException $e) {
$tx->rollback();
echo "Update failed: " . $e->getMessage() . "\n";
throw $e;
}
$conn->close();bind() automatically selects the correct ACE setter based on the PHP type
of the value, including null. Use it when building generic data-access layers
where the type is determined at runtime.
<?php
$conn = AdsConnection::connect(['path' => '...', 'serverType' => ADS_REMOTE_SERVER]);
$prep = $conn->prepare(
"UPDATE customers
SET Phone = :phone,
FaxNo = :fax,
CreditLimit = :limit,
Active = :active
WHERE CustomerID = :id"
);
// $fax may be null if the customer has no fax number
$phone = '+1-555-0100';
$fax = null; // will bind as SQL NULL
$limit = 5000.00;
$active = true;
$customerId = 42;
$prep->bind('phone', $phone); // string → AdsSetString
$prep->bind('fax', $fax); // null → AdsSetNull
$prep->bind('limit', $limit); // float → AdsSetDouble
$prep->bind('active', $active); // bool → AdsSetLogical
$prep->bind('id', $customerId); // int → AdsSetLong
$prep->execute();
echo "Customer updated.\n";
$conn->close();ADS_MONEY fields are stored internally as a 64-bit scaled integer, not a
floating-point number. The scale (number of implied decimal places) is defined
per-field in the table schema — commonly 4, meaning the value is stored in
units of 0.0001 of the currency. Always use bindMoney() for money fields;
bindDouble() will corrupt values through floating-point rounding.
<?php
$conn = AdsConnection::connect(['path' => '...', 'serverType' => ADS_REMOTE_SERVER]);
// Helper: convert a decimal dollar amount to the ADS_MONEY integer
// representation with 4 implied decimal places.
function moneyToScaled(float $dollars): int
{
return (int) round($dollars * 10000);
}
$prep = $conn->prepare(
"INSERT INTO invoices (CustomerID, InvoiceDate, Amount, Tax, Total)
VALUES (:cid, :invdate, :amount, :tax, :total)"
);
$amount = 1234.56; // $1,234.56
$tax = $amount * 0.08;
$total = $amount + $tax;
$prep->bindInt ('cid', 42);
$prep->bindDate ('invdate', date('Ymd')); // CCYYMMDD
$prep->bindMoney ('amount', moneyToScaled($amount)); // 12345600
$prep->bindMoney ('tax', moneyToScaled($tax)); // 988448 (≈ $98.8448)
$prep->bindMoney ('total', moneyToScaled($total)); // 13334048
$prep->execute();
echo "Invoice inserted.\n";
// Reading money back: divide by scale factor to get decimal value
$prep2 = $conn->prepare(
"SELECT Amount, Tax, Total FROM invoices WHERE CustomerID = :cid ORDER BY InvoiceDate DESC"
);
$prep2->bindInt('cid', 42);
$stmt = $prep2->execute();
while (($row = $stmt->fetchAssoc()) !== false) {
// ADS returns money fields as strings when fetched via AdsGetString
printf("Amount: %s Tax: %s Total: %s\n",
$row['Amount'], $row['Tax'], $row['Total']);
}
$stmt->close();
$conn->close();Use bindBinary() for ADS_BINARY (BLOB), ADS_IMAGE, and ADS_RAW fields.
Pass the raw bytes as a PHP string. For image fields, pass ADS_IMAGE as the
$type argument; for arbitrary binary data, use the default ADS_BINARY.
<?php
$conn = AdsConnection::connect(['path' => '...', 'serverType' => ADS_REMOTE_SERVER]);
// ---- Store an image from disk ----
$imageData = file_get_contents('C:\\images\\product_42.jpg');
if ($imageData === false) {
die("Could not read image file.\n");
}
$prep = $conn->prepare(
"UPDATE products
SET Photo = :photo,
PhotoSize = :size,
PhotoFormat = :fmt
WHERE ProductID = :pid"
);
$prep->bindBinary('photo', $imageData, ADS_IMAGE); // store as image BLOB
$prep->bindInt ('size', strlen($imageData));
$prep->bindString('fmt', 'JPEG');
$prep->bindInt ('pid', 42);
$prep->execute();
echo "Photo stored (" . strlen($imageData) . " bytes).\n";
// ---- Store a raw binary document (PDF, etc.) ----
$pdfData = file_get_contents('C:\\docs\\contract_7.pdf');
$prep2 = $conn->prepare(
"INSERT INTO documents (CustomerID, DocType, Content, FileSize)
VALUES (:cid, :dtype, :content, :sz)"
);
$prep2->bindInt ('cid', 100);
$prep2->bindString('dtype', 'PDF');
$prep2->bindBinary('content', $pdfData); // ADS_BINARY default
$prep2->bindInt ('sz', strlen($pdfData));
$prep2->execute();
echo "Document stored (" . strlen($pdfData) . " bytes).\n";
$conn->close();If running under Apache with mod_php:
- Install the extension as above (
C:\php\ext\php_ads.dll). - Make sure
C:\php\(where the ACE DLLs live) is in the systemPATH, not just the user PATH — Apache runs as a service under SYSTEM. - Restart Apache:
httpd -k restartor via Services.
# C:\Apache24\conf\httpd.conf
LoadModule php_module "C:/php/php8apache2_4.dll"
PHPIniDir "C:/php"
AddType application/x-httpd-php .php| Symptom | Cause | Fix |
|---|---|---|
PHP Warning: PHP Startup: Unable to load dynamic library 'php_ads' |
DLL not found or ACE DLLs missing | Check C:\php\ext\php_ads.dll exists; copy ACE DLLs to C:\php\ |
Extension loads but AdsConnection::connect() throws immediately |
ACE DLLs not on PATH at load time | Copy ace64.dll, adsloc64.dll, axcws64.dll, aicu64.dll to the php.exe directory |
Build fails with C1083: Cannot open include file: 'php.h' |
Dev pack path wrong | Check PHP_DEVPACK in Makefile.win |
| Extension loads but crashes (0xC0000005) | ZTS TSRM cache not initialised | Ensure ZEND_TSRMLS_CACHE_EXTERN() is in php_ads.h and ZEND_TSRMLS_CACHE_UPDATE() is called in both MINIT and RINIT |
AdsException [5091] or similar ACE error at connect |
Server not reachable or wrong path | Verify ADS server is running and the path uses the correct UNC format |
This extension is provided as-is for use with licensed copies of Advantage Database Server.
The ACE SDK headers and libraries (acesdk/) are copyright SAP / iAnywhere Solutions and
are included under the terms of the ADS developer license — they must not be redistributed
separately.