Install the Bundle
composer require awd-studio/es-lib-bundle
Ensure your composer.json meets the requirements (PHP 8.3+, Symfony 7.2+, Doctrine ORM).
Enable the Bundle
Add to config/bundles.php:
return [
AwdStudio\EsLibBundle\AwdEsBundle::class => ['all' => true],
];
Configure the Bundle
Create config/packages/awd_es.yaml:
awd_es:
event_store:
driver: doctrine # or 'sqlite' for testing
connection: default
aggregates:
- 'App\Entity\YourAggregate'
Define an Aggregate
Create an aggregate class (e.g., src/Entity/UserAggregate.php):
namespace App\Entity;
use AwdStudio\EsLib\Aggregate\AggregateRoot;
use AwdStudio\EsLib\Event\EventInterface;
class UserAggregate extends AggregateRoot
{
public function handle(EventInterface $event): void
{
// Handle events (e.g., update state)
}
public function create(string $name): void
{
$this->recordThat(new UserCreated($this->id, $name));
}
}
First Use Case: Persisting an Event
Inject the AggregateRepository and save an aggregate:
use AwdStudio\EsLibBundle\Repository\AggregateRepository;
class UserService
{
public function __construct(private AggregateRepository $aggregateRepo) {}
public function registerUser(string $name): void
{
$aggregate = new UserAggregate();
$aggregate->create($name);
$this->aggregateRepo->save($aggregate);
}
}
Aggregate Design
AggregateRoot for domain entities.create(), update()).handle(EventInterface) to react to past events (replay).Event Handling
EventInterface (e.g., UserCreated, UserUpdated).recordThat() to append events to the aggregate’s event stream.Repository Integration
AggregateRepository to load/save aggregates by ID:
$aggregate = $aggregateRepo->load(UserAggregate::class, $id);
$aggregate->updateProfile($newData);
$aggregateRepo->save($aggregate);
Event Storage
event_store (customizable via config).Command Handling
Map commands (e.g., RegisterUserCommand) to aggregate methods:
$commandHandler = new RegisterUserCommandHandler($aggregateRepo);
$commandHandler->handle(new RegisterUserCommand($name));
Projection Updates Subscribe to events to update read models (e.g., Elasticsearch, caches):
$eventSubscriber = new UserProjectionSubscriber($elasticsearchClient);
$eventDispatcher->addSubscriber($eventSubscriber);
Testing Aggregates
Use AggregateTestCase to test event sourcing logic:
public function testUserCreation()
{
$aggregate = new UserAggregate();
$aggregate->create('John Doe');
$this->assertEquals(['UserCreated'], $aggregate->getUncommittedEvents());
}
Doctrine Entities
If your aggregate is also a Doctrine entity, extend both AggregateRoot and BaseEntity:
use Doctrine\ORM\Mapping as ORM;
use AwdStudio\EsLib\Aggregate\AggregateRoot;
#[ORM\Entity]
class UserAggregate extends AggregateRoot
{
#[ORM\Id]
#[ORM\Column(type: 'string')]
private string $id;
}
Symfony Messenger Dispatch events as messages for async processing:
$eventBus->dispatch(new UserCreated($id, $name));
Custom Event Store
Implement EventStoreInterface for non-Doctrine storage (e.g., MongoDB, Redis):
awd_es:
event_store:
driver: custom
service: app.custom_event_store
Event Ordering
recordThat() for new events).AggregateRoot::loadFromHistory() to replay events if the stream is corrupted.Aggregate Loading
AggregateRepository::load() throws AggregateNotFoundException if the ID doesn’t exist.AggregateRepository::exists().Doctrine Event Store Mismatch
event_store table schema doesn’t match, events won’t persist.CREATE TABLE event_store (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
aggregate_id VARCHAR(255) NOT NULL,
aggregate_type VARCHAR(255) NOT NULL,
event_data JSON NOT NULL,
occurred_on DATETIME NOT NULL
);
Circular Dependencies
AggregateRepository into aggregates (violates DDD principles).Event Serialization
Symfony\Component\Serializer).@Serializer\SerializedName or implement JsonSerializable.Enable Event Logging Configure the event bus to log dispatched events:
awd_es:
event_bus:
logger: true
Inspect Event Streams
Use the EventStore service to query events:
$events = $eventStore->getEventsForAggregate(UserAggregate::class, $id);
Check Aggregate State
Override AggregateRoot::getState() to debug current state:
public function getState(): array
{
return ['name' => $this->name, 'version' => $this->version];
}
Default Connection
The connection key in event_store defaults to default. Specify a custom connection:
awd_es:
event_store:
connection: my_custom_connection
Aggregate Naming
The bundle auto-discovers aggregates listed in aggregates config. For dynamic discovery, implement AggregateRegistryInterface.
Event Versioning
The bundle doesn’t auto-version events. Manually set event_version in your event classes if needed.
Custom Event Store
Implement AwdStudio\EsLib\EventStore\EventStoreInterface and bind it as a service:
services:
app.custom_event_store:
class: App\EventStore\CustomEventStore
arguments:
- '@doctrine.dbal.connection'
Event Subscribers
Extend AwdStudio\EsLib\EventBus\SubscriberInterface:
class MySubscriber implements SubscriberInterface
{
public function handle(EventInterface $event): void
{
// React to events
}
}
Aggregate Snapshotting
Implement AwdStudio\EsLib\Aggregate\SnapshotableInterface to optimize large event streams:
class UserAggregate extends AggregateRoot implements SnapshotableInterface
{
public function takeSnapshot(): array
{
return ['name' => $this->name];
}
}
Custom Event Dispatcher
Replace the default EventBus with a custom implementation (e.g., for async dispatch):
awd_es:
event_bus:
service: app.async_event_bus
How can I help you explore Laravel packages today?