Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Ddd Symfony Bridge Laravel Package

becklyn/ddd-symfony-bridge

Symfony bridge for becklyn/ddd-core and becklyn/ddd-doctrine-bridge. Wires SimpleBus command and event buses into your app via bundles, auto-registers subscribers and handlers, and sets up the Doctrine-backed event store.

View on GitHub
Deep Wiki
Context7

Getting Started

The honest first step for a Laravel developer: this package is a Symfony bundle, not a Laravel package. It has no service provider, no config/becklyn_ddd.php, and no facade. You have three realistic paths:

  1. You're actually on Symfony — use it exactly as documented, and the day-to-day ergonomics are excellent (convention-based handler registration).
  2. You're on Laravel but want the DDD primitives — install becklyn/ddd-core and becklyn/ddd-doctrine-bridge directly and write your own EventServiceProvider/AppServiceProvider glue. Skip this bridge entirely; it saves you nothing outside a Symfony kernel.
  3. Hybrid (Laravel app + Symfony components) — you'll get bundles.php registration pain and should budget a day for it.

Assuming path 1 or 2, the minimal sequence:

composer require becklyn/ddd-symfony-bridge

Register the three bundles (SimpleBusCommandBusBundle, SimpleBusEventBusBundle, BecklynDddBundle) in config/bundles.php, then wire the convention-based auto-registration — this is the one thing worth understanding before writing any code:

# config/services.yaml
event_subscribers:
    namespace: App\
    resource: '../src/**/*Subscriber.php'
    tags: [{ name: event_subscriber, register_public_methods: true }]

command_handlers:
    namespace: App\
    resource: '../src/**/*Handler.php'
    tags: [{ name: command_handler, register_public_methods: true }]

Run php bin/console doctrine:migrations:migrate to create the event store tables (the migration ships with ddd-doctrine-bridge), then add the three serializer normalizers (BackedEnumNormalizer, PropertyNormalizer, DateTimeNormalizer) if your events carry enums, value objects, or timestamps.

Where to look first: the naming convention. Files ending in Handler.php become command handlers; files ending in Subscriber.php become event subscribers. Everything else (bus routing, middleware, persistence) is invisible.

First use case: a controller dispatches PlaceOrderCommand; PlaceOrderHandler loads the Order aggregate from the repository, calls $order->place(...), the aggregate records OrderPlaced; on commit, the event store persists the event and SimpleBus fan-outs to every subscriber with a matching typed method — e.g. SendOrderConfirmationSubscriber::handle(OrderPlaced $event). That round trip is the whole package in one sentence.

Implementation Patterns

Handler/subscriber method shape is the contract. Any public method with exactly one typed parameter and a void return is auto-registered as a handler for that type. Method name is irrelevant.

final class PlaceOrderHandler
{
    public function handle(PlaceOrderCommand $command): void { /* ... */ }
    public function handleRefundOrder(RefundOrderCommand $command): void { /* ... */ }
}

The handleXxx style for multi-type classes is the pattern you'll actually use day-to-day, because one-class-per-command gets noisy fast. Keep each handler short — it should load an aggregate, invoke one domain method, and return.

Commands are 1:1, events are 1:N. Multiple subscribers per event is the intended design (write a read-model projection in one, send mail in another, emit to a queue in a third). Multiple handlers per command is undefined behavior — no exception at boot, just whichever wins. Treat command uniqueness as an invariant you enforce in code review.

The command queueing behavior will bite you if you don't know it. SimpleBus defers commands dispatched during command handling until the outer command finishes:

# config/packages/command_bus.yaml — only if you truly need reentrancy
command_bus:
    middlewares:
        finishes_command_before_handling_next: false

Default (deferred) is the safer choice and matches how you'd expect DB::transaction nesting to behave. Leave it alone unless you have a documented ordering requirement.

Correlation and causation IDs. Since ddd-core 3.x/4.x, the CommandBus has separate dispatch() and correlated-dispatch methods, and the event store records correlationId/causationId. This is the feature that makes the event store worth its complexity — you can trace an entire saga from the originating command. Wire it from your request ID if you want end-to-end traceability. Note the 4.0.0 BC break here: the separate with/without-correlation methods are the API you should target.

Controller side: use AggregateIdParamConverter to hydrate aggregate IDs from route params rather than manually new OrderId($uuid). (4.0.1 fixed a TypeError in this path; pin ≥ 4.0.1.)

Laravel adaptation, if you go that route: reproduce the tag registration with a container tag plus a compiler pass analogue — $this->app->tag([...], 'event_subscriber') bound to a resolver that reflects over public methods and maps argument types to a handler list. Then a single EventBus implementation that iterates handlers whose first parameter type matches get_class($event). It's ~100 lines and you lose the Symfony bundle's event-store wiring, which you'll need to port from ddd-doctrine-bridge yourself.

Gotchas and Tips

  • The glob catches everything. Any class in src/ ending in Handler or Subscriber gets registered. In a Laravel codebase this is a real hazard — queue job classes named *Handler, HTTP middleware, anything with Subscriber. Audit before enabling, or narrow the resource globs to ../src/Domain/**/*Handler.php.
  • use_event_store defaults to true. If you register events but never ran the migration, you get an exception at transaction commit, not at dispatch. That's a confusing failure point — the stack trace lands in your Doctrine flush, not in your handler. Either run migrations or explicitly set the flag false in config/packages/becklyn_ddd.yaml.
  • The datetime_immutable type override is app-wide. Mapping datetime_immutable to DateTimeImmutableMicrosecondsType in doctrine.yaml changes behavior for every immutable datetime column in your schema, not just event store fields. Test your existing entities before deploying; some DB platforms and drivers don't round-trip .u cleanly.
  • Serialization failures are silent-ish. Events with private properties, enums, or DateTimeImmutable will fail to persist if the normalizers aren't tagged. PropertyNormalizer also means your event properties need to be reachable — no constructor-only immutable objects with hidden state. Design event classes as flat, serializable DTOs.
  • void is mandatory. A handler returning a value (e.g. : self for fluent APIs, or a result object) breaks registration semantics. PHPStan rule worth adding.
  • Version constraints are tight. 6.1.0 requires Symfony ^6.4 || ^7.4, doctrine/doctrine-bundle ^2.11 || ^3.0, and pulls becklyn/ddd-doctrine-bridge ^3.0 + becklyn/ddd-core ^4.0 — which means Doctrine ORM 3.x and DBAL 4.x. If your app is still on ORM 2.x, this package forces an upgrade that is bigger than the package itself.
  • Debugging the event store: query the table directly rather than instrumenting the bus. SELECT * FROM event_store ORDER BY raised_ts DESC LIMIT 20 tells you immediately whether the problem is "event not raised" or "event not persisted" — those have very different root causes (aggregate bug vs. serializer/migration/flush bug).
  • Not supported natively: outbox pattern, saga orchestration, multi-bus routing. If those are on your roadmap, budget for custom middleware rather than assuming the bundle grows into it.
  • Bus factor. Zero stars, zero dependents, one maintainer org, and the README is the documentation. The code is small and readable, so treat it as a library you may need to fork rather than a dependency you inherit forever. The extension point for anything custom is SimpleBus middleware — that's where you add logging, metrics, retry, or a transactional outbox, not by patching the bundle.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource