prooph/service-bus
Prooph Service Bus is a PHP messaging library for command, query, and event buses. It provides middleware, message routing, and flexible transports for building CQRS and event-driven applications with clean separation of concerns.
prooph/service-bus is a framework-agnostic CQRS toolkit, not a Laravel package. There is no auto-discovered service provider, facade, or publishable config. You get three buses (CommandBus, EventBus, QueryBus), message contracts from prooph/common, a plugin system, and router plugins that map message classes to handlers. Plan on owning all of the wiring.
First places to look:
docs/ in the repository: message_bus.md, command_bus.md, event_bus.md, query_bus.md, plugins.md.src/MessageBus.php for the base bus lifecycle (dispatch, route, handle, finalize).src/Plugin/Router/* for the routing behavior.tests/ for complete, runnable wiring examples; they are closer to real usage than the README.The mental model is:
Prooph\Common\Messaging\Message) is dispatched.dispatch, route, handle, and finalize provide cross-cutting concerns.Minimal Laravel wiring. Because the bus expects Psr\Container\ContainerInterface, and Laravel's container is not guaranteed to be PSR-11-compatible, add a small adapter first:
namespace App\Support;
use Psr\Container\ContainerInterface;
final class LaravelPsrContainer implements ContainerInterface
{
public function __construct(private \Illuminate\Contracts\Container\Container $app) {}
public function get(string $id): mixed
{
return $this->app->make($id);
}
public function has(string $id): bool
{
return $this->app->bound($id) || class_exists($id);
}
}
Then register one bus per concern:
namespace App\Providers;
use App\Support\LaravelPsrContainer;
use Illuminate\Support\ServiceProvider;
use Prooph\ServiceBus\CommandBus;
use Prooph\ServiceBus\Plugin\Router\CommandRouter;
final class ServiceBusServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->singleton(LaravelPsrContainer::class);
$this->app->singleton(CommandBus::class, function ($app) {
$bus = new CommandBus($app->make(LaravelPsrContainer::class));
(new CommandRouter(config('service_bus.commands')))
->attachToMessageBus($bus);
return $bus;
});
}
}
A first use case: a RegisterUser command, its handler, and a route map.
namespace App\Commands;
use Prooph\Common\Messaging\Command;
use Prooph\Common\Messaging\PayloadTrait;
final class RegisterUser extends Command
{
use PayloadTrait;
public static function with(string $email, string $name): self
{
return new self(['email' => $email, 'name' => $name]);
}
}
namespace App\Handlers;
use App\Commands\RegisterUser;
use App\Models\User;
final class RegisterUserHandler
{
public function __invoke(RegisterUser $command): void
{
User::create($command->payload());
}
}
// config/service_bus.php
return [
'commands' => [
\App\Commands\RegisterUser::class => \App\Handlers\RegisterUserHandler::class,
],
];
public function store(RegisterUserRequest $request, CommandBus $commandBus)
{
$commandBus->dispatch(
RegisterUser::with($request->string('email'), $request->string('name'))
);
return response()->noContent(201);
}
That is the whole loop: message, handler, route, dispatch. Add the event and query buses only when you actually need them.
Message design. Use one class per command, event, and query. Prefer named constructors (RegisterUser::with(...)) so payload shape is controlled at the boundary. Keep messages immutable and small; put IDs and scalar values in the payload, not Eloquent models. Use metadata for correlation/causation IDs, the authenticated user ID, and a request ID. If message classes cross process boundaries (queues, event store), serialize through Prooph\Common\Messaging\MessageFactory rather than serialize() on the object.
Route maps in config. Keep routing declarative and out of the bus construction code. A single config/service_bus.php with commands, events, and queries keys is easy to review, test, and cache. Use ::class constants, never closures or object instances, so php artisan config:cache keeps working. Route to class strings and let the container resolve them; this keeps handlers autowirable.
Command bus. One command maps to exactly one handler. Treat it as a synchronous write-side entry point: validate in a FormRequest or dedicated validator, dispatch the command, and let the handler own the transaction. If you want a transaction around the whole dispatch, attach a plugin on MessageBus::EVENT_HANDLE to begin and on MessageBus::EVENT_FINALIZE to commit or roll back based on the exception parameter. Commands should not return domain objects; return nothing, or let the caller query after dispatch.
Event bus. One event maps to many listeners. Listeners are invoked synchronously in-process by default, so keep them small. Dispatch domain events after the write transaction succeeds, or use an outbox table. Make every listener idempotent: retries, duplicated dispatches, and multi-worker consumers will happen. For side effects that can fail independently (email, webhooks, search indexing), route the event through a queue-backed producer or AsyncSwitchMessageRouter rather than doing I/O inline.
Query bus. The query bus is promise-oriented. Handlers should return a React\Promise\PromiseInterface (or a resolved value if your bus setup accepts it), and callers consume the result with ->then(...). Return read-model DTOs or arrays, never Eloquent models that leak persistence concerns. A common pattern is:
$queryBus->dispatch(GetUserById::withId($id))->then(
fn (UserDto $user) => response()->json($user),
fn (\Throwable $e) => response()->json(['error' => 'Not found'], 404),
);
Cross-cutting plugins. The Plugin contract (attachToMessageBus / detachFromMessageBus) is the extension point. Use it for logging, transaction management, authorization, message history, and dispatching metrics. A minimal logging plugin that attaches to MessageBus::EVENT_DISPATCH, EVENT_ROUTE, EVENT_HANDLE, and EVENT_FINALIZE is the fastest way to debug routing problems.
Laravel integration. Register each bus as a singleton in a dedicated service provider. Bind handlers explicitly in the container when they need configuration or repositories; otherwise rely on autowiring. If you use Laravel middleware for auth or validation, run it before dispatch rather than inside prooph plugins. For async work, keep the bus as the in-process dispatcher and use a separate worker/consumer that rebuilds the bus from config.
Testing. Unit test handlers directly by instantiating them with fakes; that is the majority of your test suite. Integration test the bus wiring with real route maps and stub handlers, asserting that each message reaches the expected handler. Do not mock the bus in controller tests unless you only care about the HTTP layer; dispatch assertions on the bus tend to hide routing errors.
handle phase, not at boot. Add a startup smoke test that dispatches each configured route to a stub handler.Psr\Container\ContainerInterface. Always wrap it; passing $app directly may work in some versions and fail in others.messageName() is the FQCN. Renaming or moving a message class silently breaks config routes and any persisted event names. If you persist events or send messages across services, override messageName() with a stable string.config:cache traps. Any closure, anonymous class, or object in the route map will break cached config or produce confusing serialization errors. Keep route maps as plain arrays of strings.then with both success and failure callbacks; rejected promises otherwise disappear silently.Prooph\ServiceBus\Exception\RuntimeException in some paths). Catch at the Laravel boundary and map to HTTP responses; do not let raw domain exceptions leak.EVENT_FINALIZE receives an exception param when handling failed. Use it for cleanup, but do not assume it means success. Commit transactions only when the exception parameter is absent.Plugin implementation or a custom router over subclassing the bus. Custom MessageFactory implementations are the right place to adapt existing payload formats without touching handlers.How can I help you explore Laravel packages today?