A full-featured FoundationDB client for PHP, built on PHP FFI bindings to libfdb_c.so.
- Full transactional API — get, set, clear, commit, with automatic retry loop and conflict resolution
- Range reads — lazy Generator-based iteration and eager fetching (
getRangeAll) - Atomic operations — add, bitAnd, bitOr, bitXor, max, min, byteMax, byteMin, compareAndClear, versionstamps
- Snapshot reads — conflict-free reads within transactions
- Watches — get notified when a key changes (
watch,getAndWatch,setAndWatch,clearAndWatch) - Tuple layer — binary-compatible with Python/Go/Java tuple encoding, with
compare()andhasIncompleteVersionstamp() - Subspace — key prefix management with
packWithVersionstampsupport - Directory layer — hierarchical namespace management with HighContentionAllocator and partitions
- Typed options — fluent API for network, database, and transaction options
- Multi-tenancy — Tenant support with
getId()and scoped transactions - Admin client — cluster administration (tenant management, server exclusion, configuration, status, force recovery)
- Database monitoring —
getMainThreadBusyness(),getClientStatus() - Locality API —
getBoundaryKeys()for data distribution analysis - Key utilities —
strinc(),printable(),prefixRange() - Error predicates —
isRetryable(),isMaybeCommitted(),isRetryableNotCommitted() - Connection string support —
openWithConnectionString() - Explicit lifecycle management —
Database::close()
- PHP 8.2+
ext-ffi— PHP FFI extensionext-gmp— for arbitrary-precision integers in the tuple layer (optional)libfdb_c.so— FoundationDB C client library (install guide)
composer require crazy-goat/fdb-phpMake sure libfdb_c.so is installed and accessible. On Ubuntu/Debian:
wget https://github.com/apple/foundationdb/releases/download/7.3.75/foundationdb-clients_7.3.75-1_amd64.deb
sudo dpkg -i foundationdb-clients_7.3.75-1_amd64.debuse CrazyGoat\FoundationDB\FoundationDB as FDB;
use CrazyGoat\FoundationDB\Transaction;
FDB::apiVersion(730);
$db = FDB::open();
// Simple read/write (auto-transact with retry)
$db->set('hello', 'world');
echo $db->get('hello'); // "world"
// Transactional
$db->transact(function (Transaction $tr) {
$tr->set('key1', 'value1');
$tr->set('key2', 'value2');
$value = $tr->get('key1')->await();
});use CrazyGoat\FoundationDB\RangeOptions;
// Lazy iteration (Generator) — memory-efficient for large ranges
$db->transact(function (Transaction $tr) {
foreach ($tr->getRangeStartsWith('users/') as $kv) {
echo $kv->key . ' = ' . $kv->value . "\n";
}
});
// Eager fetching — all results in memory at once
$results = $db->getRangeAllStartsWith('users/');
// With options
$results = $db->getRangeStartsWith('users/', new RangeOptions(
limit: 100,
reverse: true,
));use CrazyGoat\FoundationDB\Tuple\Tuple;
$packed = Tuple::pack(['users', 42, 'name']);
$unpacked = Tuple::unpack($packed); // ['users', 42, 'name']
// Sort-order preserving — binary comparison matches logical order
assert(Tuple::pack([1]) < Tuple::pack([2]));
assert(Tuple::pack(['a']) < Tuple::pack(['b']));
// Compare tuples
Tuple::compare(['users', 1], ['users', 2]); // -1
// Get range boundaries for a tuple prefix
[$begin, $end] = Tuple::range(['users']);use CrazyGoat\FoundationDB\Subspace;
$users = new Subspace(['users']);
$db->transact(function (Transaction $tr) use ($users) {
$tr->set($users->pack([42, 'name']), 'Alice');
$tr->set($users->pack([42, 'email']), 'alice@example.com');
foreach ($tr->getRangeStartsWith($users->pack([42])) as $kv) {
$tuple = $users->unpack($kv->key);
// [42, 'name'] or [42, 'email']
}
});use CrazyGoat\FoundationDB\Directory\DirectoryLayer;
$dir = new DirectoryLayer();
$users = $dir->createOrOpen($db, ['app', 'users']);
$orders = $dir->createOrOpen($db, ['app', 'orders']);
// Each directory gets a unique short prefix — no key collisions
$db->set($users->pack([42, 'name']), 'Alice');
$db->set($orders->pack([1001]), 'order data');
// List, move, remove
$subdirs = $dir->list($db, ['app']); // ['users', 'orders']
$dir->move($db, ['app', 'users'], ['app', 'customers']);
$dir->remove($db, ['app', 'orders']);// Integer atomic ops accept int directly — no pack() needed
$db->add('counter', 1);
$db->add('counter', 10);
$db->max('high_score', 999);
$db->min('response_time', 42);
// Bitwise operations
$db->bitOr('flags', 0b00001100);
$db->bitAnd('flags', 0b11110011);
$db->bitXor('flags', 0b00000001);
// Read integer values — no unpack() needed
$count = $db->getInt('counter'); // ?int
$flags = $db->getInt('flags'); // ?int
// Compare and clear (raw bytes)
$db->compareAndClear('temp', 'expected_value');// Read-only transaction with snapshot isolation
$value = $db->readTransact(function ($snap) {
return $snap->get('key')->await();
});
// Or use snapshot within a write transaction
$db->transact(function (Transaction $tr) {
// No conflict range added — won't cause transaction conflicts
$value = $tr->snapshot()->get('frequently-read-key')->await();
// Selectively add conflict for keys you care about
$tr->addReadConflictKey('important-key');
});// Watch a key — returns FutureVoid that resolves when value changes
$watch = $db->watch('config:version');
// Get current value AND set up watch atomically
[$value, $watch] = $db->getAndWatch('config:version');
echo "Current: {$value}\n";
// Set value AND watch for future changes
$watch = $db->setAndWatch('config:version', '2');
// Clear value AND watch
$watch = $db->clearAndWatch('config:version');// Create tenant via admin client
$db->admin()->createTenant('tenant-a');
// Open tenant — transactions are scoped to tenant's key space
$tenant = $db->openTenant('tenant-a');
$tr = $tenant->createTransaction();
$tr->set('key', 'value'); // isolated to tenant-a
$tr->commit()->await();
// Get tenant ID
$id = $tenant->getId();$admin = $db->admin();
// Tenant management
$admin->createTenant('new-tenant');
$admin->deleteTenant('old-tenant');
$tenants = $admin->listTenants();
// Cluster status
$status = $admin->getClusterStatus(); // array<string, mixed>
$isConsistent = $admin->consistencyCheck();
// Server management
$admin->excludeServer('10.0.0.1:4500');
$admin->includeServer('10.0.0.1:4500');
$admin->rebootWorker('10.0.0.1:4500');
// Configuration
$admin->configure('double ssd');// Network options (before opening database)
FDB::networkOptions()
->setTraceEnable('/var/log/fdb/')
->setTraceFormat('json');
// Database-level defaults
$db->options()
->setTransactionTimeout(10_000)
->setTransactionRetryLimit(5);
// Transaction options (fluent API)
$db->transact(function (Transaction $tr) {
$tr->options()
->setTimeout(5000)
->setRetryLimit(3)
->setPriorityBatch();
// ...
});use CrazyGoat\FoundationDB\FDBException;
try {
$db->transact(function (Transaction $tr) {
$tr->set('key', 'value');
});
} catch (FDBException $e) {
echo "FDB error {$e->fdbCode}: {$e->getMessage()}\n";
$e->isRetryable(); // safe to retry?
$e->isMaybeCommitted(); // may have committed?
$e->isRetryableNotCommitted(); // safe to retry, definitely not committed?
}use CrazyGoat\FoundationDB\KeyUtil;
$end = KeyUtil::strinc('prefix'); // increment last byte
$readable = KeyUtil::printable("\x00\x01\xFF"); // '\x00\x01\xff'
[$begin, $end] = KeyUtil::prefixRange('users/'); // ['users/', 'users0']$busyness = $db->getMainThreadBusyness(); // 0.0 to 1.0
$status = $db->getClientStatus(); // JSON stringuse CrazyGoat\FoundationDB\Locality;
$boundaries = Locality::getBoundaryKeys($db, "\x00", "\xFF");$db = FDB::openWithConnectionString('my_cluster:abc123@127.0.0.1:4500');Detailed documentation is available in the docs/ directory:
| Guide | Description |
|---|---|
| Getting Started | Installation, configuration, first program |
| Transactions | Transaction lifecycle, retry loops, snapshot reads, conflict ranges |
| Tuple Layer | Binary encoding, types, comparison, versionstamps |
| Subspaces | Key prefix management, nesting, range queries |
| Directory Layer | Hierarchical namespaces, partitions, HighContentionAllocator |
| Range Reads | Lazy vs eager fetching, KeySelector, StreamingMode |
| Atomic Operations | Counters, bitwise ops, compare-and-clear |
| Watches | Key monitoring, getAndWatch, setAndWatch |
| Tenants | Multi-tenancy, isolated key spaces |
| Admin Client | Cluster administration, tenant management, status |
| Options | Network, database, and transaction options |
| Error Handling | FDBException, error predicates, retry logic |
| Advanced | Locality, KeyUtil, monitoring, Futures, lifecycle |
See also the examples/ directory for runnable PHP scripts.
FoundationDB (entry point)
├── NativeClient (FFI singleton: libfdb_c + libpthread + libdl)
├── Database (FDBDatabase*, retry loop, convenience methods)
│ ├── Transaction (full read/write/atomic/commit)
│ │ ├── ReadTransaction (read operations base)
│ │ └── Snapshot (conflict-free reads)
│ ├── Tenant (multi-tenancy, scoped transactions)
│ └── AdminClient (cluster administration via Special Keys)
├── Future hierarchy (8 types wrapping FDBFuture*)
├── Tuple layer (binary encoding, cross-language compatible)
├── Subspace (key prefix management)
├── Directory layer
│ ├── DirectoryLayer (create/open/move/remove/list/exists)
│ ├── DirectorySubspace (directory + subspace)
│ ├── DirectoryPartition (isolated sub-tree)
│ └── HighContentionAllocator (unique prefix allocation)
├── Locality (data distribution, boundary keys)
├── KeyUtil (strinc, printable, prefixRange)
├── Error handling (FDBException, error predicates)
└── Option wrappers (NetworkOptions, DatabaseOptions, TransactionOptions)
- PHP 8.2+ with
ext-ffi - Docker + Docker Compose (for integration tests)
- Composer
composer installcomposer lint # PHPStan + PHPCS + Rector (dry-run)
composer lint:fix # Auto-fix code style
composer phpstan # PHPStan level 9
composer cs # PHP CodeSniffer
composer rector # Rector dry-runcomposer test:unit # 317 unit tests (no FDB required)Integration tests require a running FoundationDB instance:
docker compose up -d
docker compose exec php vendor/bin/phpunit --testsuite=Integration
docker compose down -vMIT — see LICENSE.