CQRS in Laravel: Commands, Handlers, Query Objects | Mohamed Said        [  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png)   Mohamed Said Laravel Backend Engineer  ](https://msaied.com) [ Home ](https://msaied.com) [ Projects ](https://msaied.com/projects) [ Articles  ](https://msaied.com/articles) [ Certificates ](https://msaied.com/certificates) [ Contact ](https://msaied.com#contact-section) 

       [  ](https://github.com/EG-Mohamed)       

 [ Home ](https://msaied.com) [ Projects ](https://msaied.com/projects) [ Articles ](https://msaied.com/articles) [ Certificates ](https://msaied.com/certificates) [ Contact ](https://msaied.com#contact-section) 

  [ home ](https://msaied.com)    [ articles ](https://msaied.com/articles)    CQRS in Laravel Without a Framework: Commands, Handlers, and Query Objects        On this page       1. [  Why Roll Your Own CQRS Layer? ](#why-roll-your-own-cqrs-layer)
2. [  The Command Bus ](#the-command-bus)
3. [  A Concrete Command and Handler ](#a-concrete-command-and-handler)
4. [  Query Objects Instead of Fat Repositories ](#query-objects-instead-of-fat-repositories)
5. [  Testing with Pest ](#testing-with-pest)
6. [  Middleware on the Bus ](#middleware-on-the-bus)
7. [  Key Takeaways ](#key-takeaways)

  ![CQRS in Laravel Without a Framework: Commands, Handlers, and Query Objects](https://cdn.msaied.com/376/bec9da4b7a7ddeee26dac3df6f5d6c44.png)

  #laravel   #cqrs   #architecture   #clean-code   #testing  

 CQRS in Laravel Without a Framework: Commands, Handlers, and Query Objects 
============================================================================

     6 Jul 2026      3 min read    ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said  

       Table of contents

1. [  01   Why Roll Your Own CQRS Layer?  ](#why-roll-your-own-cqrs-layer)
2. [  02   The Command Bus  ](#the-command-bus)
3. [  03   A Concrete Command and Handler  ](#a-concrete-command-and-handler)
4. [  04   Query Objects Instead of Fat Repositories  ](#query-objects-instead-of-fat-repositories)
5. [  05   Testing with Pest  ](#testing-with-pest)
6. [  06   Middleware on the Bus  ](#middleware-on-the-bus)
7. [  07   Key Takeaways  ](#key-takeaways)

 Why Roll Your Own CQRS Layer?
-----------------------------

Heavy CQRS packages bring conventions you may not need and abstractions that obscure what is actually happening. For most Laravel applications a thin, explicit layer — commands, handlers, and query objects wired through the service container — delivers 90% of the benefit with none of the magic.

This article builds that layer from scratch.

---

The Command Bus
---------------

A command is a plain DTO expressing intent. A handler executes it. A bus resolves the handler and calls it.

```php
// app/CQRS/CommandBus.php
namespace App\CQRS;

use Illuminate\Contracts\Container\Container;

final class CommandBus
{
    public function __construct(private readonly Container $container) {}

    public function dispatch(object $command): mixed
    {
        $handlerClass = $this->resolveHandler($command);
        $handler = $this->container->make($handlerClass);

        return $handler->handle($command);
    }

    private function resolveHandler(object $command): string
    {
        // Convention: App\Commands\FooCommand → App\Handlers\FooHandler
        $base = class_basename($command);
        $handler = str_replace('Command', 'Handler', $base);
        $class = 'App\\Handlers\\' . $handler;

        if (! class_exists($class)) {
            throw new \RuntimeException("No handler found for {$base}");
        }

        return $class;
    }
}

```

Register it as a singleton in `AppServiceProvider`:

```php
$this->app->singleton(CommandBus::class);

```

---

A Concrete Command and Handler
------------------------------

```php
// app/Commands/RegisterUserCommand.php
namespace App\Commands;

final readonly class RegisterUserCommand
{
    public function __construct(
        public string $email,
        public string $name,
        public string $plainPassword,
    ) {}
}

```

```php
// app/Handlers/RegisterUserHandler.php
namespace App\Handlers;

use App\Commands\RegisterUserCommand;
use App\Models\User;
use Illuminate\Support\Facades\Hash;

final class RegisterUserHandler
{
    public function handle(RegisterUserCommand $command): User
    {
        return User::create([
            'email'    => $command->email,
            'name'     => $command->name,
            'password' => Hash::make($command->plainPassword),
        ]);
    }
}

```

In a controller:

```php
$user = $bus->dispatch(new RegisterUserCommand(
    email: $request->email,
    name:  $request->name,
    plainPassword: $request->password,
));

```

The controller knows nothing about hashing, model creation, or side effects.

---

Query Objects Instead of Fat Repositories
-----------------------------------------

Query objects encapsulate a single read concern. They return data, never modify state.

```php
// app/Queries/ActiveSubscribersQuery.php
namespace App\Queries;

use App\Models\User;
use Illuminate\Pagination\CursorPaginator;

final class ActiveSubscribersQuery
{
    public function __construct(
        private readonly int $perPage = 50,
        private readonly ?string $cursor = null,
    ) {}

    public function get(): CursorPaginator
    {
        return User::query()
            ->whereHas('subscription', fn ($q) => $q->active())
            ->orderBy('id')
            ->cursorPaginate($this->perPage, cursor: $this->cursor);
    }
}

```

Call it directly — no bus needed for queries:

```php
$page = (new ActiveSubscribersQuery(
    perPage: 25,
    cursor: $request->query('cursor'),
))->get();

```

---

Testing with Pest
-----------------

Handlers are plain classes, so testing is straightforward:

```php
it('creates a user with a hashed password', function () {
    $command = new RegisterUserCommand(
        email: 'alice@example.com',
        name: 'Alice',
        plainPassword: 'secret',
    );

    $user = app(RegisterUserHandler::class)->handle($command);

    expect($user)
        ->email->toBe('alice@example.com')
        ->and(Hash::check('secret', $user->password))->toBeTrue();
});

```

No HTTP overhead, no mocking the bus — just the handler under test.

---

Middleware on the Bus
---------------------

Add cross-cutting concerns (logging, transactions, authorization) by wrapping `dispatch`:

```php
public function dispatch(object $command): mixed
{
    return DB::transaction(
        fn () => $this->resolveAndCall($command)
    );
}

```

For finer control, accept an array of middleware callables and pipe the command through them before reaching the handler — the same pattern Laravel's own `Pipeline` uses.

---

Key Takeaways
-------------

- **Commands are write-only DTOs**; handlers own all mutation logic.
- **Query objects** replace ad-hoc repository methods with focused, testable read concerns.
- **Convention-based handler resolution** keeps the bus tiny; swap it for explicit mapping when you need more control.
- **No package dependency** means you own the abstraction and can evolve it freely.
- Handlers are plain classes — Pest tests are fast and require no HTTP layer.
- Wrapping `dispatch` in a DB transaction is a one-liner that protects every command uniformly.

 Found this useful?

          [  ](https://twitter.com/intent/tweet?url=https%3A%2F%2Fmsaied.com%2Farticles%2Fcqrs-in-laravel-without-a-framework-commands-handlers-and-query-objects&text=CQRS+in+Laravel+Without+a+Framework%3A+Commands%2C+Handlers%2C+and+Query+Objects) [  ](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fmsaied.com%2Farticles%2Fcqrs-in-laravel-without-a-framework-commands-handlers-and-query-objects) 

 Frequently Asked Questions 
----------------------------

  3 questions  

     Q01  Should every controller action go through the command bus?        No. Simple CRUD with no domain logic can stay in the controller or a single action class. Introduce the bus where you have meaningful side effects, multiple steps, or logic you want to test in isolation. 

      Q02  How do I handle commands that need to dispatch jobs or fire events?        Do it inside the handler. Handlers can inject the Queue, EventDispatcher, or any other service via the container. Keeping side effects inside the handler keeps the controller ignorant of them. 

      Q03  Is this pattern compatible with Filament actions?        Yes. Filament's action closures receive the record and form data, so you can instantiate a command and call the bus directly inside the action's `action` callback without any extra wiring. 

  Continue reading

 More Articles 
---------------

 [ View all    ](https://msaied.com/articles) 

 [ ![Domain-Driven Design in Laravel: Value Objects, DTOs, and Actions Without Bloat](https://cdn.msaied.com/377/b5c2aef77aed51ee8694f377085af424.png) laravel ddd architecture 

### Domain-Driven Design in Laravel: Value Objects, DTOs, and Actions Without Bloat

Learn how to model domain concepts with value objects, DTOs, and single-action classes in Laravel — keeping yo...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 6 Jul 2026     4 min read  

  Read    

 ](https://msaied.com/articles/domain-driven-design-in-laravel-value-objects-dtos-and-actions-without-bloat) [ ![What's Missing from Your PHP Development Environment: Meet DDLess](https://cdn.msaied.com/379/baa8990d4c7b46d18498d69d68f9b6d2.png) DDLess PHP Debugging Laravel Tools 

### What's Missing from Your PHP Development Environment: Meet DDLess

DDLess is a PHP development workbench that brings step debugging, an in-breakpoint playground, and an interact...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 6 Jul 2026     3 min read  

  Read    

 ](https://msaied.com/articles/whats-missing-from-your-php-development-environment-meet-ddless) [ ![Event Sourcing in Laravel: Aggregates, Projectors, and Rebuilding State from Events](https://cdn.msaied.com/375/d5f6b9ed0a38be33a23430a1637a06d5.png) laravel event-sourcing ddd 

### Event Sourcing in Laravel: Aggregates, Projectors, and Rebuilding State from Events

A practical walkthrough of event sourcing in Laravel — defining aggregates, writing projectors, and safely reb...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 6 Jul 2026     4 min read  

  Read    

 ](https://msaied.com/articles/event-sourcing-in-laravel-aggregates-projectors-and-rebuilding-state-from-events) 

   [  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png)   Mohamed Said Laravel Backend Engineer  ](https://msaied.com)Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

Explore

- [Home](https://msaied.com)
- [Projects](https://msaied.com/projects)
- [Articles](https://msaied.com/articles)
- [Certificates](https://msaied.com/certificates)
- [Contact](https://msaied.com#contact-section)

Connect

- [   hello@msaied.com ](mailto:hello@msaied.com)
- [   +20 109 461 9204 ](tel:+201094619204)

© 2026 Mohamed Said. All rights reserved.

 [  ](https://github.com/EG-Mohamed) [  ](https://www.linkedin.com/in/msaiedm/) [  ](https://wa.me/201094619204) [  ](mailto:hello@msaied.com) [  ](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
