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

Service Bus Laravel Package

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.

Deep Wiki
Context7

Getting Started

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:

  1. A message object (implementing Prooph\Common\Messaging\Message) is dispatched.
  2. A router plugin maps the message name (by default the FQCN) to a handler identifier.
  3. The bus resolves that identifier through a PSR-11 container and invokes it.
  4. Plugins listening on 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.

Implementation Patterns

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.

Gotchas and Tips

  • Dormant project. Last release was 2021 and the prooph ecosystem is largely inactive. It still runs on modern PHP in many setups, but expect possible deprecation warnings on PHP 8.1+ and no upstream fixes. BSD-3-Clause means you can fork or vendor it if needed. For greenfield Laravel work, compare against Symfony Messenger, Laravel's native bus, or Tactician before committing.
  • No Laravel auto-wiring. Nothing is discovered. If a handler is not bound or autowirable, the failure happens at dispatch time, inside the handle phase, not at boot. Add a startup smoke test that dispatches each configured route to a stub handler.
  • PSR-11 container mismatch. Laravel's container is not a guaranteed Psr\Container\ContainerInterface. Always wrap it; passing $app directly may work in some versions and fail in others.
  • Message names are your routing keys. By default 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.
  • Event bus is synchronous. A slow listener slows the request. If a listener throws, it can abort the dispatch chain and surface as a 500. Wrap non-critical listeners or move them to a queue/outbox.
  • Query promises are easy to drop. If you dispatch a query and ignore the returned promise, nothing visible happens. Always attach then with both success and failure callbacks; rejected promises otherwise disappear silently.
  • Exception wrapping. Handler exceptions may be wrapped by the bus (Prooph\ServiceBus\Exception\RuntimeException in some paths). Catch at the Laravel boundary and map to HTTP responses; do not let raw domain exceptions leak.
  • Finalize runs in failure paths. 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.
  • No middleware pipeline. Unlike Laravel's bus, there is no ordered middleware stack. Ordering is controlled by listener priorities on the four bus events. Document the intended order (auth → transaction → handle → finalize → log) or it will drift.
  • Listener idempotency. Retried messages, duplicate dispatches, and concurrent workers will re-run listeners. Use unique constraint checks, idempotency keys, or an outbox with at-least-once delivery.
  • Debugging. The fastest debugging tool is a plugin that logs message name, handler identifier, and phase on every event. Log the resolved handler identifier, not just the message, because most failures are route/container mismatches.
  • Extension points. For anything the package does not provide, prefer a 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.
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.
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core