dddominio/event-sourcing-bundle
Install the Bundle
composer require dddominio/event-sourcing-bundle "1.0@dev"
Ensure your composer.json includes "minimum-stability": "dev" if using dev versions.
Enable the Bundle
Add to config/bundles.php (Symfony 4.3+):
return [
// ...
DDDominio\EventSourcingBundle\DDDominioEventSourcingBundle::class => ['all' => true],
];
Configure the Bundle
Create config/packages/dddominio_event_sourcing.yaml:
dddominio_event_sourcing:
event_store: 'doctrine' # or 'in_memory' for testing
event_store:
doctrine:
connection: default
entity_manager: default
First Use Case: Publishing an Event
Define an event class (e.g., src/Event/UserRegistered.php):
namespace App\Event;
use DDDominio\EventSourcing\Event;
class UserRegistered extends Event
{
public function __construct(public string $userId, public string $email) {}
}
Publish it in a command or service:
use DDDominio\EventSourcingBundle\EventPublisher;
class UserRegistrationCommand
{
public function __construct(private EventPublisher $publisher) {}
public function handle(string $userId, string $email)
{
$this->publisher->publish(new UserRegistered($userId, $email));
}
}
Event Sourcing with Aggregates
Define an aggregate root (e.g., src/Domain/UserAggregate.php):
namespace App\Domain;
use DDDominio\EventSourcing\AggregateRoot;
use App\Event\UserRegistered;
class UserAggregate extends AggregateRoot
{
public function register(string $email)
{
$this->recordThat(new UserRegistered($this->id, $email));
}
}
Replay events to reconstruct state:
$aggregate = $eventStore->retrieve(AggregateRoot::class, $userId);
Event Listeners
Subscribe to events in config/packages/dddominio_event_sourcing.yaml:
dddominio_event_sourcing:
listeners:
App\EventListener\SendWelcomeEmail: ['App\Event\UserRegistered']
Implement the listener:
namespace App\EventListener;
use App\Event\UserRegistered;
use DDDominio\EventSourcingBundle\EventListener\EventListenerInterface;
class SendWelcomeEmail implements EventListenerInterface
{
public function handle(UserRegistered $event): void
{
// Send email logic
}
}
Command Handling Use Symfony’s messenger or a custom command bus:
namespace App\Command;
use DDDominio\EventSourcingBundle\CommandBus;
class RegisterUserCommand
{
public function __construct(
private CommandBus $bus,
private UserAggregate $aggregate
) {}
public function handle(string $userId, string $email)
{
$this->bus->dispatch(
new RegisterUser($userId, $email),
$this->aggregate
);
}
}
Projection (Read Models) Create a projection to materialize events into a read model:
namespace App\Projection;
use App\Event\UserRegistered;
use DDDominio\EventSourcingBundle\Projection\ProjectionInterface;
class UserProjection implements ProjectionInterface
{
public function __invoke(UserRegistered $event, User $user): void
{
$user->email = $event->email;
$user->save();
}
}
Register in config:
dddominio_event_sourcing:
projections:
App\Projection\UserProjection: ['App\Event\UserRegistered']
Event Store Configuration
EventRecord) has a RecordedOn timestamp and is mapped to a table with id, aggregate_id, aggregate_type, event_name, payload, and version.Aggregate Loading
retrieve() with the exact aggregate type and ID. Mixing types/IDs causes AggregateNotFoundException.N but the store has N+1, the bundle throws ConcurrencyException. Handle retries in your application logic.Event Serialization
__serialize()/__unserialize() or implement JsonSerializable for complex objects.Listener Ordering
listeners:
App\EventListener\HighPriority: ['App\Event\UserRegistered', 100]
App\EventListener\LowPriority: ['App\Event\UserRegistered', -100]
Event Store Inspection
Query the event_record table directly to verify events:
SELECT * FROM event_record WHERE aggregate_id = 'user-123' ORDER BY recorded_on;
For in-memory store, dump the store’s events:
$store = $container->get('dddominio_event_sourcing.event_store');
dump($store->getEventsFor('user-123'));
Aggregate Reconstruction If an aggregate fails to load, check:
aggregate_type in the event store matches the class name.Performance
$projection->project($eventStore->getEventsFor($aggregateId), $readModel);
DQL or QueryBuilder for projections to avoid loading all events.Custom Event Stores
Implement DDDominio\EventSourcing\EventStoreInterface for non-Doctrine stores (e.g., MongoDB, Redis):
class RedisEventStore implements EventStoreInterface
{
public function appendEvents(string $aggregateId, array $events): void
{
// Redis logic
}
public function getEventsFor(string $aggregateId): array
{
// Redis logic
}
}
Register in config:
dddominio_event_sourcing:
event_store: 'custom'
event_store:
custom: App\EventStore\RedisEventStore
Custom Event Publishers
Extend DDDominio\EventSourcingBundle\EventPublisher to add middleware (e.g., logging, validation):
class CustomEventPublisher extends EventPublisher
{
public function publish(Event $event): void
{
$this->logEvent($event); // Custom logic
parent::publish($event);
}
}
Bind in services:
services:
DDDominio\EventSourcingBundle\EventPublisher: '@App\EventPublisher\CustomEventPublisher'
Event Metadata
Attach metadata to events (e.g., RecordedOn, UserId):
$event = new UserRegistered($userId, $email);
$event->setMetadata(['user_id' => $userId, 'source' => 'api']);
Access in listeners:
$metadata = $event->getMetadata()['user_id'];
How can I help you explore Laravel packages today?