simple-bus/doctrine-orm-bridge
Doctrine ORM bridge for SimpleBus/MessageBus. Provides command bus middleware to run command handling inside Doctrine transactions and to dispatch domain events generated by entities. Part of the SimpleBus ecosystem.
Install Dependencies
composer require simplebus/message-bus simplebus/doctrine-orm-bridge doctrine/orm
Register the Bridge Add the transaction middleware to your SimpleBus pipeline in a service provider:
use SimpleBus\DoctrineORMBridge\Middleware\TransactionMiddleware;
use SimpleBus\MessageBus\Middleware\MiddlewareStack;
$entityManager = $this->app->make(\Doctrine\ORM\EntityManagerInterface::class);
$bus = new \SimpleBus\MessageBus\MessageBus(
new MiddlewareStack(
new TransactionMiddleware($entityManager),
// Other middleware...
)
);
First Use Case: Transactional Command Create a command handler that persists entities:
use SimpleBus\MessageBus\Command\CommandHandler;
class CreateUserHandler implements CommandHandler
{
private $entityManager;
public function __construct(\Doctrine\ORM\EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}
public function handle(CreateUser $command)
{
$user = new User($command->email, $command->password);
$this->entityManager->persist($user);
// No need to manually flush/commit - handled by middleware
}
}
Dispatch a Command
$bus->dispatch(new CreateUser('user@example.com', 'password123'));
// In your command handler
public function handle(SomeCommand $command)
{
$entity = new Entity();
$entity->setProperty($command->value);
// Transaction is automatically committed if no exceptions
// or rolled back if exceptions occur
}
$middlewareStack = new MiddlewareStack(
new TransactionMiddleware($entityManager),
new ValidateCommandMiddleware(),
new LogCommandMiddleware()
);
use SimpleBus\DoctrineORMBridge\DomainEvent\DomainEvent;
class User implements DomainEvent
{
public function occurredOn(): \DateTimeInterface
{
return new \DateTime();
}
}
use Doctrine\ORM\Mapping as ORM;
use SimpleBus\DoctrineORMBridge\DomainEvent\DomainEventRecorder;
class UserEntity
{
#[ORM\PrePersist]
public function recordDomainEvents()
{
DomainEventRecorder::record(new User());
}
}
use SimpleBus\DoctrineORMBridge\Middleware\DomainEventDispatcherMiddleware;
$middlewareStack = new MiddlewareStack(
new TransactionMiddleware($entityManager),
new DomainEventDispatcherMiddleware($entityManager, $eventBus)
);
$bus = new \SimpleBus\MessageBus\MessageBus(
new MiddlewareStack(
new ConditionalTransactionMiddleware($entityManager, function ($message) {
return $message instanceof RequiresTransaction;
}),
// Other middleware...
)
);
Service Provider Setup:
public function register()
{
$this->app->singleton(\SimpleBus\MessageBus\MessageBus::class, function ($app) {
$entityManager = $app->make(\Doctrine\ORM\EntityManagerInterface::class);
$bus = new \SimpleBus\MessageBus\MessageBus(
new MiddlewareStack(
new TransactionMiddleware($entityManager),
new DomainEventDispatcherMiddleware($entityManager, $app->make(\SimpleBus\MessageBus\EventBus::class))
)
);
return $bus;
});
}
Binding Handlers:
$this->app->bind(\SimpleBus\MessageBus\Command\CommandHandler::class, function ($app, $command) {
return new CreateUserHandler($app->make(\Doctrine\ORM\EntityManagerInterface::class));
});
Mocking the EntityManager:
$entityManager = $this->createMock(\Doctrine\ORM\EntityManagerInterface::class);
$entityManager->method('persist')->willReturnCallback(function ($entity) {
// Store entity for assertions
});
$entityManager->method('flush')->willReturnCallback(function () {
// Simulate flush behavior
});
$bus = new \SimpleBus\MessageBus\MessageBus(
new MiddlewareStack(new TransactionMiddleware($entityManager))
);
Testing Event Dispatching:
$eventBus = $this->createMock(\SimpleBus\MessageBus\EventBus::class);
$eventBus->expects($this->once())
->method('dispatch')
->with($this->isInstanceOf(User::class));
$middlewareStack = new MiddlewareStack(
new TransactionMiddleware($entityManager),
new DomainEventDispatcherMiddleware($entityManager, $eventBus)
);
Transaction Rollback Leaks
EntityManager may enter a broken state, causing subsequent commands to fail.EntityManager (since v3.0.0), but ensure your application can handle this gracefully. Test with:
$entityManager->expects($this->once())->method('reset');
Event Deduplication
Circular Dependencies
EntityManager and the bridge also uses it, ensure proper dependency injection to avoid circular references.Middleware Order Matters
DomainEventDispatcherMiddleware before TransactionMiddleware will dispatch events before the transaction completes, leading to inconsistent state.TransactionMiddleware first in the stack.Doctrine Event Listeners Conflict
@PrePersist) that also modify the entity state, they may interfere with domain event recording.Transaction Debugging
$entityManager->getConnection()->getConfiguration()->setSQLLogger(new \Doctrine\DBAL\Logging\EchoSQLLogger());
TransactionRequiredException if commands fail unexpectedly.Event Dispatching Issues
DomainEventRecorder:
$recorder = DomainEventRecorder::getRecorder();
$events = $recorder->getRecordedEvents();
DomainEvent interface and have an occurredOn() method.Middleware Stack Debugging
use Psr\Log\LoggerInterface;
class LoggingMiddleware implements Middleware
{
private $logger;
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
public function handle($message, callable $next)
{
$this->logger->info('Handling message', ['class' => get_class($message)]);
return $next($message);
}
}
Custom Transaction Strategies
TransactionMiddleware to support custom transaction managers or isolation levels:
class CustomTransactionMiddleware extends TransactionMiddleware
{
public function __construct(
\Doctrine\ORM\EntityManagerInterface $entityManager,
private \Doctrine\DBAL\Connection $connection
) {
parent::__construct($entityManager);
}
protected function beginTransaction()
{
$this->connection->beginTransaction();
}
}
Event Filtering
DomainEventDispatcherMiddleware:
class FilteredEventDispatcherMiddleware extends DomainEventDispatcherMiddleware
{
public function __construct(
\Doctrine\ORM\EntityManagerInterface $entityManager,
\SimpleBus\MessageBus\EventBus $eventBus,
private callable $filter
)
How can I help you explore Laravel packages today?