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 Lite Laravel Package

psamatt/service-bus-lite

psamatt/service-bus-lite is a lightweight service bus for Laravel and PHP applications. Route commands and queries to dedicated handlers, keep controllers thin, and decouple business logic with a simple, extensible middleware pipeline.

View on GitHub
Deep Wiki
Context7

Getting Started

composer require psamatt/service-bus-lite

Before writing any code, read two things in this order: README.md and the src/ tree. This is a small, low-traffic package (12 stars, 0 dependents), so the source is the documentation. You are looking for the three contracts that make up the bus:

  • a command bus — dispatch(object $command): void (or returns nothing meaningful)
  • a query bus — ask(object $query): mixed / dispatch() returning a result
  • a middleware contract — typically __invoke(object $message, callable $next) or a handle() method

Confirm the handler-resolution strategy next. Most buses of this shape either (a) map message class → handler class explicitly in a registry, (b) resolve {MessageClass}Handler from a PSR-11 container by convention, or (c) take a callable. That single detail determines everything about how you wire it in Laravel.

Then, in a ServiceBusServiceProvider::register():

$this->app->singleton(CommandBus::class, function ($app) {
    return new CommandBus(
        $app,                          // Illuminate\Container implements PSR-11
        [
            ValidateCommand::class,
            TransactionMiddleware::class,
            LogCommand::class,
        ],
    );
});
$this->app->singleton(QueryBus::class, fn ($app) => new QueryBus($app));

Your first end-to-end use case should be the smallest meaningful one: a single RegisterUser command, a RegisterUserHandler that writes one row, and a controller that dispatches it. Get that round-trip working before you add middleware, because middleware ordering is the hardest thing to debug here.

Implementation Patterns

One handler per message, handlers as plain classes. Keep handlers free of framework coupling other than injected repositories/services. They should be trivially instantiable in a unit test with mocks — no bus, no container, no app(). This is the primary payoff of using a bus at all; if your handlers still call app() you've gained nothing over fat controllers.

Constructor-injected immutable messages. Commands and queries are final readonly-ish DTOs with public promoted properties and no setters. Validate in the constructor (throwing InvalidArgumentException) for invariants, and use middleware for anything that needs the DB or container.

Push cross-cutting concerns into the middleware stack, in this order:

[WrapInTransaction::class, ValidateMessage::class, AuthorizeCommand::class, LogDispatch::class]

Transaction middleware outermost so a failing handler rolls back cleanly; logging innermost so it sees the resolved handler and can measure duration. Build the stack once at container-bind time, not per dispatch.

Controller pattern. Controllers become uniformly thin — $this->dispatch(new RegisterUser(...)) — and never touch Eloquent. Keep the dispatch call as the only line after request validation; that consistency is what makes the pattern readable at scale.

Queries return data, commands don't. Enforce this by convention even if the library doesn't. A command that returns a value is a query wearing a disguise and will get reused in places where its side effects are unwelcome. If you need the new ID, generate the UUID in the controller and pass it into the command.

Wire the bus into queued jobs. A job whose handle() calls $this->commandBus->dispatch(...) gives you async execution with zero changes to handlers. This is the cheapest way to get background processing and the reason to bind the bus as a singleton.

Testing strategy. Unit-test handlers directly (new RegisterUserHandler($fakeRepo)), and feature-test the bus wiring (does the handler get resolved? does middleware run in order? does the transaction roll back?) in a handful of dedicated tests. Don't re-test every command through the bus — that's slow and tests the same code path twice.

Gotchas and Tips

There is no Laravel package here. No auto-discovery, no published config, no facade. You write the service provider yourself, and you should treat that provider as first-class application code. The upside: total control. The downside: nothing upgrades for free.

PSR-11 compatibility. Laravel's Illuminate\Container\Container implements Psr\Container\ContainerInterface, so passing $app into a container-aware bus generally works. But PSR-11's get() has no autowiring guarantee — if the bus only calls $container->get(Handler::class) and the handler isn't explicitly bound, it may or may not autowire. If handlers fail to resolve, bind them explicitly ($this->app->bind(FooHandler::class)) or pass a closure-based resolver instead of the raw container.

Middleware is where bugs hide. A middleware that forgets return $next($message) silently swallows the dispatch and returns null — the classic symptom is "nothing happened and no error." Add a logging middleware on day one so a missing handler or a short-circuited pipeline shows up in storage/logs/laravel.log instead of in production silence.

Never inject the bus into a handler. It compiles fine and then you get recursive dispatch chains, doubled transactions, and stack traces that lie. If a handler needs to trigger a follow-on action, publish a domain event and let a separate listener dispatch the next command.

Transaction boundary conflicts. If you use DB::transaction() middleware and your repository methods also open transactions, you'll get surprising savepoints and partial rollbacks. Pick one level and stick to it — middleware is usually the right one.

Beware of bus-as-singleton state. Middleware instances are constructed once per request lifecycle. Anything cache-like accumulated inside a middleware instance (a log buffer, an actor ID) persists across dispatches. Keep middleware stateless or reset explicitly.

Maturity risk is real. 12 stars, 0 dependents, no ecosystem. Pin the exact version in composer.json ("psamatt/service-bus-lite": "1.0.*" rather than ^1.0), and vendor the source into your repo if you depend on it heavily — a composer update years from now may not find it. Before adopting, compare against league/tactician (same middleware model, far larger community) or Laravel's built-in Illuminate\Contracts\Bus\Dispatcher with dispatchSync() / ShouldQueue, which gives you the bus and queue integration without a third dependency. This package is worth it only if its specific handler-resolution or query-bus shape matches how you want to write code; otherwise it's a thin wrapper you could reproduce in ~100 lines.

Extension points to exploit. The middleware contract is the whole extension surface — custom AuthorizeCommand, Idempotency, LogQueryTime, CacheQueryResult middleware are all five-line classes. That composability is the package's real value, and it's worth writing your own middleware library instead of reaching for another dependency.

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.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin