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

Message Bus Bundle Laravel Package

silpo-tech/message-bus-bundle

A Symfony message bus bundle that wires commands, queries, and events to their handlers with configurable middleware, helping decouple business logic, simplify service composition, and keep PHP applications testable and maintainable.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a Symfony bundle, not a Laravel package. If you're coming from Laravel, mentally map it to a stricter, container-driven version of Laravel's bus: messages are immutable DTOs, handlers are services, and middleware is a first-class pipeline configured in YAML rather than registered ad hoc.

Install and register

composer require silpo-tech/message-bus-bundle
// config/bundles.php
return [
    // ...
    SilpoTech\MessageBusBundle\MessageBusBundle::class => ['all' => true],
];

Look here first, in this order

  1. README.md — usually the only real documentation for a 2-star bundle; treat it as the spec.
  2. src/DependencyInjection/Configuration.php — the authoritative list of every config key, default, and bus name. This is more reliable than the README.
  3. src/DependencyInjection/MessageBusExtension.php — shows which service tags auto-register handlers and middleware.
  4. src/MessageBusBundle.php — compiler passes reveal hidden wiring (auto-tagging, aliases).

Dump the real config instead of guessing

bin/console config:dump-reference message_bus
bin/console debug:container --tag=<handler_tag_from_extension.php>

First use case: a synchronous command

// src/Application/Command/CancelOrder.php
final class CancelOrder
{
    public function __construct(
        public readonly string $orderId,
        public readonly string $reason,
    ) {}
}
// src/Application/Command/CancelOrderHandler.php
final class CancelOrderHandler
{
    public function __construct(private OrderRepository $orders) {}

    public function __invoke(CancelOrder $command): void
    {
        $this->orders->get($command->orderId)->cancel($command->reason);
    }
}

Wire the handler per the bundle's convention (usually a service tag or autoconfigure: true), inject the bus, and dispatch from a controller:

public function __invoke(CancelOrder $command, CommandBusInterface $bus): Response
{
    $bus->dispatch($command);
    return new Response(null, 204);
}

If nothing happens, debug:container the bus service and check the handler is actually tagged — auto-registration is the #1 source of "it silently does nothing."

Implementation Patterns

Separate command, query, and event buses. The bundle's value is convention: one handler per command/query, many handlers per event. Keep message classes in src/Application/Command|Query|Event, handlers next to them, and never let a command return a value — return an ID and query for state.

Middleware is where the real behavior lives. Typical day-to-day stack, ordered outermost to innermost:

  1. Logging/tracing (correlation ID into logs)
  2. Transaction wrapping (Doctrine beginTransaction/commit/rollBack, must wrap the handler)
  3. Validation (throw a domain exception before the handler runs)
  4. Authorization
  5. Handler

Configure order in YAML; the bundle almost certainly exposes a per-bus middleware list. If order isn't explicit, it follows tag priority — set priority on your middleware service definition.

Custom middleware pattern

final class TransactionMiddleware implements MiddlewareInterface
{
    public function __construct(private EntityManagerInterface $em) {}

    public function handle(object $message, callable $next): mixed
    {
        $this->em->wrapInTransaction(fn () => $next($message));
    }
}

Register it with the bundle's middleware tag and confirm via debug:container --tag=<middleware_tag>.

Sync by default, async by exception. Route only genuinely slow or retryable messages to a transport (Symfony Messenger under the hood). A practical split: commands that must return an HTTP response stay sync; events, exports, notifications, and third-party calls go async. Then dispatch-then-return-202 becomes your standard controller shape.

Events fan out, commands don't. Use events for cross-cutting side effects (email, audit, cache invalidation). Because every handler runs, make them idempotent and short — the bus won't deduplicate for you.

Testing pattern

  • Unit-test handlers directly by calling __invoke() — the bus adds nothing there.
  • Kernel/integration test the pipeline: boot the kernel, fetch the bus from the container, dispatch, assert DB state. This is what catches missing tags and wrong middleware order.
  • Replace the bus with a spy in controller tests to assert what was dispatched, not what it did.

Laravel-equivalent mapping (useful when porting habits): Laravel job/command → message, handle() → __invoke()/handler, dispatch() → $bus->dispatch(), queue connection → Messenger transport, Bus::fake() → a spy bus service in the test container.

Gotchas and Tips

It is Symfony-only. There's no service provider, no config/message_bus.php, no facade. In a Laravel app you can technically pull the bundle in for its message/handler interfaces, but every dispatch path needs hand-wiring. Realistically, vendor the pattern rather than the package: copy the interfaces and write a small Bus wrapper bound as a singleton.

Silent no-ops from tag autoconfiguration. Handlers not tagged → dispatch resolves zero handlers and either throws "no handler" or (worse, in some setups) returns null. Always run bin/console debug:container --tag=... after adding a handler directory. If you use a custom services.yaml that overrides _defaults, autoconfigure: true may be lost.

Middleware order is a correctness issue, not a preference. Transaction middleware inside validation means you open transactions for invalid messages; logging inside the transaction means your "failed" log entries get rolled back. Pin the order explicitly and cover it with a kernel test.

Async + transactions don't mix. If a message is routed to a transport, the transaction middleware won't span the HTTP request and the worker. Use the outbox pattern (persist the message row in the same transaction, relay it later) instead of assuming the bus gives you atomicity.

Debugging commands to keep in muscle memory

bin/console debug:messenger                 # buses, handlers, middleware per bus
bin/console messenger:failed:show          # failed async messages
bin/console messenger:failed:retry <id>
bin/console debug:event-dispatcher         # if events are wired via dispatcher

Add a logging middleware early — the stack traces from deep middleware chains are unreadable otherwise.

Config quirks. Bus aliases matter: injecting MessageBusInterface when the bundle defines command.bus/query.bus resolves to whichever is aliased, which is rarely what you want. Inject concrete bus service IDs. Also check whether the bundle merges or replaces the default messenger config — if it defines its own buses, the stock messenger.routing keys may need to reference the new bus names.

Extension point worth knowing. Beyond middleware, the compiler pass is the clean place to add cross-cutting auto-tagging (e.g., every class implementing your CommandHandlerInterface in src/Application). Write a tiny compiler pass in your app instead of listing services by hand.

Project maturity risk. Two stars, zero dependents, and a release date that reads as future-dated — verify the tag actually exists on Packagist and inspect the git history before standardizing on it. Pin an exact version in composer.json, and read composer.json for the Symfony/Messenger version constraints; a bundle this small often lags major Symfony releases. Have a fallback plan: since it's a thin layer over Symfony Messenger, replacing it later means swapping the bus service IDs and re-registering middleware — keep your message and handler classes free of bundle-specific types so that migration stays mechanical.

Upgrade/verification ritual. After any composer update, re-run the kernel test that dispatches one command, one query, and one event through the real container. That single test catches nearly every regression this bundle can introduce.

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.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
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