samir-hussein/laravel-cqrs

A clean CQRS (Command Query Responsibility Segregation) pattern implementation for Laravel applications


Keywords
pattern, command, query, laravel, cqrs, architecture
License
MIT

Documentation

Laravel CQRS Package

A clean and simple CQRS (Command Query Responsibility Segregation) pattern implementation for Laravel applications.

Features

  • Simple & Clean: Easy to understand and implement
  • Auto-resolution: Handlers are automatically resolved based on naming conventions
  • Laravel Integration: Seamless integration with Laravel's service container
  • Type-safe: Full type hints and interfaces
  • Flexible: Works with any Laravel project structure
  • Validation Support: Built-in validation methods for commands and queries
  • Middleware Pipeline: Wrap handlers with middleware for logging, transactions, authorization, etc.
  • Universal Dispatch: Single dispatch() method that works with both Commands and Queries

Installation

Step 1: Install via Composer

composer require samir-hussein/laravel-cqrs

The package will be automatically discovered by Laravel.

Step 2: Publish Configuration (Optional)

php artisan vendor:publish --tag=cqrs-config

This creates config/cqrs.php where you can configure namespaces, middleware, and handler mappings.

Step 3: Create Directory Structure (Optional)

The package will create directories automatically when you use Artisan commands. Or create them manually:

app/
├── CQRS/
│   ├── Commands/
│   ├── Queries/
│   └── Handlers/

Step 4: Use Artisan Commands (Recommended)

The package includes Artisan commands to generate CQRS files:

# Create a command
php artisan cqrs:command User/CreateUserCommand

# Create a query
php artisan cqrs:query User/GetUserQuery

# Create a command handler
php artisan cqrs:handler User/CreateUserCommandHandler --type=command

# Create a query handler
php artisan cqrs:handler User/GetUserQueryHandler --type=query

That's it! You're ready to use the package.

Example 1: Creating and Using a Command

This example shows how to create a command with validation and middleware support.

1. Create the Command

<?php

namespace App\CQRS\Commands\User;

use LaravelCQRS\Command;

class CreateUserCommand extends Command
{
    /**
     * Create a new command instance.
     *
     * @param array $data
     */
    public function __construct(array $data = [])
    {
        parent::__construct($data);
    }

    public function getName(): ?string
    {
        return $this->get('name');
    }

    public function getEmail(): ?string
    {
        return $this->get('email');
    }

    public function getPassword(): ?string
    {
        return $this->get('password');
    }

    /**
     * Define validation rules
     */
    public function rules(): array
    {
        return [
            'name' => ['required', 'string', 'max:255'],
            'email' => ['required', 'email', 'unique:users,email'],
            'password' => ['required', 'string', 'min:8'],
        ];
    }

    /**
     * Custom validation messages (optional)
     */
    public function messages(): array
    {
        return [
            'email.unique' => 'This email address is already registered.',
            'password.min' => 'Password must be at least 8 characters.',
        ];
    }
}

2. Create the Command Handler

Using Artisan (Recommended):

php artisan cqrs:handler User/CreateUserCommandHandler --type=command

Or manually create the file:

<?php

namespace App\CQRS\Handlers\User;

use App\Models\User;
use App\Repositories\UserRepository;
use LaravelCQRS\Command;
use LaravelCQRS\Commands\User\CreateUserCommand;
use LaravelCQRS\Contracts\CommandHandlerInterface;
use Illuminate\Support\Facades\Hash;

class CreateUserCommandHandler implements CommandHandlerInterface
{
    public function __construct(
        private UserRepository $userRepository
    ) {}

    public function handle(Command $command): User
    {
        /** @var CreateUserCommand $command */
        $data = $command->getData();
        
        // Hash password before creating user
        if (isset($data['password'])) {
            $data['password'] = Hash::make($data['password']);
        }

        return $this->userRepository->create($data);
    }
}

3. Configure Middleware (Optional)

Add middleware in config/cqrs.php:

