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

Command Bus Bundle Laravel Package

konradpodgorski/command-bus-bundle

A command bus package for PHP/Laravel that dispatches commands to their handlers, promoting clean separation between application logic and infrastructure. It provides a structured way to organize command execution and middleware-style handling.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a Symfony bundle — the extension point is config/bundles.php, not a Laravel service provider. If you landed here from a Laravel backlog, stop and read the Gotchas section first: there is no productive path to using this package in a Laravel app. The rest of this document assumes Symfony, or assumes you are reading it as a design reference to replicate natively.

Minimal setup:

  1. composer require konradpodgorski/command-bus-bundle
  2. Register the bundle:
    // config/bundles.php
    return [
        // ...
        KonradPodgorski\CommandBusBundle\CommandBusBundle::class => ['all' => true],
    ];
    
  3. Look first at DependencyInjection/ in the vendor source. It is the entire contract of the bundle. Specifically:
    • Configuration.php — the config tree (command_bus.handlers, mapping, middleware). This tells you what is configurable without reading the README.
    • Any CompilerPass — this tells you how handlers are discovered and tagged. Almost every "why isn't my handler being found" question is answered here.
  4. Find the bus service ID. Run bin/console debug:container | grep -i command and bin/console debug:container --tag=<tag used by the compiler pass>. Do this before writing a single line of application code; the tag name is the whole integration surface.

First use case — a single synchronous round-trip:

final class RegisterUser
{
    public function __construct(
        public readonly string $email,
        public readonly string $plainPassword,
    ) {}
}

final class RegisterUserHandler
{
    public function __construct(private UserRepository $users, private PasswordHasher $hasher) {}

    public function __invoke(RegisterUser $command): UserId
    {
        $user = User::register($command->email, $this->hasher->hash($command->plainPassword));
        $this->users->save($user);

        return $user->id();
    }
}

Then dispatch from a controller:

public function __invoke(Request $request, CommandBus $bus): Response
{
    $id = $bus->dispatch(new RegisterUser($request->get('email'), $request->get('password')));

    return new JsonResponse(['id' => (string) $id], 201);
}

The win on day one is that the controller is now four lines and holds no domain logic. That is the entire value proposition: your controller stops being where write-side consistency goes to die.

Implementation Patterns

Command → handler, one-to-one. Every command DTO has exactly one handler. Enforce this in code review — the moment a command has two handlers, the dispatch result becomes ambiguous and the whole pattern loses its "predictable results" selling point.

Dispatch from the edge only. Controllers, console commands, message consumers, and event listeners dispatch commands. Services and handlers do not dispatch sibling commands — they call the other handler's collaborators or dispatch a follow-up from the edge. Nested dispatch inside a transaction is the fastest way to get silently swallowed writes.

Middleware pipeline for cross-cutting concerns. This is where the package actually earns its keep. Wrap the bus with middleware in this order, outer to inner:

  1. Logging / metrics (outermost — records attempts and failures)
  2. Validation (rejects bad commands before touching the DB)
  3. Authorization (rejects before opening a transaction)
  4. Transaction (innermost — everything a handler does is atomic)
  5. Handler invocation

Each middleware wraps $next($command) and must be a container-managed service so it can receive dependencies. Do not put business logic in middleware; it should be generic across every command or it belongs in the handler.

Attribute-based mapping vs. configuration mapping. The bundle advertises both. Prefer attributes colocated with the handler — the mapping lives next to the code it describes and survives class renames in a way that a YAML/XML block does not. Use configuration mapping only when you need to override handler selection in a specific environment (e.g. swap a real notification handler for a no-op in test).

Transaction boundary as a middleware, not a service. The single highest-leverage thing you get from this pattern:

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

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

Now every command is atomic by default and nobody has to remember beginTransaction(). That eliminates the class of bug where a fat controller writes half a domain operation and throws.

