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

Cqrs Laravel Package

revision-ten/cqrs

A lightweight CQRS package for Laravel that separates writes from reads using commands, queries, handlers, and buses. It helps organize business logic into clear, testable actions with simple Laravel-friendly integration.

View on GitHub
Deep Wiki
Context7

Getting Started

  1. Install and inspect before wiring anything up.

    composer require revision-ten/cqrs
    

    Laravel's package discovery should register the service provider automatically; if not, add it to config/app.php manually. Immediately after install, read vendor/revision-ten/cqrs/README.md and skim src/ — with a package this small (2 stars, no dependents) the source is the documentation, and you need to confirm the exact class names (CommandBus, QueryBus, handler interfaces, middleware contracts) before writing code against them.

  2. Check for a publishable config.

    php artisan vendor:publish --provider="RevisionTen\Cqrs\CqrsServiceProvider"
    

    Look for config/cqrs.php — typically this holds handler-to-command mappings, bus middleware stacks, and default queue connections. If no config is published, handler registration is probably done in a service provider or via auto-resolution from the container.

  3. Start with one vertical slice: a single command. Before touching queries or events, take one write operation in your app — e.g. RegisterUser — and convert it:

    // app/Users/Commands/RegisterUser.php
    final class RegisterUser
    {
        public function __construct(
            public readonly string $email,
            public readonly string $name,
        ) {}
    }
    
    // app/Users/Handlers/RegisterUserHandler.php
    class RegisterUserHandler
    {
        public function __construct(private UserRepository $users) {}
    
        public function handle(RegisterUser $command): User
        {
            return $this->users->create($command->email, $command->name);
        }
    }
    

    Register the handler per the package's convention, then dispatch from a controller:

    public function store(RegisterUserRequest $request, CommandBus $bus)
    {
        $user = $bus->dispatch(new RegisterUser(
            $request->validated('email'),
            $request->validated('name'),
        ));
    
        return UserResource::make($user);
    }
    
  4. Where to look first when something breaks: the bus implementation in src/ (to see how handlers are resolved and whether middleware runs), then your handler registration (the #1 cause of "no handler found" errors), then bootstrap/cache/ (stale config).

Implementation Patterns

Feature-folder over layer-folder. Group by domain, not by type. app/Users/Commands/, app/Users/Queries/, app/Users/Handlers/ beats scattering Commands/ across the whole app. It keeps the command/handler pair adjacent and makes deletion trivial.

One handler, one public handle() method. Keep handlers thin orchestrators. Business logic lives in domain services or aggregates the handler composes. This is what makes handlers testable without a bus at all.

Commands return identifiers, not entities (pragmatically). Strict CQRS says a command returns void and the client re-queries. In a Laravel HTTP context that means two round trips and a race condition. Returning the new aggregate's ID (or the aggregate itself) from handle() is the common compromise — just be consistent across the codebase so nobody guesses.

Queries stay read-only and side-effect free. A query handler should be safe to call twice, safe to retry, and safe to cache. If it touches a write model, it's a command in disguise.

final class GetUserProfile
{
    public function __construct(public readonly int $userId) {}
}

Middleware is where cross-cutting concerns live. The bus almost certainly supports a middleware stack (transaction wrapping, logging, validation, authorization). Wrap commands in a DB transaction at the bus level, not in each handler:

class TransactionMiddleware
{
    public function __construct(private DatabaseManager $db) {}

    public function handle($command, Closure $next)
    {
        return $this->db->transaction(fn () => $next($command));
    }
}

Register it once in the config/provider; every command becomes atomic for free. Use the same mechanism for an audit-log middleware that records command class + payload.

Dispatch domain events from within the command's transaction, but queue listeners outside it. If the package provides an event dispatcher alongside the bus, publish events after the transaction commits — otherwise a listener fires on data that rolls back.

Reuse the bus inside queued jobs. If a command is slow, wrap it: dispatch(new ProcessOrderJob($orderId)) where the job constructs and dispatches the command. Don't make the command itself ShouldQueue unless the package explicitly supports serializable command payloads — most don't handle closures or model instances safely.

Test handlers directly, test dispatch through the bus. new RegisterUserHandler($repo)->handle(new RegisterUser(...)) for logic; $bus->dispatch(...) + Queue::fake()/Event::fake() for wiring.

Bind the bus to an interface. $this->app->bind(CommandBusInterface::class, PackageCommandBus::class) lets you swap in a fake or a decorated bus later without touching call sites.

Gotchas and Tips

  • Handler registration is usually the fragile part. If handlers are mapped in a config array, config:cache will bake that map in — adding a new command in production means re-running php artisan config:cache. Confirm this early; it's the most common source of "works locally, fails in prod."
  • Small package, no dependents — treat it as a liability. With 2 stars and 0 dependents, bus-factor is 1 and maintenance is uncertain. Pin the version exactly ("revision-ten/cqrs": "1.x" with a committed composer.lock), and if you depend on it heavily, consider vendoring the bus into app/Infrastructure/ — it's likely a few hundred lines, and owning it removes an upgrade risk.
  • Read the constructor of the bus class. Whether it resolves handlers from the container (and therefore supports constructor injection) or instantiates them with new changes how you write every handler. Don't assume Laravel-style DI.
  • Commands must be immutable value objects. public readonly properties (PHP 8.1+), no setters, no Eloquent models as constructor params (they serialize badly and mutate). Pass scalars/IDs.
  • Don't inject request state into commands. Resolve Request values in the controller and pass primitives. This keeps commands replayable from a queue or an audit log.
  • Nested dispatch needs care. If a handler dispatches another command and both are transaction-wrapped, you get nested transactions. Laravel handles this with savepoints, but only for the default connection — set the connection explicitly in the middleware.
  • Debugging dispatch failures: add a first-and-last middleware that logs get_class($command) and timing. Since there's no Laravel debug integration for this package, a two-line logging middleware will save you hours versus dd() inside handlers.
  • Extension points to know: the middleware stack (add your own), the container bindings for bus/handler-resolver (swap implementations), and — if present — the config's handler map (replace with auto-discovery from app/**/Handlers if you find the manual map tedious).
  • Watch for return-type mismatches. If the bus's dispatch() signature declares void but handlers return values, PHP will silently swallow or error depending on strictness. Check declare(strict_types=1) in the package's files.
  • If the package is abandoned mid-project, the escape hatch is a thin App\Cqrs\Bus you own that mirrors the same dispatch() API: $handler = app($this->map[$command::class] ?? ...). Swapping is a one-line container binding change if you bound to an interface from day one — which is the strongest argument for doing that on step one.
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.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console