UmaDB PHP Extension
January 5, 2026 ยท View on GitHub
PHP bindings for UmaDB event store, built with Rust using ext-php-rs.
UmaDB is a specialist event store for Dynamic Consistency Boundaries (DCB), enabling flexible, query-driven append conditions for implementing business rules without hardcoded aggregate boundaries.
Note
See wwwision/dcb-eventstore-umadb for a more convenient and typesafe API
Features
- โจ Full DCB API - Complete implementation of the Dynamic Consistency Boundaries specification
- ๐ High Performance - Rust-powered with zero-copy data handling
- ๐ Type Safe - Leverages PHP 8.0+ type system
- ๐ช Sync Client - Blocking operations suitable for traditional PHP applications
- ๐ฏ Simple API - Read, append, head operations with intuitive builder patterns
- ๐ Idempotent Appends - UUID-based event deduplication
- ๐ท๏ธ Tag-based Filtering - Efficient event queries
- โก Optimistic Concurrency - Position-based conflict detection
- ๐งต Thread Safe (ZTS) - Full support for FrankenPHP and multi-threaded environments
Requirements
- PHP 8.4 or higher
- UmaDB Server running and accessible
- Rust 1.70 or higher (only for building from source)
Installation
Option 1: PIE (Recommended)
Note: Requires Rust 1.70+ and PIE installed. See PIE.md for details.
# Install Rust (if not already installed)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Install PIE
wget https://github.com/php/pie/releases/latest/download/pie.phar
chmod +x pie.phar && sudo mv pie.phar /usr/local/bin/pie
# Install extension
pie install wwwision/umadb-php
Option 2: Pre-built Binaries
Download the pre-compiled extension for your platform from the latest release:
Linux (x86_64):
wget https://github.com/bwaidelich/umadb-php/releases/latest/download/umadb-linux-x86_64.so
sudo cp umadb-linux-x86_64.so $(php-config --extension-dir)/umadb.so
macOS (Intel):
wget https://github.com/bwaidelich/umadb-php/releases/latest/download/umadb-macos-x86_64.dylib
sudo cp umadb-macos-x86_64.dylib $(php-config --extension-dir)/umadb.so
macOS (Apple Silicon):
wget https://github.com/bwaidelich/umadb-php/releases/latest/download/umadb-macos-arm64.dylib
sudo cp umadb-macos-arm64.dylib $(php-config --extension-dir)/umadb.so
Verify checksum (optional but recommended):
# Download checksum file
wget https://github.com/bwaidelich/umadb-php/releases/latest/download/umadb-linux-x86_64.so.sha256
# Verify (Linux/macOS)
shasum -a 256 -c umadb-linux-x86_64.so.sha256
Option 3: Building from Source
# Clone the repository
git clone https://github.com/bwaidelich/umadb-php.git
cd umadb-php
# Build the extension
./build.sh
# Or use Make
make build
# Install to PHP extension directory
sudo make install
See INSTALL.md for detailed instructions.
Enable the Extension
After installation (any option), add to your php.ini:
extension=umadb.so
Or for CLI only, use:
php -d extension=umadb.so your-script.php
Verify installation:
php -m | grep umadb
Quick Start
<?php
use UmaDB\Client;
use UmaDB\Event;
// Connect to UmaDB server
$client = new Client('http://localhost:50051');
// Create an event
$event = new Event(
event_type: 'UserCreated',
data: json_encode(['userId' => '12345', 'name' => 'Alice']),
tags: ['user:12345']
);
// Append event
$position = $client->append([$event]);
echo "Event appended at position: {$position}\n";
// Read all events
$events = $client->read();
foreach ($events as $seqEvent) {
echo "[{$seqEvent->position}] {$seqEvent->event->event_type}\n";
}
// Get current head
$head = $client->head();
echo "Current head: {$head}\n";
API Reference
Client
Constructor
new Client(
string $url,
?string $ca_path = null,
?int $batch_size = null
?string $api_key = null
)
Parameters:
$url- Server URL (e.g.,http://localhost:50051orhttps://server:50051)$ca_path- Optional path to CA certificate for TLS (self-signed certs)$batch_size- Optional batch size hint for reading events (default: server decides)$api_key- Optional API key for authentication
Note on Named Arguments: Parameter names use snake_case (not camelCase). When using named arguments, all preceding optional parameters must be provided explicitly (even as null) due to ext-php-rs limitations.
Examples:
// Basic connection
$client = new Client('http://localhost:50051');
// TLS with self-signed certificate
$client = new Client('https://localhost:50051', ca_path: '/path/to/ca.pem');
// Custom batch size (must provide all parameters explicitly)
$client = new Client('http://localhost:50051', ca_path: null, batch_size: 100);
// API key (must provide all parameters explicitly)
$client = new Client('http://localhost:50051', ca_path: null, batch_size: null, api_key: 'secret-key');
read()
public function read(
?Query $query = null,
?int $start = null,
?bool $backwards = false,
?int $limit = null,
?bool $subscribe = false
): array
Reads events from the event store.
Parameters:
$query- Optional query to filter events by type and tags$start- Starting position (inclusive if forward, exclusive if backward)$backwards- Read backwards from start position$limit- Maximum number of events to return$subscribe- Subscribe to new events (streaming mode)
Returns: Array of SequencedEvent objects
Examples:
// Read all events
$events = $client->read();
// Read with query
$query = new Query([
new QueryItem(types: ['OrderCreated'], tags: ['order:1234'])
]);
$events = $client->read(query: $query);
// Read backwards with limit
$events = $client->read(start: 1000, backwards: true, limit: 10);
// Subscribe to new events
$events = $client->read(subscribe: true);
append()
public function append(
array $events,
?AppendCondition $condition = null
): int
Appends events to the event store.
Parameters:
$events- Array ofEventobjects to append$condition- Optional append condition for consistency enforcement
Returns: Position of the last appended event
Throws: IntegrityException if append condition fails
Examples:
// Simple append
$event = new Event('UserCreated', $data, ['user:1234']);
$position = $client->append([$event]);
// Append with condition
$condition = new AppendCondition($query, after: $head);
$position = $client->append([$event], $condition);
// Append multiple events
$position = $client->append([
new Event('OrderCreated', $data1, ['order:1234']),
new Event('OrderPaid', $data2, ['order:1234', 'payment:4321']),
]);
head()
public function head(): ?int
Returns the current head position of the event store, or null if empty.
Example:
$head = $client->head();
if ($head === null) {
echo "Store is empty\n";
} else {
echo "Last event at position: {$head}\n";
}
Event
new Event(
string $event_type,
string $data,
?array $tags = null,
?string $uuid = null
)
Represents an event in the event store.
Properties:
string $event_type- Event type identifier (read-only)string $data- Binary event data (read-only)array $tags- Tags for filtering (read-only)?string $uuid- Optional UUID for idempotency (read-only)
Example:
$event = new Event(
event_type: 'UserRegistered',
data: json_encode(['userId' => '123', 'email' => 'user@example.com']),
tags: ['user:123', 'email:' . sha1('user@example.com')],
uuid: '550e8400-e29b-41d4-a716-446655440000'
);
SequencedEvent
Represents an event with its position in the stream.
Properties:
Event $event- The event object (read-only)int $position- Position in the stream (read-only)
Note: This class is returned by read() and cannot be instantiated directly.
Query
new Query(?array $items = null)
A query for filtering events. An event matches if it matches any query item (OR logic).
Parameters:
$items- Array ofQueryItemobjects
Example:
$query = new Query([
new QueryItem(types: ['UserCreated'], tags: ['user:1234']),
new QueryItem(types: ['UserUpdated'], tags: ['user:1235']),
]);
QueryItem
new QueryItem(
?array $types = null,
?array $tags = null
)
A query item specifying event types and tags to match.
Matching Rules:
- Event type must be in
$types(or$typesis empty/null = match all types) - All tags in
$tagsmust be present in the event tags (AND logic)
Parameters:
$types- Array of event type strings to match$tags- Array of tag strings that must all be present
Examples:
// Match all "OrderCreated" events
$item = new QueryItem(types: ['OrderCreated']);
// Match events with specific tags
$item = new QueryItem(tags: ['order', 'order:12345']);
// Match specific types with specific tags
$item = new QueryItem(
types: ['OrderPaid', 'OrderShipped'],
tags: ['order:12345']
);
// Match all events (no filter)
$item = new QueryItem();
AppendCondition
new AppendCondition(
Query $fail_if_events_match,
?int $after = null
)
Condition for conditional appends, enabling optimistic concurrency control and business rule enforcement.
Parameters:
$fail_if_events_match- Query that must not match any existing events$after- Optional position constraint (fail if events exist after this position)
Examples:
// Prevent duplicate events
$condition = new AppendCondition(
fail_if_events_match: new Query([
new QueryItem(types: ['UserRegistered'], tags: ['user:1234'])
])
);
// Optimistic concurrency (position-based)
$head = $client->head();
$condition = new AppendCondition(
fail_if_events_match: new Query([]),
after: $head
);
// Combined: business rule + position check
$condition = new AppendCondition(
fail_if_events_match: $boundaryQuery,
after: $lastKnownPosition
);
Exception Classes
All exceptions extend PHP's base Exception class and are in the UmaDB\Exception namespace:
UmaDB\Exception\IntegrityException- Append condition failedUmaDB\Exception\TransportException- gRPC/network errorsUmaDB\Exception\CorruptionException- Data corruption detectedUmaDB\Exception\IoException- I/O errorsUmaDB\Exception\UmaDBException- Generic UmaDB error
Example:
use UmaDB\Exception\IntegrityException;
try {
$client->append([$event], $condition);
} catch (IntegrityException $e) {
echo "Append condition failed: {$e->getMessage()}\n";
}
Usage Examples
Idempotent Appends
Use UUIDs to make appends idempotent:
$uuid = '550e8400-e29b-41d4-a716-446655440000';
$event = new Event('OrderCreated', $data, ['order:1234'], $uuid);
// First append
$position1 = $client->append([$event]); // Returns position 100
// Retry (e.g., after network failure) - same UUID
$position2 = $client->append([$event]); // Returns position 100 (same!)
assert($position1 === $position2); // true
Prevent Duplicate Email Registration
$email = 'alice@example.com';
$emailHash = sha1($email);
// Define consistency boundary
$boundaryQuery = new Query([
new QueryItem(types: ['UserRegistered'], tags: ["email:{$emailHash}"])
]);
// Read current state
$head = $client->head();
// Create append condition
$condition = new AppendCondition($boundaryQuery, $head);
// Try to register
$event = new Event(
'UserRegistered',
json_encode(['email' => $email, 'name' => 'Alice'}),
["email:$emailHash"]
);
try {
$position = $client->append([$event], $condition);
echo "User registered successfully\n";
} catch (IntegrityException $e) {
echo "Email already registered\n";
}
Multi-step Workflow Coordination
$workflowId = 'workflow-123';
// Step 1: Start workflow
$event1 = new Event(
'WorkflowStarted',
json_encode(['workflowId' => $workflowId]),
["workflow:{$workflowId}", 'step:1']
);
$client->append([$event1]);
// Step 2: Prevent duplicate execution
$step2Boundary = new Query([
new QueryItem(types: ['WorkflowStep2Completed'], tags: ["workflow:{$workflowId}"])
]);
$head = $client->head();
$condition = new AppendCondition($step2Boundary, $head);
$event2 = new Event(
'WorkflowStep2Completed',
json_encode(['workflowId' => $workflowId, 'result' => 'success']),
["workflow:{$workflowId}", 'step:2']
);
$client->append([$event2], $condition); // Only succeeds once
Query Filtering
// Query by event type
$query = new Query([
new QueryItem(types: ['OrderCreated', 'OrderUpdated'])
]);
$orderEvents = $client->read(query: $query);
// Query by tags
$query = new Query([
new QueryItem(tags: ['order:12345'])
]);
$specificOrderEvents = $client->read(query: $query);
// Complex query (OR logic between items)
$query = new Query([
// Match user events
new QueryItem(types: ['UserCreated'], tags: ['user:1234']),
// OR payment events
new QueryItem(types: ['PaymentProcessed'], tags: ['payment:4321']),
]);
$events = $client->read(query: $query);
Development
Building
# Build release version
make build
# Build debug version
make build-dev
# Run Rust tests
make test-rust
# Run clippy
make clippy
# Format code
make fmt
Testing
# Install PHP dependencies
composer install
# Run PHP tests (requires running UmaDB server)
make test
# Or directly with PHPUnit
vendor/bin/phpunit
Running Examples
Start UmaDB server:
# In UmaDB repository
cargo run --bin umadb -- --listen 127.0.0.1:50051 --db-path /tmp/umadb-test.db
Run examples:
php examples/basic.php
php examples/query.php
php examples/consistency.php
Architecture
This extension uses ext-php-rs to create Rust-powered PHP extensions with:
- Zero-copy data transfer where possible
- Type-safe FFI between PHP and Rust
- Automatic memory management via reference counting
- Native PHP exception handling
- Thread-safe design for ZTS and FrankenPHP compatibility
The extension wraps the umadb-client Rust crate, providing a synchronous client that internally manages a Tokio runtime for async gRPC operations.
FrankenPHP & Multi-Threading
The extension fully supports FrankenPHP and other multi-threaded PHP environments:
- Built with ZTS (Zend Thread Safety) support enabled
- Thread-safe initialization using Rust's
std::sync::Once - Each client instance is independent and thread-safe
- No global mutable state
When compiled against FrankenPHP's ZTS-enabled PHP, the extension automatically uses thread-safe code paths. No special configuration is needed.
Comparison with Python Bindings
Similar to the Python bindings (umadb package), this PHP extension:
- โ Exposes the same DCB API
- โ Uses synchronous client only (blocking operations)
- โ Pre-collects read results into arrays/lists
- โ Provides idiomatic language bindings
Differences:
- PHP extension is compiled and loaded as a native extension
- Python uses PyO3 and Maturin for packaging
- PHP has no async/await (yet), so sync-only is natural
License
Licensed under the MIT License. See LICENSE-MIT for details.
Contributing
Contributions are welcome! Please ensure:
- Code is formatted:
make fmt - Clippy passes:
make clippy - Tests pass:
make testandmake test-rust - Examples run successfully