A modern PHP client library for KurrentDB (formerly EventStoreDB) HTTP API, designed for event sourcing applications.
Note: This library uses the HTTP API. For TCP integration, see prooph/event-store-client.
- âś… Support for KurrentDB HTTP API
- âś… Event stream management (read, write, delete)
- âś… Optimistic concurrency control
- âś… Stream iteration (forward and backward)
- âś… Batch operations for performance
- âś… Built-in HTTP caching support
- âś… PSR-7 and PSR-18 compliant
- âś… Type-safe with PHP 8.4 features
- âś… Comprehensive error handling
The library follows a clean architecture with a facade pattern that promotes separation of concerns and follows SOLID principles:
EventStore acts as a facade that delegates operations to specialized services:
- StreamReader - Handles all stream reading operations
- StreamWriter - Manages stream writing and deletion operations
- StreamIteratorFactory - Creates stream iterators for navigation
EventStoreFactory provides the recommended way to instantiate EventStore with proper dependency injection and connection validation:
// Simple creation with default dependencies
$eventStore = EventStoreFactory::create($uri);
// With custom HTTP client
$eventStore = EventStoreFactory::createWithHttpClient($uri, $httpClient);
// With all custom dependencies
$eventStore = EventStoreFactory::createWithDependencies(
$uri,
$httpClient,
$streamReader,
$streamWriter,
$streamIteratorFactory
);- Testability - Each service can be mocked independently
- Separation of Concerns - Clear boundaries between reading, writing, and iteration
- SOLID Principles - Interface segregation and dependency inversion
- Maintainability - Easier to extend and modify individual components
- Type Safety - Strong typing throughout the service layer
- PHP 8.4 or higher
- KurrentDB server (HTTP API enabled)
composer require friendsofouro/kurrentdb-coreFor the complete package with additional integrations:
composer require friendsofouro/kurrentdbgit clone [email protected]:FriendsOfOuro/kurrentdb-php-core.git
cd kurrentdb-php-core
# Start the environment
make up
# Install dependencies
make install
# Run tests to verify setup
make testuse KurrentDB\EventStoreFactory;
// Create EventStore using factory (recommended)
$eventStore = EventStoreFactory::create('http://admin:[email protected]:2113');
// Or with custom HTTP client
use KurrentDB\Http\GuzzleHttpClient;
$httpClient = new GuzzleHttpClient();
$eventStore = EventStoreFactory::createWithHttpClient(
'http://admin:[email protected]:2113',
$httpClient
);use KurrentDB\WritableEvent;
use KurrentDB\WritableEventCollection;
// Write a single event
$event = WritableEvent::newInstance(
'UserRegistered',
['userId' => '123', 'email' => '[email protected]'],
['timestamp' => time()] // optional metadata
);
$version = $eventStore->writeToStream('user-123', $event);
// Write multiple events atomically
$events = new WritableEventCollection([
WritableEvent::newInstance('OrderPlaced', ['orderId' => '456']),
WritableEvent::newInstance('PaymentProcessed', ['amount' => 99.99])
]);
$eventStore->writeToStream('order-456', $events);use KurrentDB\StreamFeed\EntryEmbedMode;
$feed = $eventStore->openStreamFeed('user-123');
// Get entries and read events
foreach ($feed->getEntries() as $entry) {
$event = $eventStore->readEvent($entry->getEventUrl());
echo sprintf("Event: %s, Version: %d\n",
$event->getType(),
$event->getVersion()
);
}
// Read with embedded event data for better performance
$feed = $eventStore->openStreamFeed('user-123', EntryEmbedMode::BODY);use KurrentDB\StreamFeed\LinkRelation;
// Navigate through pages
$feed = $eventStore->openStreamFeed('large-stream');
$nextPage = $eventStore->navigateStreamFeed($feed, LinkRelation::NEXT);
// Use iterators for convenient traversal
$iterator = $eventStore->forwardStreamFeedIterator('user-123');
foreach ($iterator as $entryWithEvent) {
$event = $entryWithEvent->getEvent();
// Process event...
}
// Backward iteration
$reverseIterator = $eventStore->backwardStreamFeedIterator('user-123');use KurrentDB\ExpectedVersion;
// Write with expected version
$eventStore->writeToStream(
'user-123',
$event,
5
);
// Special version expectations
$eventStore->writeToStream('new-stream', $event, ExpectedVersion::NO_STREAM);
$eventStore->writeToStream('any-stream', $event, ExpectedVersion::ANY);use KurrentDB\StreamDeletion;
// Soft delete (can be recreated)
$eventStore->deleteStream('old-stream', StreamDeletion::SOFT);
// Hard delete (permanent, will be 410 Gone)
$eventStore->deleteStream('obsolete-stream', StreamDeletion::HARD);Improve performance with built-in caching:
// Filesystem cache
$httpClient = GuzzleHttpClient::withFilesystemCache('/tmp/kurrentdb-cache');
$eventStore = EventStoreFactory::createWithHttpClient($url, $httpClient);
// APCu cache (in-memory)
$httpClient = GuzzleHttpClient::withApcuCache();
$eventStore = EventStoreFactory::createWithHttpClient($url, $httpClient);
// Custom PSR-6 cache
use Symfony\Component\Cache\Adapter\RedisAdapter;
$cacheAdapter = new RedisAdapter($redisClient);
$httpClient = GuzzleHttpClient::withPsr6Cache($cacheAdapter);
$eventStore = EventStoreFactory::createWithHttpClient($url, $httpClient);For advanced use cases, you can provide custom implementations of the core services:
use KurrentDB\EventStoreFactory;
use KurrentDB\Service\StreamReaderInterface;
use KurrentDB\Service\StreamWriterInterface;
use KurrentDB\Service\StreamIteratorFactoryInterface;
// Create custom service implementations
$customStreamReader = new MyCustomStreamReader($httpClient);
$customStreamWriter = new MyCustomStreamWriter($httpClient);
$customIteratorFactory = new MyCustomIteratorFactory($streamReader);
// Create EventStore with custom dependencies
$eventStore = EventStoreFactory::createWithDependencies(
$uri,
$httpClient,
$customStreamReader,
$customStreamWriter,
$customIteratorFactory
);Read multiple events efficiently:
// Collect event URLs
$eventUrls = [];
foreach ($feed->getEntries() as $entry) {
$eventUrls[] = $entry->getEventUrl();
}
// Batch read
$events = $eventStore->readEventBatch($eventUrls);
foreach ($events as $event) {
// Process events...
}use KurrentDB\Exception\StreamNotFoundException;
use KurrentDB\Exception\WrongExpectedVersionException;
use KurrentDB\Exception\StreamGoneException;
try {
$eventStore->writeToStream('user-123', $event, 10);
} catch (WrongExpectedVersionException $e) {
// Handle version conflict
echo "Version mismatch: " . $e->getMessage();
} catch (StreamNotFoundException $e) {
// Stream doesn't exist
echo "Stream not found: " . $e->getMessage();
} catch (StreamGoneException $e) {
// Stream was permanently deleted (hard delete)
echo "Stream gone: " . $e->getMessage();
}You can provide your own HTTP client implementing HttpClientInterface:
use KurrentDB\Http\HttpClientInterface;
class MyCustomHttpClient implements HttpClientInterface
{
public function send(RequestInterface $request): ResponseInterface
{
// Custom implementation
}
public function sendBatch(RequestInterface ...$requests): \Iterator
{
// Batch implementation
}
}
$eventStore = EventStoreFactory::createWithHttpClient($url, new MyCustomHttpClient());# Start KurrentDB and build PHP container
make up
# Install dependencies
make install
# Run tests
make test
# Run tests with coverage
make test-coverage
# Check code style
make cs-fixer-ci
# Fix code style
make cs-fixer
# Run static analysis
make phpstan
# Run benchmarks
make benchmark
# View logs
make logs
# Stop containers
make downThe project uses PHPUnit for testing:
# Run all tests
make test
# Run with coverage report
make test-coverage
# Run specific test file
docker compose exec php bin/phpunit tests/Tests/EventStoreTest.phpEventStore- Main facade class for all operationsEventStoreFactory- Factory for creating EventStore instances with proper dependenciesWritableEvent- Represents an event to be writtenWritableEventCollection- Collection of events for atomic writesStreamFeed- Paginated view of a streamEvent- Represents a read event with version and metadata
StreamReader- Handles stream reading operationsStreamWriter- Manages stream writing and deletionStreamIteratorFactory- Creates stream iterators for navigation
StreamDeletion- SOFT or HARD deletion modesEntryEmbedMode- NONE, RICH, or BODY embed modesLinkRelation- FIRST, LAST, NEXT, PREVIOUS, etc.
EventStoreInterface- Main service interfaceHttpClientInterface- HTTP client abstractionWritableToStream- Objects that can be written to streams
The project includes a complete Docker setup with:
- KurrentDB (latest) with projections enabled and health checks
- PHP container with all required extensions and dependencies
- Persistent volumes for KurrentDB data and logs
- Automatic service dependency management
The KurrentDB instance is configured with:
- HTTP API on port 2113
- Default credentials:
admin:changeit - All projections enabled
- AtomPub over HTTP enabled
Contributions are welcome! Please feel free to submit a Pull Request.
Before submitting:
# Run tests
make test
# Check code style
make cs-fixer-ci
# Run static analysis
make phpstan
# Check source dependencies
make check-src-depsThe project includes dependency validation using composer-require-checker to ensure all used dependencies are properly declared in composer.json:
# Check for missing dependencies in source code
make check-src-depsThis project is licensed under the MIT License - see the LICENSE file for details.
This project is not endorsed by Event Store LLP nor Kurrent Inc.