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

onliner/command-bus-bundle

A Symfony bundle that provides a command bus for dispatching command objects to their handlers. It integrates the bus with Symfony's service container, letting commands and handlers be registered and resolved automatically so application code stays decoupled from the handling logic.

View on GitHub
Deep Wiki
Context7

Getting Started

  • Install with composer require onliner/command-bus-bundle:^0.2. Pin to 0.2.* rather than ^0.2 — this is a pre-1.0 package and minor bumps are allowed to break.
  • Before writing a line of application code, resolve the bundle-vs-Laravel ambiguity. The package is named a Symfony bundle, but the description claims it resolves handlers through Laravel's container. Open composer.json first and check: does it require symfony/framework-bundle, illuminate/support, or both? Does it declare extra.laravel.providers? Does it ship a *Bundle.php or a *ServiceProvider.php? That one file determines every step below.
  • Confirm the PHP floor: v0.2.0's only release note is "support php 8.2". Read the actual php constraint — >=8.2, ^8.2, and ^8.1 || ^8.2 behave very differently if you're on 8.1 or 8.3.
  • Register the service provider manually if auto-discovery isn't declared, in bootstrap/providers.php (Laravel 11+) or config/app.php. Publish config if offered: php artisan vendor:publish --provider="Onliner\CommandBus\...\ServiceProvider".
  • Look first at, in order: composer.json → the provider/bundle class (this shows you the container bindings and the bus implementation class) → the Command/Handler contracts → any middleware classes shipped in the repo.
  • First use case — replace one fat controller action with a command. Define a command DTO, a handler with a single handle() method, and dispatch it from the controller instead of calling a service. That's the smallest possible slice that exercises the container resolution and proves the package works in your app.
// app/Application/Order/CreateOrder.php
final class CreateOrder
{
    public function __construct(
        public readonly string $customerId,
        public readonly array $lines,
    ) {}
}

// app/Application/Order/CreateOrderHandler.php
final class CreateOrderHandler
{
    public function __construct(private OrderRepository $orders) {}

    public function handle(CreateOrder $command): string
    {
        return $this->orders->create($command->customerId, $command->lines)->id;
    }
}
// Controller stays thin
public function store(StoreOrderRequest $request, CommandBus $bus)
{
    $id = $bus->dispatch(new CreateOrder(
        $request->user()->id,
        $request->validated('lines'),
    ));

    return response()->json(['id' => $id], 201);
}

Implementation Patterns

One command, one handler, one reason to change. Treat each command as a use-case boundary. Commands are immutable DTOs — readonly promoted properties, scalars and value objects only. No Eloquent models, no Request, no Auth::user() captured inside. Resolve those before dispatch and pass primitives, otherwise async execution silently serializes stale or unserializable state.

Keep the bus out of your domain. Inject the bus interface into controllers, console commands, and queued jobs only. Handlers should dispatch other commands sparingly; if you find yourself chaining three deep, you wanted a saga or an event, not a bus.

Middleware is where the day-to-day value lives. The pluggable pipeline is the reason to adopt this over hand-rolled new XHandler() calls. Typical Laravel order, outer to inner:

  1. Correlation/request ID — stamp a UUID into a log context so sync and AMQP-dispatched work traces together.
  2. Transaction wrapper — DB::transaction(fn () => $next($command)) for commands that write; leave queries out of the pipeline.
  3. Logging/timing — log command class, duration, outcome. This is your primary debugging surface.
  4. Authorization — Gate::authorize() against a policy keyed off the command class.
  5. Validation for async paths — FormRequests only cover HTTP; a command arriving over AMQP has had no validation. Re-validate invariant fields in middleware.

Sync vs. AMQP — split by intent, not by size.

Path Use for Returns
Sync (in-process) Queries, fast writes, anything the caller needs a result from Handler return value
AMQP Slow side effects: emails, PDFs, third-party calls, fan-out void

If a command needs a return value, it can't be async. Design the API so the controller gets an ID or a 202 Accepted and a job-status endpoint, rather than trying to poll the bus for a result.

// Routing decision belongs in config, not in the controller.
// config/command_bus.php (shape to confirm against source)
return [
    'async' => [
        \App\Application\Order\CreateOrder::class,
        \App\Application\Mail\SendReceipt::class,
    ],
];

Testing. The single highest-leverage habit: swap the real bus for an in-memory implementation in feature tests. Assert intent, not internals.

public function test_checkout_dispatches_create_order(): void
{
    $this->instance(CommandBus::class, $bus = new InMemoryCommandBus());

    $this->postJson('/orders', [...])->assertCreated();

    $bus->assertDispatched(CreateOrder::class, fn ($c) => $c->customerId === 'u_1');
}

Keep a separate, smaller suite that boots the real bus with the real container and runs a happy-path command end-to-end. That catches the class of bug where auto-resolution fails only in production wiring.

Folder layout that ages well — group by feature, not by layer:

app/Application/Order/
  CreateOrder.php
  CreateOrderHandler.php
  CancelOrder.php
  CancelOrderHandler.php

Handler registration. Prefer whatever the package calls explicit binding over relying on autowiring. Auto-resolution via the container usually works for zero-arg or type-hinted constructors, but a handler whose dependencies are interfaces needs explicit bind() calls. Do it once in a dedicated CommandBusServiceProvider so upgrade diffs are localised.

Gotchas and Tips

  • The "Symfony bundle / Laravel container" contradiction is your first bug. If the repo is genuinely a Symfony bundle (no extra.laravel.providers), there is no auto-discovery and possibly no Laravel-friendly container contract. Don't burn a day debugging a service provider that doesn't exist — read the source tree before installing.
  • 0.x means no compatibility promise. Read the diff between tags before every upgrade, not the release note. One line saying "PHP 8.2 support" could mean the PHP constraint widened (safe) or narrowed to ^8.2 only (locks out 8.3+). composer why-not onliner/command-bus-bundle 0.2.0 tells you which.
  • Handlers must not hold state. If the container resolves handlers as singletons, a handler with a mutable property will leak between dispatches. Verify the binding's lifetime; if in doubt, register handlers as transient and keep every handler property readonly.
  • Commands crossing AMQP must be serializable. Closures, resources, Eloquent models with loaded relations, and objects with non-public state all fail or silently degrade. Enforce json_encode/unserialize round-trip in a unit test for every async command.
  • Async commands need idempotency keys. AMQP delivers at-least-once. A retried SendReceipt will email twice. Put a dedupe key on the command and a unique index on the consuming side, or accept duplicates deliberately.
  • Middleware order is load-bearing. A transaction wrapper placed inside the logging middleware means a rolled-back command still logs success. Test the pipeline order explicitly — a small test that logs to an array and asserts the sequence is cheap insurance.
  • Failures in async handlers disappear. With sync dispatch an exception hits the controller; over AMQP it goes to your worker log or dead-letter queue. Configure a failed handler equivalent and alarm on the DLQ from day one, before you have traffic.
  • Don't inject the bus into a handler to re-dispatch the same command class. Instant infinite loop, and it'll only surface in the worker.
  • Debugging aids: the logging middleware you write yourself will out-perform anything the package offers at this maturity. Add command, correlation_id, duration_ms, outcome fields. For the wiring itself: php artisan about, php artisan package:discover, php artisan config:clear, composer dump-autoload.
  • Extension points to inspect before building on them — published config keys, container bindings in the provider, any shipped middleware or events. In a 0.x package these are the least stable surfaces. Cover your integration with one focused test per extension point so an upgrade break is loud and obvious rather than a silent behaviour change in production.
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.
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect
youbar/easy-crud
c-point-labs/laravel-hostyourai
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo