becklyn/ddd-doctrine-bridge
Doctrine ORM bridge for becklyn/ddd-core: provides event store and transaction manager implementations plus ORM mappings and a migration. Includes a DBAL type override to persist microsecond-precision event timestamps (MySQL/Oracle).
Install Dependencies
composer require becklyn/ddd-core becklyn/ddd-doctrine-bridge doctrine/orm doctrine/migrations
Configure Doctrine
Add Doctrine’s EntityManager to Laravel’s service container (e.g., in config/app.php or a custom service provider):
$this->app->bind(\Doctrine\ORM\EntityManagerInterface::class, function ($app) {
return EntityManager::create($app['db.connection'], $config);
});
Run Migrations Publish and execute the provided Doctrine migration:
php artisan vendor:publish --provider="Doctrine\Migrations\Bundle\DoctrineMigrationsBundle" --tag="migrations"
php artisan doctrine:migrations:migrate
Register Event Store
Bind the Doctrine event store to becklyn/ddd-core in a service provider:
$this->app->bind(\Becklyn\Ddd\Core\EventStoreInterface::class, function ($app) {
return new \Becklyn\Ddd\EventStore\Infrastructure\Doctrine\DoctrineEventStore(
$app->make(\Doctrine\ORM\EntityManagerInterface::class)
);
});
Override datetime_immutable Type (Optional)
For microsecond precision in MySQL/Oracle, add this during bootstrapping (e.g., in AppServiceProvider):
use Doctrine\DBAL\Types\Type;
Type::overrideType('datetime_immutable', \Becklyn\Ddd\DateTime\Infrastructure\Doctrine\DateTimeImmutableMicrosecondsType::class);
First Use Case: Persist an Aggregate Event
$eventStore = app(\Becklyn\Ddd\Core\EventStoreInterface::class);
$eventStore->record(
new \App\Domain\Events\UserRegistered(
uuid: Uuid::fromString('...'),
timestamp: new \DateTimeImmutable(),
payload: ['email' => 'user@example.com']
)
);
becklyn/ddd-core's AggregateRoot alongside the Doctrine-backed event store.class UserAggregate extends AggregateRoot
{
public function register(string $email): void
{
$this->recordThat(new UserRegistered(
uuid: $this->id(),
timestamp: new \DateTimeImmutable(),
payload: ['email' => $email]
));
}
}
// Persist aggregate state via event store
$aggregate = new UserAggregate();
$aggregate->register('user@example.com');
$eventStore->record($aggregate->pullDomainEvents());
$entityManager = app(\Doctrine\ORM\EntityManagerInterface::class);
$entityManager->getConnection()->beginTransaction();
try {
$eventStore->record($events);
$entityManager->flush();
$entityManager->getConnection()->commit();
} catch (\Exception $e) {
$entityManager->getConnection()->rollBack();
throw $e;
}
// In a Doctrine event listener
$eventStore->listen(UserRegistered::class, function (UserRegistered $event) {
$user = new UserReadModel($event->payload['email']);
$entityManager->persist($user);
$entityManager->flush();
});
becklyn/ddd-core events.// Dispatch Laravel events from DDD events
$eventStore->listen(UserRegistered::class, function (UserRegistered $event) {
event(new \App\Events\UserRegisteredLaravelEvent($event->payload));
});
Service Provider Setup Centralize Doctrine and DDD bindings in a dedicated service provider:
class DddServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->bind(\Becklyn\Ddd\Core\EventStoreInterface::class, function ($app) {
return new \Becklyn\Ddd\EventStore\Infrastructure\Doctrine\DoctrineEventStore(
$app->make(\Doctrine\ORM\EntityManagerInterface::class)
);
});
}
}
Database Schema
event_entry table.CREATE INDEX idx_event_entry_aggregate_id ON event_entry(aggregate_id);
Event Serialization
JsonSerializable or use a serializer like jms/serializer for complex payloads.class UserRegistered implements JsonSerializable
{
public function jsonSerialize(): array
{
return [
'uuid' => $this->uuid->toString(),
'timestamp' => $this->timestamp->format(\DateTimeInterface::ATOM),
'payload' => $this->payload,
];
}
}
Testing
use Doctrine\ORM\Tools\SchemaTool;
use Doctrine\ORM\Mapping\Driver\AnnotationDriver;
public function setUp(): void
{
$this->entityManager = EntityManager::create(
['url' => 'sqlite:///:memory:'],
$this->getDoctrineConfig()
);
$this->eventStore = new DoctrineEventStore($this->entityManager);
}
Microsecond Timestamp Quirks
DateTimeImmutableMicrosecondsType may fail silently on unsupported databases (e.g., PostgreSQL, SQLite).if (in_array($connection->getDatabasePlatform()->getName(), ['mysql', 'oci'])) {
Type::overrideType('datetime_immutable', DateTimeImmutableMicrosecondsType::class);
}
Transaction Isolation
SET TRANSACTION ISOLATION LEVEL READ COMMITTED in MySQL or optimize aggregate loading.Event Deduplication
(aggregate_id, event_type, event_id) in the event_entry table.Laravel Queue Conflicts
PendingDispatch).queue:work --once.Aggregate Loading
Event Store Inspection
event_entry table directly to verify events:
SELECT * FROM event_entry ORDER BY aggregate_id, version DESC;
Doctrine Logging
$entityManager->getConnection()->getConfiguration()->setSQLLogger(new \Doctrine\DBAL\Logging\EchoSQLLogger());
Event Deserialization Errors
UnexpectedValueException when replaying events.JsonSerializable or use a consistent serializer.Microsecond Precision Debugging
DateTimeImmutableMicrosecondsType is registered and the database supports microseconds.Custom Event Types
event_entry table for metadata (e.g., event_metadata JSON column):
/**
* @ORM\Column(type="json", nullable=true)
*/
private ?array $metadata = null;
Event Projections
class EventProjectionListener
{
public function __invoke(object $event): void
{
// Logic to update read models
}
}
Transaction Retry Logic
EventStoreInterface to handle transient failures:
class RetryableEventStore implements EventStoreInterface
{
public function record(iterable $events):
How can I help you explore Laravel packages today?