damgru/stupid-event-sourcing-symfony
Installation
composer require damgru/stupid-event-sourcing-symfony
Add the bundle to config/bundles.php (Symfony) or config/app.php (Laravel via Symfony bridge):
Damgru\StupidEventSourcingSymfony\StupidEventSourcingBundle::class => ['all' => true],
Database Configuration
Ensure your config/database.php (Laravel) or .env (Symfony) includes a events connection pointing to your event storage table (e.g., events table with id, aggregate_id, aggregate_type, event_name, payload, occurred_on).
First Event
Define an event class (e.g., app/Events/UserRegistered.php):
namespace App\Events;
class UserRegistered
{
public function __construct(public string $userId, public string $email) {}
}
Publish it via the command:
php artisan events:publish UserRegistered
(Laravel) or php bin/console damgru:events:publish UserRegistered (Symfony).
First Aggregate
Create an aggregate root (e.g., app/Domain/User.php):
namespace App\Domain;
use Damgru\StupidEventSourcing\AggregateRoot;
use App\Events\UserRegistered;
class User extends AggregateRoot
{
public function register(string $email): void
{
$this->recordThat(new UserRegistered($this->id, $email));
}
}
Use the aggregate in a service:
$user = User::retrieve($userId);
$user->register($email);
$user->persist(); // Saves events to DB
Event Sourcing Workflow
HandleEvents trait to react to events.
use Damgru\StupidEventSourcingSymfony\EventListener\HandleEvents;
class UserRegisteredListener implements HandleEvents
{
public function handle(UserRegistered $event): void
{
// Side effects (e.g., send welcome email)
}
}
$projection = new UserProjection();
$projection->replayEventsForAggregate($userId);
CQRS Integration
query builder or Symfony’s Doctrine to query projections.Testing
AggregateRoot and verify events:
$user = new User();
$user->register('[email protected]');
$this->assertCount(1, $user->pullUncommittedEvents());
EventStoreTestCase (if provided) or manually replay events:
$events = EventStore::findEventsForAggregate($userId);
$projection = new UserProjection();
foreach ($events as $event) {
$projection->apply($event);
}
EventStore to the container:
$this->app->bind(EventStore::class, function ($app) {
return new EventStore($app->make(Connection::class));
});
Bus or Events facade to dispatch events:
event(new UserRegistered($userId, $email));
config/packages/damgru_stupid_event_sourcing.yaml:
damgru_stupid_event_sourcing:
connection: events
public function __construct(private EventStore $eventStore) {}
Event Schema Mismatches
payload column in the events table uses json or jsonb type (PostgreSQL) or longtext (MySQL).Schema::table('events', function (Blueprint $table) {
$table->json('payload')->change();
});
Aggregate ID Collisions
class@id) for aggregate_id to avoid conflicts.AggregateRoot::id() to enforce UUIDs:
public function id(): string
{
return (string) Str::uuid();
}
Event Ordering
occurred_on order. If using multiple processes, ensure clocks are synchronized or use a sequence number.Projection Lag
persist() is called on the aggregate root.events table has the expected records:
SELECT * FROM events WHERE aggregate_id = 'user@123';
#[AsArray] (Symfony) or #[Serializable] (PHP 8.1+) attributes or implement JsonSerializable.EventStore to add features (e.g., snapshotting):
class CustomEventStore extends EventStore
{
public function snapshot(AggregateRoot $aggregate): void
{
// Custom logic
}
}
EventSubscriberInterface or Laravel’s ObservesEvents:
class AuditSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
UserRegistered::class => 'onUserRegistered',
];
}
public function onUserRegistered(UserRegistered $event): void
{
// Log or audit
}
}
class UserRegistered implements Serializable
{
public function __construct(
public string $userId,
public string $email,
public int $version = 1
) {}
}
How can I help you explore Laravel packages today?