Transactions? Not your problem!
In most PHP projects, transaction management falls on the developer. But a forgotten DB::transaction() or a missing rollback, and the data sometimes ends up in an inconsistent state.
In Phexium, the TransactionalCommandBus is a decorator of the CommandBus. It implements the same interface, and on every dispatch, it wraps execution in a transaction:
$this->transaction->begin();
try {
$this->innerCommandBus->dispatch($command);
$this->transaction->commit();
} catch (Throwable $e) {
$this->transaction->rollback();
throw $e;
}
On the handler side, there is no trace of transaction management. The handler does its save(), dispatches its events. The begin/commit/rollback cycle lives in the decorator, not in the handler.
Why this approach?
Other strategies exist for managing transactions in a CQRS architecture:
- Transaction in the Handler: the most common default approach, but it is an anti-pattern in Clean Architecture. The handler mixes business logic and technical concerns, violating the single responsibility principle.
- Unit of Work: relevant in an ORM ecosystem (Doctrine, Eloquent), but overkill for a lightweight framework that does without. The transactional decorator is simpler and more explicit.
- Middleware Pipeline: the most flexible and composable approach. But as long as the transaction remains the only cross-cutting concern on the bus, the decorator offers the right level of simplicity.
- AOP: elegant in theory with a declarative
#[Transactional]annotation, but not idiomatic in PHP and hard to debug in practice.
The transactional decorator sits at the sweet spot: it respects SOLID, remains simple and explicit, and evolves naturally into a pipeline as needs grow.