Test handlers in isolation. Handlers are plain services with constructor injection. Unit-test them by instantiating directly with fakes — no bus, no container, no kernel boot. Reserve a small number of integration tests for the wiring (one test that boots the container and asserts a known command resolves to its handler) rather than duplicating every handler test through the bus.

Read it as a reference, build it natively if you are on Laravel. The same pattern on Laravel needs no dependency:

// app/Commands/RegisterUser.php  (a plain DTO)
final class RegisterUser { /* readonly props */ }

// app/Handlers/RegisterUserHandler.php
final class RegisterUserHandler { public function handle(RegisterUser $c): UserId { /* ... */ } }

// Dispatch
$id = Bus::dispatch(new RegisterUser($email, $password));

Add DB::transaction() inside the handler, or use a ShouldQueue job for async, or Bus::chain() for orchestration. Everything the bundle offers has a framework-native equivalent that every Laravel hire already knows.

Gotchas and Tips

It is a Symfony bundle. This is a disqualifier for Laravel, not a tradeoff. Laravel has no bundle/kernel/container-compiler concept; composer require on a modern Laravel app will likely fail resolution against symfony/* and PHP 8.1+ pins, and if it does install, you have dragged an old Symfony component tree in alongside illuminate/*. If a ticket sends you here as a "Laravel package," push back before writing any code.

Synchronous only — as far as the description goes. There is no mention of transports, retries, failure queues, scheduling, or dead-letter handling. A failing handler throws an exception and propagates to the caller; whether that leaves partial state depends entirely on whether you put a transaction middleware in place. Do not assume rollback. Add the transaction middleware on day one.

Zero adoption means zero external bug-finding. 0 stars, 0 dependents, a single maintainer. Every defect you hit, you discover yourself, and you are the de facto maintainer. Pin the version in composer.lock, and if you go ahead anyway, plan to vendor or fork the ~hundreds of lines it actually is.

debug:container is your debugging tool, not the docs. When a handler isn't invoked:

  • bin/console debug:container --tag=<handler tag> — is the handler registered at all?
  • Look at the compiler pass: it may require handlers to be in a specific namespace, implement a specific interface, or be public services.
  • bin/console cache:clear — compiler passes run at container compile time, so a stale cache hides new handler registrations.

Auto-wiring failures present as boot-time errors. A handler whose constructor takes an unfamiliar service produces a Cannot autowire service error at container compile, i.e. at deploy time — not in your unit tests. Boot the kernel in CI.

Middleware ordering is load-bearing and invisible. Transaction inside validation means you open a DB transaction for invalid commands; transaction outside the handler but inside logging means a metrics failure rolls back a committed write. Order middleware explicitly in configuration and write one test per boundary (invalid command never opens a transaction; thrown handler exception rolls back).

Exceptions are your failure semantics. There is no built-in retry. Decide now which exceptions are "command failed, surface to user" versus "transient, retryable," and document it — otherwise you will bolt a retry loop onto a controller later and reintroduce exactly the fat-controller problem the pattern was supposed to solve.

Extension points are the bus middleware and the compiler pass, in that order. Middleware is the sanctioned way to add behaviour. Subclassing or replacing the bus service ID is a fork, not an extension — and given the maintenance profile, "it's a small fork anyway" should be a conscious decision made in the open, not a Friday-afternoon shortcut.

Knowledge concentration is the real operational risk. The code is small enough that a senior engineer understands it in half a day — which feels cheap. But nobody else in the market uses it, there's no issue history to search, and no community to ask. That half-day of understanding lives in one head. Write down the wiring (the tag name, the config keys, the middleware order) in your repo's README the moment you adopt it, or the next hire will read vendor/ to answer every question.

Ramp-up cost is near-zero with the native alternative. Compare against Symfony Messenger (first-party, async, retries, transports) or Laravel's Illuminate\Bus (known by every hire, versioned with the framework, covered by every upgrade guide). The bus-factor argument alone closes the case for anything on a revenue-critical write path.

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