gnugat/query-bus
A lightweight query bus for PHP that dispatches queries to dedicated handlers, helping decouple read operations from application code. Includes middleware support for composing cross-cutting concerns around query execution.
Architecture fit
This is a CQRS-style read-side package: a QueryBus dispatches a query object to the single QueryMatcher that claims to handle it. That maps cleanly onto Laravel's service-container idiom (bindings, AppServiceProvider registration, constructor injection), so it fits the framework's conventions rather than fighting them. It is a narrow, single-responsibility library — it does not impose a persistence layer, HTTP layer, or DTO contract beyond the query/matcher pairing. That is good for adoption: it can be introduced in one bounded context without a rewrite of controllers or models.
The v1.0.0 content is thin but substantive: matcher resolution logic plus explicit prioritization at registration time. Prioritization is the architecturally interesting part. It implies overlapping matchers are expected — multiple matchers may match a query, and the highest-priority one wins. That is a deliberate escape hatch (e.g. tenant-specific overrides, feature-flagged read paths, test doubles replacing production matchers), and it is also the package's main architectural hazard, because resolution order becomes implicit behavior encoded in registration order.
Integration feasibility
Low-to-moderate. The surface area is small: register matchers in a service provider, inject QueryBus where needed. There is no stated dependency on queues, caching, or a specific Laravel major version, so a spike should be a day or two of work, not a sprint. The genuine cost is not wiring — it is the decision about where queries live and whether the team accepts query objects as a first-class concept. That is an organizational decision, not a technical one.
Technical risk
Key questions
Stack fit Strong fit for a Laravel monolith or modular monolith that already leans on the container. It is framework-native and additive — no database driver, no queue backend, no new infrastructure. It is a poor fit for teams with no CQRS vocabulary, or for codebases where read logic is already well-factored into repositories/services; there, it adds a layer without removing one.
Migration path Strangler-fig, one read path at a time. Do not convert existing controllers wholesale. Pick a low-traffic, well-understood read endpoint, introduce the query + matcher, route it through the bus, and leave the rest of the app untouched. The package's minimalism makes it easy to remove later if the experiment fails — a genuine advantage over heavier CQRS frameworks.
Compatibility
Unverified from the release notes. Confirm supported Laravel/PHP ranges against our current runtime before committing; confirm whether a major-version bump of Laravel is likely to strand us. Pin the version explicitly and avoid ^ ranges until the project demonstrates a stable release cadence.
Sequencing
QueryServiceProvider, matchers registered in one place, priorities explicit and commented where non-obvious. Forbid priority-by-registration-order in code review.Maintenance Low ongoing surface area — one dependency, no runtime services, no schema. The burden is code hygiene: registrations accumulate, and priority annotations rot. Budget a standing convention (one registration file, explicit priorities) or the bus will slowly become unpredictable. Monitor upstream release frequency and responsiveness; a v1.0.0 package with a two-line changelog has not yet demonstrated a maintenance cadence.
Support External support is uncertain and probably thin — likely a single maintainer. Internal mitigation: keep the integration thin enough that we could vendor or replace the package in under a week. Concretely, wrap dispatch behind our own narrow interface so call sites never depend on package classes directly. That one indirection is worth the cost here.
Scaling Runtime scaling is a non-issue — dispatch is in-process method resolution, negligible versus I/O. The scaling concern is cognitive and organizational: as matchers multiply, reasoning about "which matcher will handle this query" gets harder. Priority is a tool that helps at low counts and hurts at high counts. Keep the matcher count per bounded context small and treat priority collisions as a design smell, not a feature.
Failure modes
Ramp-up Low for anyone fluent in Laravel DI: a competent mid-level engineer should be productive in half a day. The real ramp-up cost is conceptual — teaching the team when not to introduce a query. Pair the technical onboarding with a short written guideline covering: what belongs in a query (a read intent, not a command), when to skip the bus (trivial one-line reads), and how priorities are assigned and reviewed. Without that, expect six weeks to sprawl into wrapping every model access, at which point the abstraction is costing more than it returns.
Recommendation: Proceed to a one-day spike, contingent on answering questions 1, 2, and 4. Do not adopt broadly until priority and unmatched-query semantics are pinned by our own tests.
How can I help you explore Laravel packages today?