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

Bus Laravel Package

open-solid/bus

OpenSolid Bus is a lightweight command bus for Laravel that helps organize application logic into commands and handlers. It offers a clean API for dispatching commands, resolving handlers, and keeping controllers and services thin, focused, and easy to test.

View on GitHub
Deep Wiki
Context7

Getting Started

  1. Install the package alongside a Laravel app (the core is framework-agnostic, but you want the service provider):

    composer require open-solid/bus
    

    Laravel package discovery should register the service provider automatically; if not, add it to config/app.php under providers.

  2. Publish and inspect the config (if the package ships one). This is where the command/query-to-handler map and the default middleware stack usually live:

    php artisan vendor:publish --provider="OpenSolid\Bus\Laravel\BusServiceProvider"
    

    Read this file first — it tells you how the package wants to be wired and is the single best source of truth when the README is thin.

  3. Create your first command and handler. A command is a plain DTO with readonly properties; the handler is a dedicated class with a single entry point (__invoke or handle) and constructor-injected dependencies:

    final readonly class CreateUser
    {
        public function __construct(
            public string $name,
            public string $email,
        ) {}
    }
    
    final class CreateUserHandler
    {
        public function __construct(private UserRepository $users) {}
    
        public function __invoke(CreateUser $command): User
        {
            return $this->users->create($command->name, $command->email);
        }
    }
    
  4. Bind the command to the handler — explicit bindings are the whole point of the package, so this step is mandatory, not optional. Register it in a service provider or in the package config map:

    $bus->bind(CreateUser::class, CreateUserHandler::class);
    
  5. Dispatch from a thin controller:

    public function store(StoreUserRequest $request, Bus $bus): JsonResponse
    {
        $user = $bus->dispatch(new CreateUser(
            $request->string('name'),
            $request->string('email'),
        ));
    
        return response()->json($user, 201);
    }
    

First use case: take one bloated controller action — typically a store method with validation, persistence, and an event dispatch — and move only the persistence step into a handler. Leave the FormRequest where it is. That gives you a working slice with minimal surface area and shows you how the bus behaves before you scale it.

Implementation Patterns

Separate commands from queries at the type level. The package distinguishes the two, so keep the intent explicit in your codebase: commands mutate and (usually) return nothing or an ID, queries return data and never mutate. This makes it obvious when a middleware like a transaction wrapper should apply — transactions belong on commands only.

Controller → command → handler, with HTTP concerns kept in HTTP. Validate with a FormRequest (shape, types, auth), then construct a command from the validated data. Never pass a Request object or an Eloquent model into a command if you can avoid it — pass scalars/IDs. That keeps the handler testable without HTTP or database fixtures.

Middleware is where cross-cutting concerns live. Use the pipeline for:

  • Validation — domain-level invariants that a FormRequest can't express (e.g., "email must not already exist").
  • Transactions — wrap the handler in DB::transaction so a failing command doesn't leave partial writes.
  • Logging/metrics — log command class + payload hash + duration for every dispatch, so handlers stay clean.
  • Authorization — if you need business-level checks beyond policies.

Order matters: put validation and authorization before the transaction middleware, and logging outermost so it captures failures too.

Group bindings by module. A single BusServiceProvider with fifty bind() calls rots fast. Split into BillingBusServiceProvider, AccountsBusServiceProvider, etc., or keep one provider that imports a per-module map array. Either way, the binding list doubles as a readable index of every action in the system — that's a genuine architectural win with this style.

Testing is the payoff. Because handlers are plain classes, test them directly with the container or new CreateUserHandler($fakeRepo) — no HTTP, no actingAs, no assertDatabaseHas unless the handler actually touches the DB. For the dispatch path itself, dispatch the command from a feature test and assert the observable outcome. If the package supports handler fakes or a fake bus, use it to assert that a command was dispatched in controller tests without running the handler.

Queries returning read models. Use queries for list/detail endpoints: ListUsers, GetUserProfile. Return DTOs or collections from the handler. This is where the pattern pays off most in a Laravel app, because you can bypass Eloquent's active-record leakage and return exactly the shape your API needs.

Compose handlers only through the bus, not directly. If CreateOrderHandler needs to send a confirmation, dispatch a SendOrderConfirmation command rather than instantiating that handler — you get its middleware (logging, transactions) for free and avoid coupling to the handler class.

Gotchas and Tips

  • Maturity check first. Three stars, zero dependents, and a release date that looks placeholder-ish warrant a compatibility audit: confirm the package's PHP and Laravel version constraints against your app, run the test suite on your version, and pin the version in composer.json. For a production app, budget an hour to read the source of the container/handler resolver before committing.

  • Every command needs a binding — and a missing one fails at dispatch time, not boot time. This is the number-one trap. Add a boot-time assertion or a test that scans your command directory and asserts a binding exists for each class; it's a few lines with ReflectionClass and will save you a class not found / no handler bound error in staging.

  • Config caching. If bindings are declared in config, php artisan config:cache will freeze them — a new binding "not working" in a cached environment usually just needs php artisan config:clear. Prefer provider-based bind() calls over config arrays if you deploy with config caching on.

  • Middleware order is not obvious. Transaction-inside-validation gives you a transaction spanning a throwaway validation failure; transaction-outside means a validation exception rolls back nothing (fine) but a logging middleware placed inside the transaction will log a rollback it caused. Write a small test that throws in a handler and asserts the transaction rolled back.

  • Keep commands serializable-ish. The bus is synchronous by default, so this isn't urgent — but if you later move a command onto a queue, any object payload (models, Request, closures) will break serialization. Restricting commands to scalars, IDs, and simple value objects now keeps that door open.

  • Don't dispatch the bus from inside a handler in a loop without thinking. It's fine for fan-out, but N nested dispatches multiply middleware (including transactions) and can produce deeply nested transaction scopes. Use DB::transaction's savepoint behavior deliberately, or batch outside the handler.

  • Debugging a failing dispatch: check the binding first ($bus resolution in php artisan tinker), then whether the handler is being constructed (constructor injection errors look like "target not instantiable"), then middleware. Log at the start of each middleware during bring-up; remove it once the pipeline is stable.

  • Extension points worth knowing: custom middleware classes (implement whatever the package's middleware contract is), the container used to instantiate handlers (swap it if you need scoped resolution), and the bus itself if it's an interface — decorating it is the cleanest way to add metrics or a dev-mode "echo every dispatch" tracer without touching the package.

  • Avoid Request/Auth facade calls in handlers. It's tempting and it collapses the testability that motivated the pattern. If a handler needs the current user, pass the user ID in the command from the controller.

  • Naming consistency pays off. With 30+ commands, prefix handlers with the command name (CreateUser → CreateUserHandler) and keep one handler per file. The explicit binding map is only legible if the names line up predictably.

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.
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib