macnificos/broadway-bundle
Symfony bundle for Broadway event sourcing that wires command and event buses, event stores, read models, and sagas into the container, reducing boilerplate when building CQRS and event-driven PHP applications.
Minimal setup
composer require macnificos/broadway-bundle broadway/broadway
Because the last release is from 2019, Flex will likely not auto-register the bundle. Add it manually in config/bundles.php:
return [
// ...
Macnificos\BroadwayBundle\MacnificosBroadwayBundle::class => ['all' => true],
];
Look here first
The single most useful file is src/DependencyInjection/Configuration.php (or Configuration.php at the package root). It enumerates every config key the bundle exposes — event store driver, serializer, command bus middleware, subscriber wiring. Read that before searching the docs, because docs for a 0-star package are effectively non-existent.
Check the README.md and the bundle's services.xml/services.yaml next to see which service IDs (broadway.command_bus, broadway.event_bus, broadway.event_store, broadway.read_model.*) are registered and what tags are applied.
Minimal config
# config/packages/broadway.yaml
broadway:
event_store:
dbal:
connection: doctrine.dbal.default_connection
# table usually defaults to "event_store"; verify in Configuration.php
Create the event-store table once (the bundle does not normally ship migrations — copy the schema from broadway/broadway's dbal docs, or the bundle's Resources/).
First use case
Pick one small aggregate — e.g. a User — and run a full vertical slice:
RegisterUser command + RegisterUserHandler.User aggregate extending Broadway\Domain\AggregateRoot.UserRegistered event.UserProjector that listens to the event and writes to a read-model table.Then inject CommandBus into a controller and dispatch. If a row lands in event_store and your read model is populated, the wiring is correct.
Command → aggregate → event
final class RegisterUserHandler implements CommandHandler
{
public function __construct(private Repository $repository) {}
public function __invoke(RegisterUser $command): void
{
$user = User::register($command->id, $command->email);
$this->repository->save($user);
}
}
If the bundle auto-tags handlers via CommandHandler interface, this is enough. If not, add broadway.command_handler (verify the exact tag in services.yaml) — this is the first thing to confirm when "commands are silently ignored."
Read models / projectors
final class UserProjector implements EventListener
{
public function __construct(private Connection $db) {}
public function handle(MessageInterface $message): void
{
if ($message->getPayload() instanceof UserRegistered) {
$this->db->insert('user_read', [
'id' => $message->getPayload()->id,
'email' => $message->getPayload()->email,
]);
}
}
}
Register it as broadway.event_listener (or whatever the bundle's tag is). Prefer one listener per read model and switch on payload type, rather than one listener handling many concerns.
Multiple event stores
Use the bundle's named stores: a DBAL store for production, an InMemoryEventStore for tests. Override the service in config/packages/test/broadway.yaml:
services:
broadway.event_store:
class: Broadway\EventStore\InMemoryEventStore
Wrap each integration test in a kernel reset so events don't leak between tests.
Wiring Broadway repositories
The bundle may not auto-wire Broadway\EventSourcing\EventSourcingRepository. If it doesn't, register it yourself:
services:
App\Repository\UserRepository:
class: Broadway\EventSourcing\EventSourcingRepository
arguments:
- '@App\Domain\User'
- '@broadway.event_store'
- '@broadway.event_stream_decorator'
Async work
Broadway itself is synchronous — commands dispatch events inline. For async, attach an event listener that pushes serialized messages to a Symfony Messenger bus, and consume on the other side. Keep the aggregate's own state synchronous; treat projections as the async part.
Testing
Unit-test aggregates by replaying events directly (User::register() then assert getUncommittedEvents()), and integration-test the full stack through the CommandBus. Snapshot tests against SerializablePayload-based events catch serialization regressions early.
broadway/broadway directly into services.yaml — the bundle mostly saves you ~50 lines of DI config.debug:container --tag=<tag> to your debugging checklist.EventSourcingRepository::save() writes the stream; your projector runs in the same request unless decoupled. Wrap read+write in a transaction only when the projector is synchronous; never assume it for async.Configuration.php and services.yaml in the bundle once and write down the real service IDs/tags — that's your internal doc, since the external docs are thin.broadway.event_store, broadway.command_bus) rather than subclassing the bundle. The bundle's services are thin — swapping an InMemoryEventStore or adding a middleware via a decorator is the intended escape hatch.connection config. If you use multiple entity managers or a non-default connection, set connection: explicitly.bin/console debug:container broadway.*. If that works, the bundle is worth keeping; if not, drop it and wire Broadway manually.How can I help you explore Laravel packages today?