becklyn/ddd-core
DDD/CQRS/event-sourcing core building blocks for PHP: entity identities, domain events, command handling, transactions, and an event store workflow. Framework-agnostic abstractions with Symfony/Doctrine/SimpleBus bridge packages available.
Install the Core Package and Bridges
composer require becklyn/ddd-core
composer require becklyn/ddd-doctrine-bridge becklyn/ddd-symfony-bridge
For Laravel, prefer becklyn/ddd-symfony-bridge (Symfony-compatible) and adapt bindings manually.
Define a Domain Event
namespace App\Domain\Events;
use Becklyn\Ddd\Events\AbstractDomainEvent;
class OrderCreated extends AbstractDomainEvent
{
public function __construct(
public string $orderId,
public string $customerId,
public float $totalAmount
) {}
}
Create an Aggregate Root
namespace App\Domain\Orders;
use Becklyn\Ddd\Entities\AbstractAggregateId;
use Becklyn\Ddd\Entities\EventSourcedProviderCapabilities;
use Becklyn\Ddd\Events\DomainEvent;
class OrderId extends AbstractAggregateId {}
class Order
{
use EventSourcedProviderCapabilities;
public function __construct(
private OrderId $id,
private string $customerId,
private array $items = []
) {}
public function addItem(string $productId, int $quantity): void
{
$this->items[] = [$productId, $quantity];
$this->recordThat(new OrderItemAdded($this->id->value(), $productId, $quantity));
}
}
Write a Command and Handler
// Command
namespace App\Domain\Orders\Commands;
class AddItemToOrder implements CommandInterface
{
public function __construct(
public OrderId $orderId,
public string $productId,
public int $quantity
) {}
}
// Handler
namespace App\Domain\Orders\Handlers;
use Becklyn\Ddd\Commands\CommandHandler;
class AddItemToOrderHandler extends CommandHandler
{
public function execute(AddItemToOrder $command): ?Order
{
$order = $this->orderRepository->find($command->orderId);
$order->addItem($command->productId, $command->quantity);
return $order;
}
}
Dispatch the Command
use Becklyn\Ddd\Commands\CommandBus;
$commandBus = app(CommandBus::class);
$commandBus->dispatch(new AddItemToOrder(
new OrderId('order-123'),
'prod-456',
2
));
Configure Symfony/Laravel Bindings
becklyn/ddd-symfony-bridge’s CommandBus and EventBus services.AppServiceProvider:
$this->app->bind(CommandBus::class, function ($app) {
return new SimpleBusCommandBus($app->make(CommandHandler::class));
});
Command-Driven Architecture
Illuminate\Bus\DispatchesCommands trait for controllers:
use Illuminate\Bus\DispatchesCommands;
use App\Domain\Orders\Commands\AddItemToOrder;
class OrderController
{
use DispatchesCommands;
public function addItem(OrderId $orderId, string $productId, int $quantity)
{
$this->dispatch(new AddItemToOrder($orderId, $productId, $quantity));
}
}
Event Sourcing with Projections
// Projection for read-optimized Order
namespace App\Domain\Orders\Projections;
use Becklyn\Ddd\EventSourcing\Projection;
class OrderProjection implements Projection
{
public function apply(OrderCreated $event): void
{
// Update read-optimized Order entity
}
}
Saga Orchestration
namespace App\Domain\Orders\Subscribers;
use Becklyn\Ddd\Events\EventSubscriber;
use App\Domain\Orders\Commands\ProcessPayment;
class PaymentSubscriber implements EventSubscriber
{
public static function subscribedTo(): array
{
return [OrderCreated::class];
}
public function handle(OrderCreated $event)
{
$this->commandBus->dispatch(new ProcessPayment(
new OrderId($event->orderId),
$event->totalAmount
));
}
}
Transaction Boundaries
DB::transaction() for custom logic:
public function execute(Command $command)
{
DB::transaction(function () use ($command) {
// Handle command
});
}
Testing with BDD Traits
use Becklyn\Ddd\Commands\Testing\CommandHandlerTestTrait;
class AddItemToOrderHandlerTest
{
use CommandHandlerTestTrait;
public function testAddItem()
{
$this->givenAnOrderExists('order-123');
$this->whenHandling(new AddItemToOrder(
new OrderId('order-123'),
'prod-456',
2
));
$this->thenAnEventWasRecorded(OrderItemAdded::class);
}
}
Aggregate Loading Performance
EventStore::getAggregateStream() is slow for large datasets.$aggregate = $this->eventStore->getAggregateStream(Order::class, $orderId);
// Cache result for 5 minutes
Cache::put("order:$orderId", $aggregate, now()->addMinutes(5));
Event Ordering in Subscribers
SimpleBus with a queue (e.g., RabbitMQ) or Laravel’s queue system.# config/packages/simple_bus.yaml
simple_bus:
transports:
default: 'doctrine://default'
Correlation IDs in Commands
correlationId on commands can break saga tracking.namespace App\Domain\Commands\Middleware;
use Becklyn\Ddd\Commands\CommandInterface;
class CorrelationIdMiddleware
{
public function __invoke(CommandInterface $command)
{
$command->setCorrelationId(Uuid::generate());
return $command;
}
}
Event Replay Conflicts
EventSourcedProviderCapabilities::apply() to handle conflicts:
protected function apply(DomainEvent $event): void
{
if ($event instanceof OrderItemAdded && $this->items[$event->productId] !== null) {
throw new EventReplayConflictException();
}
// Apply event logic
}
Doctrine EntityManager Leaks
EventManager after rollbacks can cause stale events.$eventManager->clear() in TransactionManager::rollback():
public function rollback(): void
{
$this->entityManager->rollback();
$this->eventManager->clear();
}
Laravel-Specific Optimizations
Queue for async event handling:
namespace App\Domain\Events\Handlers;
use Becklyn\Ddd\Events\EventSubscriber;
use Illuminate\Bus\Queueable;
class AsyncOrderSubscriber implements EventSubscriber, Queueable
{
// ...
}
Custom Event Store
namespace Tests\EventStore;
use Becklyn\Ddd\EventSourcing\EventStore;
class InMemoryEventStore implements EventStore
{
private array $streams = [];
public function load(string $aggregateType, string $aggregateId): array
{
return $this
How can I help you explore Laravel packages today?