'middleware' => [
    'global' => [
        \App\CQRS\Middleware\LoggingMiddleware::class,
    ],
    'App\CQRS\Commands\User\CreateUserCommand' => [
        \App\CQRS\Middleware\TransactionMiddleware::class,
        \App\CQRS\Middleware\AuthorizationMiddleware::class,
    ],
],

4. Use in Controller

<?php

namespace App\Http\Controllers;

use App\CQRS\Commands\User\CreateUserCommand;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use LaravelCQRS\CQRS;

class UserController extends Controller
{
    public function store(Request $request): JsonResponse
    {
        // CQRS::dispatch() automatically:
        // 1. Validates the command (using rules() method)
        // 2. Applies middleware (if configured)
        // 3. Dispatches to the handler
        $user = CQRS::dispatch(new CreateUserCommand($request->all()));

        return response()->json($user, 201);
    }
}

What happens:

  1. Validation: Command data is validated using rules() method
  2. Middleware: Global and command-specific middleware are executed
  3. Handler: CreateUserCommandHandler processes the command
  4. Response: User is created and returned

Example 2: Creating and Using a Query

This example shows how to create a query with validation and middleware support.

1. Create the Query

Using Artisan (Recommended):

php artisan cqrs:query User/GetUserQuery

Or manually create the file:

<?php

namespace App\CQRS\Queries\User;

use LaravelCQRS\Query;

class GetUserQuery extends Query
{
    /**
     * Create a new query instance.
     *
     * @param array $data
     */
    public function __construct(array $data = [])
    {
        parent::__construct($data);
    }

    public function getId(): int|string|null
    {
        return $this->get('id');
    }

    /**
     * Define validation rules
     */
    public function rules(): array
    {
        return [
            'id' => ['required', 'integer', 'min:1'],
        ];
    }

    /**
     * Custom validation messages (optional)
     */
    public function messages(): array
    {
        return [
            'id.required' => 'User ID is required.',
            'id.integer' => 'User ID must be a valid number.',
        ];
    }
}

2. Create the Query Handler

Using Artisan (Recommended):

php artisan cqrs:handler User/GetUserQueryHandler --type=query

Or manually create the file:

<?php

namespace App\CQRS\Handlers\User;

use App\Models\User;
use App\Repositories\UserRepository;
use LaravelCQRS\Contracts\QueryHandlerInterface;
use LaravelCQRS\Query;
use LaravelCQRS\Queries\User\GetUserQuery;

class GetUserQueryHandler implements QueryHandlerInterface
{
    public function __construct(
        private UserRepository $userRepository
    ) {}

    public function handle(Query $query): ?User
    {
        /** @var GetUserQuery $query */
        return $this->userRepository->find($query->getId());
    }
}

3. Configure Middleware (Optional)

Add middleware in config/cqrs.php:

'middleware' => [
    'global' => [
        \App\CQRS\Middleware\LoggingMiddleware::class,
    ],
    'App\CQRS\Queries\User\GetUserQuery' => [
        \App\CQRS\Middleware\CacheMiddleware::class, // Cache query results
    ],
],

4. Use in Controller

<?php

namespace App\Http\Controllers;

use App\CQRS\Queries\User\GetUserQuery;
use Illuminate\Http\JsonResponse;
use LaravelCQRS\CQRS;

class UserController extends Controller
{
    public function show(string $id): JsonResponse
    {
        // CQRS::dispatch() automatically:
        // 1. Validates the query (using rules() method)
        // 2. Applies middleware (if configured - e.g., caching)
        // 3. Dispatches to the handler
        $user = CQRS::dispatch(new GetUserQuery(['id' => $id]));

        if (!$user) {
            return response()->json(['error' => 'User not found'], 404);
        }

        return response()->json($user);
    }
}

What happens:

  1. Validation: Query parameters are validated using rules() method
  2. Middleware: Global and query-specific middleware are executed (e.g., caching)
  3. Handler: GetUserQueryHandler processes the query
  4. Response: User data is returned (or 404 if not found)

Validation

Commands and Queries support built-in validation. Override these methods in your command/query classes:

  • rules(): array - Define validation rules
  • messages(): array - Custom error messages (optional)
  • attributes(): array - Custom attribute names (optional)

