innmind/command-bus
A lightweight command bus for PHP that maps commands to handlers and dispatches them through a middleware pipeline, making it easy to add concerns like transactions, logging, and validation around command execution.
innmind/command-bus is a small, interface-first PHP library. There is no framework glue, no autodiscovery, and almost no surface area — that is the point. Expect to read the whole src/ directory in one sitting (it is tiny) before writing any code.
Installation and first look:
composer require innmind/command-bus
Note up front that the repository is archived. Composer will still install it from Packagist, but you should treat it as "vendored code with a nice API" rather than a maintained dependency. If your project needs it long-term, fork it, pin the commit SHA, or copy the interface into your own namespace and reimplement the trivial parts.
Where to look first, in order:
src/CommandBus.php (or similarly named interface) — this is the contract you will type-hint against everywhere.src/ — usually a synchronous dispatcher that maps a command class to a handler.composer.json require block — it will pull in other innmind/* packages (immutable collections, OS/filesystem abstractions), which tells you which style of code the library expects.Minimal first use case: a single command, a single handler, and one dispatch call. Define the command as an immutable DTO, define the handler as an invokable class, wire the two together, then call the bus from your controller/console command. Do not try to build a CQRS framework on day one; the library deliberately refuses to be one.
Command as an immutable value object. Innmind code leans on immutability, so commands should be final, readonly where your PHP version allows, and free of behaviour. All the intent lives in the constructor arguments. This makes commands trivially serialisable for logging and queueing.
Handler-per-command, not handler-per-bus. The usual shape is one handler class with an __invoke(MyCommand $command) method (or an interface with a handle() method — check the README, it varies across Innmind packages). Kept this narrow, the bus itself stays dumb and testable.
Wrapping the bus rather than extending it. Because the contract is a single method, the cleanest integration is the decorator pattern: implement CommandBus yourself, hold the inner bus, and add cross-cutting behaviour.
final class TransactionalBus implements CommandBus
{
public function __construct(
private CommandBus $inner,
private Connection $db,
) {}
public function dispatch(object $command): void
{
$this->db->transactional(fn() => $this->inner->dispatch($command));
}
}
This is where the library earns its keep — logging, timing, retries, authorisation, and metrics all become one-class wrappers that compose.
Wiring in a service container. The package does not ship a container binding. In innmind/mantle projects you register the bus as a service and inject it; in Laravel/Symfony you bind the interface to a singleton concrete implementation and register handlers as tagged services. Whichever you choose, keep handler resolution in the container, not in the bus.
Testing. Because the contract is one method, a test double is a few lines:
final class RecordingBus implements CommandBus
{
public array $dispatched = [];
public function dispatch(object $command): void { $this->dispatched[] = $command; }
}
This is the strongest day-to-day argument for the package: your application code depends on a one-method interface that you can fake instantly, and swapping in an async/queued implementation is a container change rather than an application change.
Async escape hatch. The library gives you a synchronous core, but the decorator pattern means you can add a QueuedBus that serialises commands and pushes them onto a queue, with a worker that reconstructs them and calls the synchronous bus. Nothing in the package fights you on this — but nothing helps you either, so serialisation strategy is entirely yours.
composer.json PHP constraint against your runtime before committing.Innmind\Immutable\Maybe or similar, do not fight it — pattern-match on the return value rather than adding ->unwrap() calls. Mixing imperative try/catch with Maybe returns is the fastest way to unreadable code.toArray() or jsonSerialize() for structured logs; var_export on nested value objects produces noise.CommandBus and wrap". Treat any concrete class in the package as an implementation detail you are allowed to reimplement.How can I help you explore Laravel packages today?