Install the Package
composer require becklyn/ddd-symfony-bridge
Enable Bundles
Add to config/bundles.php:
return [
// ...
SimpleBus\SymfonyBridge\SimpleBusCommandBusBundle::class => ['all' => true],
SimpleBus\SymfonyBridge\SimpleBusEventBusBundle::class => ['all' => true],
Becklyn\Ddd\BecklynDddBundle::class => ['all' => true],
];
Configure Auto-Discovery
Add to config/services.yaml:
event_subscribers:
resource: '../src/**/*Subscriber.php'
tags: ['event_subscriber']
command_handlers:
resource: '../src/**/*Handler.php'
tags: ['command_handler']
Run Migrations (if using event store)
php bin/console doctrine:migrations:migrate
Create Your First Command/Event
src/Command/CreateUserCommand.php):
namespace App\Command;
class CreateUserCommand { /* ... */ }
src/Handler/CreateUserCommandHandler.php):
namespace App\Handler;
use App\Command\CreateUserCommand;
class CreateUserCommandHandler {
public function handle(CreateUserCommand $command) { /* ... */ }
}
use App\Command\CreateUserCommand;
use Becklyn\Ddd\CommandBus\CommandBusInterface;
class UserController {
public function __construct(private CommandBusInterface $commandBus) {}
public function create() {
$this->commandBus->dispatch(new CreateUserCommand());
}
}
Create Your First Event Subscriber
src/Event/UserCreatedEvent.php):
namespace App\Event;
class UserCreatedEvent { /* ... */ }
src/Subscriber/UserCreatedSubscriber.php):
namespace App\Subscriber;
use App\Event\UserCreatedEvent;
class UserCreatedSubscriber {
public function handle(UserCreatedEvent $event) { /* ... */ }
}
CreateUserCommand from a controller.UserCreatedEvent.// Controller
$this->commandBus->dispatch(new UpdateInventoryCommand($productId, $quantity));
// Handler
public function handle(UpdateInventoryCommand $command) {
$product = $this->productRepository->find($command->productId);
$product->updateQuantity($command->quantity);
$product->save(); // Raises events like `InventoryUpdatedEvent`
}
class SendWelcomeEmailSubscriber {
public function handle(UserCreatedEvent $event) {
$this->mailer->send('welcome', $event->user->email);
}
}
class AuditLoggerSubscriber {
public function handle(OrderCreatedEvent $event) { /* ... */ }
public function handle(OrderCancelledEvent $event) { /* ... */ }
}
causationId and correlationId (from becklyn/ddd-core) to track event chains:
$eventBus->dispatch(
new UserCreatedEvent($user),
causationId: $command->id,
correlationId: $command->id
);
config/packages/becklyn_ddd.yaml:
becklyn_ddd:
use_event_store: true
$events = $eventStore->getEventsForAggregate(
AggregateId::fromString($aggregateId),
0, // from version
10 // limit
);
class CreateOrderHandler {
public function __construct(
private OrderRepository $orderRepo,
private PaymentGateway $paymentGateway
) {}
public function handle(CreateOrderCommand $command) {
$order = $this->orderRepo->create($command->details);
$this->paymentGateway->charge($order->total());
}
}
public function testCreateOrderCommand() {
$handler = new CreateOrderHandler($this->mockOrderRepo(), $this->mockPaymentGateway());
$command = new CreateOrderCommand(/* ... */);
$handler->handle($command);
$this->assertTrue($this->mockOrderRepo()->wasCreated());
}
public function testUserCreatedSubscriber() {
$subscriber = new SendWelcomeEmailSubscriber($this->mockMailer());
$event = new UserCreatedEvent(/* ... */);
$subscriber->handle($event);
$this->assertEmailWasSent();
}
public function testEventStorePersistence() {
$eventStore = $this->createEventStore();
$event = new UserCreatedEvent(/* ... */);
$eventStore->append($aggregateId, $event);
$storedEvents = $eventStore->getEventsForAggregate($aggregateId);
$this->assertCount(1, $storedEvents);
}
AggregateIdParamConverter to resolve aggregate IDs from route parameters.
# config/routes.yaml
app_order_show:
path: /orders/{id}
controller: App\Controller\OrderController::show
requirements:
id: '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'
// Controller
public function show(AggregateId $id, EventStore $eventStore) {
$events = $eventStore->getEventsForAggregate($id);
// Rehydrate aggregate from events
}
# config/packages/command_bus.yaml
command_bus:
middlewares:
finishes_command_before_handling_next: false
public function handle(ProcessPaymentCommand $command) {
$this->commandBus->dispatch(new SendReceiptEmailCommand($command->orderId));
// Continue processing payment...
}
Naming Conventions
Subscriber or Handler are auto-discovered.
services.yaml:
tags:
- { name: event_subscriber, register_public_methods: true }
*EventSubscriber and *CommandHandler for clarity.Event Store Migrations
doctrine:migrations:migrate after enabling use_event_store: true causes runtime exceptions.
Circular Dependencies
Enum Serialization
BackedEnumNormalizer.
services.yaml (as shown in the docs).Microsecond Precision
MicrosecondsOracleSessionInit listener (as shown in the docs).**Command Handler Uniqu
How can I help you explore Laravel packages today?