The CQRS::dispatch() method automatically validates before dispatching. If validation fails, a ValidationException is thrown.

Manual validation:

$command = new CreateUserCommand($request->all());

// Check if valid
if ($command->isValid()) {
    // Get errors
    $errors = $command->errors();
}

// Or validate and get validated data
$validatedData = $command->validate();

See VALIDATION_USAGE.md for complete validation guide.

Middleware

Middleware allows you to wrap handler execution for cross-cutting concerns:

  • Logging: Log all command/query executions
  • Transactions: Wrap database operations in transactions
  • Authorization: Check permissions before execution
  • Caching: Cache query results
  • Performance Monitoring: Track execution time

Create middleware:

<?php

namespace App\CQRS\Middleware;

use LaravelCQRS\Command;
use LaravelCQRS\Contracts\MiddlewareInterface;
use LaravelCQRS\Query;
use Closure;
use Illuminate\Support\Facades\DB;

class TransactionMiddleware implements MiddlewareInterface
{
    public function handle(Command|Query $commandOrQuery, Closure $next): mixed
    {
        if ($commandOrQuery instanceof Command) {
            return DB::transaction(function () use ($commandOrQuery, $next) {
                return $next($commandOrQuery);
            });
        }
        
        return $next($commandOrQuery);
    }
}

Configure in config/cqrs.php:

'middleware' => [
    'global' => [
        \App\CQRS\Middleware\LoggingMiddleware::class,
    ],
    'App\CQRS\Commands\User\CreateUserCommand' => [
        \App\CQRS\Middleware\TransactionMiddleware::class,
    ],
],

See MIDDLEWARE_USAGE.md for complete middleware guide.

Naming Conventions

The package automatically resolves handlers based on naming:

  • Command: App\CQRS\Commands\User\CreateUserCommand

  • Handler: App\CQRS\Handlers\User\CreateUserCommandHandler

  • Query: App\CQRS\Queries\User\GetUserQuery

  • Handler: App\CQRS\Handlers\User\GetUserQueryHandler

Configuration

The configuration file (config/cqrs.php) includes:

  • handler_namespace: Base namespace for handlers (default: App\CQRS\Handlers)
  • command_namespace: Base namespace for commands (default: App\CQRS\Commands)
  • query_namespace: Base namespace for queries (default: App\CQRS\Queries)
  • auto_resolve_handlers: Enable/disable auto-resolution (default: true)
  • handler_mappings: Manual mappings for custom handler locations
  • middleware: Global and command/query-specific middleware configuration

Artisan Commands

The package includes convenient Artisan commands to generate CQRS files:

Create a Command

php artisan cqrs:command User/CreateUserCommand

This creates: app/CQRS/Commands/User/CreateUserCommand.php

Create a Query

php artisan cqrs:query User/GetUserQuery

This creates: app/CQRS/Queries/User/GetUserQuery.php

Create a Handler

# Create a command handler
php artisan cqrs:handler User/CreateUserCommandHandler --type=command

# Create a query handler
php artisan cqrs:handler User/GetUserQueryHandler --type=query

This creates:

  • app/CQRS/Handlers/User/CreateUserCommandHandler.php (for commands)
  • app/CQRS/Handlers/User/GetUserQueryHandler.php (for queries)

Note: The --type option is required for handlers to determine which interface and base class to use.

Using Buses Directly

You can also use the buses directly instead of the CQRS helper:

use LaravelCQRS\Bus\CommandBus;
use LaravelCQRS\Bus\QueryBus;

// In controller
public function __construct(
    private CommandBus $commandBus,
    private QueryBus $queryBus
) {}

public function store(Request $request): JsonResponse
{
    $command = new CreateUserCommand($request->all());
    $command->validate(); // Manual validation
    $user = $this->commandBus->dispatch($command);
    return response()->json($user, 201);
}

Architecture Flow

Route → Controller → CQRS::dispatch() → Validation → Middleware Pipeline → Handler → Repository

Requirements

  • PHP 8.2+
  • Laravel 10.0+ | 11.0+ | 12.0+

License

MIT

Support

For issues and questions, please open an issue on GitHub.