artisansdk/cqrs
ArtisanSDK CQRS provides command and query buses for Laravel, routing commands and queries to handlers via the container. It supports middleware and clean separation of write/read concerns, helping build maintainable, testable applications.
The package is a thin, opinionated CQRS scaffold for Laravel — it gives you a command bus, a query bus, and a middleware pipeline for cross-cutting concerns. It does not try to be an event-sourcing framework, so expect to plug it into an otherwise normal Laravel app.
Install and wire up:
composer require artisansdk/cqrs
php artisan vendor:publish --tag=cqrs-config # inspect the config before customizing
The service provider registers CommandBus and QueryBus in the container and (in most versions) exposes them as facades. Skim config/cqrs.php first — it is where middleware stacks, default queues, and query cache settings live. If you see defaults you do not want (e.g. global log/transaction middleware), disable them there rather than fighting them in code.
First real use case — a self-handling command. Start with the smallest possible thing: one command, one handler, dispatched from a controller.
use ArtisanSdk\CQRS\Command;
class RegisterUser extends Command
{
public function handle(array $attributes): \App\Models\User
{
return \App\Models\User::create($attributes);
}
}
// Controller
public function store(RegisterUserRequest $request)
{
$user = RegisterUser::make()->execute($request->validated());
return UserResource::make($user);
}
Where to look first (in this order):
Command and Query base classes — the handle() / __invoke() / execute() contract, and any rules() / middleware() hooks, are the API surface that matters.Concerns\ traits (DispatchesCommands, DispatchesQueries, etc.) — these are what you sprinkle on controllers, jobs, and tests.src/Middleware/ — read every middleware class once. This is where most of the package's "personality" lives (guards, validation, transactions, caching).Do not start by writing an interface-per-command and a separate handler class. The self-handling form is the shortest path and covers 80% of what most teams need.
Self-handling vs. handler classes. The base Command/Query can contain the logic in handle() directly, or you can keep a separate handler and bind it. The self-handling style is idiomatic for this package and keeps call sites short:
OrderShipped::make()->execute($orderId, $trackingNumber);
Reach for separate handler classes only when you have multiple implementations of the same command (e.g. sync vs. queued, or provider-swappable integrations). For most CRUD-adjacent code, self-handling is faster to read and debug.
Queries as a caching primitive. Query subclasses support caching at the bus level — a key improvement over hand-rolled Cache::remember() calls, because the cache key, TTL, and tag semantics are consistent across the app.
class ActiveSubscribers extends Query
{
protected $ttl = 300; // seconds
protected $tags = ['subscribers']; // if your cache store supports tags
public function handle(string $plan): Collection
{
return Subscriber::where('plan', $plan)->active()->get();
}
}
$subs = ActiveSubscribers::make()->execute('pro');
Invalidate on write with the same tags from inside the command that mutates the data — do this deliberately, because the query bus will not know to evict anything when you write through Eloquent.
Middleware as the integration seam. This is the package’s main selling point over raw commands. Put auth, validation, transactions, logging, and metrics in the middleware stack rather than in every handler:
class RefundOrder extends Command
{
protected $middleware = [
\ArtisanSdk\CQRS\Middleware\Guard::class,
\App\CQRS\Middleware\EnsureOrderIsRefundable::class,
\ArtisanSdk\CQRS\Middleware\Transaction::class,
];
public function handle(int $orderId) { /* ... */ }
}
Rules:
null and letting the caller ??-guard.Dispatching from anywhere via traits. Drop DispatchesCommands / DispatchesQueries onto controllers, jobs, and console commands to get a consistent $this->command(...)/$this->query(...) helper. This is small sugar but it keeps call sites uniform and makes it trivial to swap to Bus::fake() in tests later.
Composition over chaining. Commands should call other commands via the bus, not call handlers directly. That gives you the middleware for free and preserves the audit trail if you log at the bus level. Be careful with depth — three nested commands with a Transaction middleware each will create three savepoints. Prefer flat composition and use a single Transaction middleware at the top-level command.
Async by queue. Commands can be queued like any Laravel job and dispatched through the bus — useful for slow side effects (search index updates, emails). Keep queued command payloads to scalars + IDs; DTOs work but must be Serializable and stable across deploys.
Testing. Fake the bus in feature tests so you assert intent instead of side effects:
CommandBus::fake();
$this->post('/orders/42/refund')->assertOk();
CommandBus::assertDispatched(RefundOrder::class, fn ($c) => $c->orderId === 42);
Reserve real invocations for a small set of unit tests around each handler — that is where the business rules live.
Middleware order is load-bearing. Most CQRS middleware failures trace back to ordering. Guard after Transaction means you open a DB transaction before checking permissions, which will silently serialize unrelated requests. Put cheap/authorization middleware first, keep the transaction middleware last, and add a test that asserts the resolved order for critical commands.
Query caching has no invalidation magic. The Query bus caches the return value. It has no idea you ran an UPDATE. Unless you write through the bus (and invalidate with matching tags or keys), you will serve stale data. Pick a single strategy — tags, or explicit key prefixes — and document it. If your cache store doesn’t support tags (e.g. plain file/dynamo drivers), the $tags property will either be ignored or throw; check the driver before relying on it.
Handlers and commands resolve through the container. That means constructor dependencies (repos, gateways, config) must be resolvable. If a handler takes a scalar constructor arg, the container will fail at dispatch time with a confusing "unresolvable primitive" error. Keep handlers dependency-injected and put all request data in execute() arguments.
Queued commands lose their container state between tries. Anything resolved inside handle() at dispatch time (auth user via Auth::user(), request-scoped singletons) is not the same user when the job runs. Pass explicit IDs. Also check $tries and $timeout on the command — the bus doesn't override them, and a runaway transaction will hold locks until Laravel kills the worker.
The execute() signature matters for the fakes. Bus::fake() assertions compare the instance the bus sees. If your command uses named arguments in handle() or a __invoke($dto), keep that shape consistent — otherwise a fake in one test will pass while the real dispatch in staging type-errors. Wrap the shape in a small make(array $payload) factory to avoid drift.
Don’t put domain rules in middleware. It is tempting because middleware is global and easy to add, but middleware runs even when the command short-circuits (guard fails, cache hit, etc.). Business invariants belong in handle(), where they run every time and are unit-testable in isolation.
Debugging a dispatch. If a command returns null unexpectedly, in order: (1) is a Query caching a stale/empty result, (2) did a middleware short-circuit and return early, (3) is the handler resolved from a bound interface whose concrete impl is the wrong one? Add a simple Log middleware at the front of the stack (->handle($command, $next)) that logs class name, arguments, and duration — it pays for itself within a day.
Extension points worth knowing. The bus itself is a Laravel-style pipeline, so any middleware implementing the (callable $next) => ... shape works; you don’t need to extend package base classes. The container bindings for CommandBus/QueryBus are the clean swap point if you outgrow the built-in bus — bind your own implementation in a service provider and keep the facade and traits API identical, and call-site code won’t change.
Facade aliasing collisions. Many teams already have a Bus or Command alias for another package. Alias the facades explicitly (e.g. CqrsCommandBus) rather than relying on the default registration; it will save you a very confusing afternoon when \Illuminate\Bus\Dispatcher and this package's bus shadow each other.
Pin the version. This is an 84-star package with a small maintainer surface — lock the minor version in composer.json and read the changelog before bumps. The base class signatures (handle, execute, middleware contract) have historically shifted between majors, and there is no deprecation shim.
How can I help you explore Laravel packages today?