Skip to content

crazy-goat/fdb-php

Repository files navigation

FoundationDB PHP Client

CI License: MIT

A full-featured FoundationDB client for PHP, built on PHP FFI bindings to libfdb_c.so.

Features

  • 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() and hasIncompleteVersionstamp()
  • Subspace — key prefix management with packWithVersionstamp support
  • 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 monitoringgetMainThreadBusyness(), getClientStatus()
  • Locality APIgetBoundaryKeys() for data distribution analysis
  • Key utilitiesstrinc(), printable(), prefixRange()
  • Error predicatesisRetryable(), isMaybeCommitted(), isRetryableNotCommitted()
  • Connection string supportopenWithConnectionString()
  • Explicit lifecycle managementDatabase::close()

Requirements

  • PHP 8.2+
  • ext-ffi — PHP FFI extension
  • ext-gmp — for arbitrary-precision integers in the tuple layer (optional)
  • libfdb_c.so — FoundationDB C client library (install guide)

Installation

composer require crazy-goat/fdb-php

Make 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.deb

Quick Start

use 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();
});

Usage

Range Reads

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,
));

Tuple Layer

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']);

Subspace

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']
    }
});

Directory Layer

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']);

Atomic Operations

// 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');

Snapshot Reads

// 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');
});

Watches

// 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');

Multi-Tenancy

// 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 Client

$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');

Options

// 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();
    // ...
});

Error Handling

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?
}

Key Utilities

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']

Database Monitoring

$busyness = $db->getMainThreadBusyness(); // 0.0 to 1.0
$status = $db->getClientStatus();          // JSON string

Locality API

use CrazyGoat\FoundationDB\Locality;

$boundaries = Locality::getBoundaryKeys($db, "\x00", "\xFF");

Connection Strings

$db = FDB::openWithConnectionString('my_cluster:abc123@127.0.0.1:4500');

Documentation

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.

Architecture

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)

Development

Prerequisites

  • PHP 8.2+ with ext-ffi
  • Docker + Docker Compose (for integration tests)
  • Composer

Setup

composer install

Linting

composer 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-run

Testing

composer 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 -v

License

MIT — see LICENSE.

About

Full-featured FoundationDB client for PHP via FFI — transactions, atomic ops, watches, directory layer, multi-tenancy, admin API

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages