Оригинал: CQRS in PHP: The Ultimate Guide for Modern Developers

Перевод для канала Мы ж программист

Введение

Если вы давно создаете приложения на PHP, то наверняка видели, как сервисы или контроллеры делают всё – получают данные, преобразуют их, сохраняют и так далее. Это работает, конечно. Но по мере роста вашего приложения код становится хрупким, нечитаемым и сложным для поддержки.

Вы наверняка спрашивали себя: «Почему эта простая операция чтения запуталась в бизнес-логике и логике сохранения?».

Знакомьтесь с CQRSCommand Query Responsibility Segregation. Модное название для очень простой идеи.

CQRS (Command Query Responsibility Segregation) – это шаблон проектирования, который разделяет чтение данных (запросы) и запись/обновление данных (команды).

➡️ Команды изменяют состояние (например, создание пользователя).

➡️ Запросы считывают состояние (например, получение информации о пользователе).

Разделяя обязанности, вы получаете более чистую архитектуру, упрощаете тестирование и улучшаете масштабируемость.

Ключевые концепции CQRS

Давайте разберемся:

Command (Команда)

Command – это объект, который представляет собой намерение выполнить действие.

Примеры:

CreateUserCommand

UpdateProductStockCommand

DeleteOrderCommand

Команды не возвращают данных – только результат успеха/неудачи.

Query (Запрос)

Query – это объект, представляющий запрос данных.

Примеры:

GetUserByEmailQuery

GetOrdersByCustomerQuery

SearchProductsQuery

Запросы не могут менять состояние системы.

Handlers (Обработчики)

Каждая команда или запрос имеет соответствующий Handler.

CreateUserCommandHandler

GetUserByEmailQueryHandler

Обработчики содержат актуальную логику операции.

Зачем использовать CQRS в PHP?

Вот некоторые основные преимущества:

  • Разделение ответственности — Логика чтения и записи не связаны.
  • Масштабируемость — Вы можете масштабировать чтение и запись независимо друг от друга..
  • Тестируемость — Каждое действие становится тестируемой единицей.
  • Безопасность — Легко реализовать контроль доступа на основе ролей для каждой команды/запроса.
  • Удобство обслуживания — Избегайте раздутых классов служб.

Настройка PHP-проекта к работе с CQRS

Давайте рассмотрим настройку CQRS в PHP-приложении. Для наглядности мы будем использовать обычный PHP (без фреймворков), но это можно легко адаптировать под Laravel, Symfony и т.д.

Структура каталогов:

Plaintext
/src
  /Command
    CreateUserCommand.php
    CreateUserHandler.php
  /Query
    GetUserByEmailQuery.php
    GetUserByEmailHandler.php
  /Bus
    CommandBus.php
    QueryBus.php
  /Handler
    HandlerInterface.php

1. Создание команды и обработчика

CreateUserCommand.php

PHP
class CreateUserCommand
{
    public string $name;
    public string $email;
    public string $password;

    public function __construct(string $name, string $email, string $password)
    {
        $this->name = $name;
        $this->email = $email;
        $this->password = $password;
    }
}

CreateUserHandler.php

PHP
class CreateUserHandler
{
    protected UserRepository $repository;

    public function __construct(UserRepository $repository)
    {
        $this->repository = $repository;
    }

    public function handle(CreateUserCommand $command): void
    {
        $user = new User(
            $command->name,
            $command->email,
            password_hash($command->password, PASSWORD_BCRYPT)
        );

        $this->repository->save($user);
    }
}

2. Создание запроса и обработчика

GetUserByEmailQuery.php

PHP
class GetUserByEmailQuery
{
    public string $email;

    public function __construct(string $email)
    {
        $this->email = $email;
    }
}

GetUserByEmailHandler.php

PHP
class GetUserByEmailHandler
{
    protected UserRepository $repository;

    public function __construct(UserRepository $repository)
    {
        $this->repository = $repository;
    }

    public function handle(GetUserByEmailQuery $query): ?User
    {
        return $this->repository->findByEmail($query->email);
    }
}

3. Построение шины команд и запросов

Шина (bus) направляет команду/запрос к соответствующему обработчику.

CommandBus.php

PHP
class CommandBus
{
    private array $handlers = [];

    public function register(string $commandClass, object $handler): void
    {
        $this->handlers[$commandClass] = $handler;
    }

    public function dispatch(object $command): void
    {
        $class = get_class($command);
        if (!isset($this->handlers[$class])) {
            throw new Exception("No handler found for $class");
        }

        $this->handlers[$class]->handle($command);
    }
}

QueryBus.php

PHP
class QueryBus
{
    private array $handlers = [];

    public function register(string $queryClass, object $handler): void
    {
        $this->handlers[$queryClass] = $handler;
    }

    public function dispatch(object $query): mixed
    {
        $class = get_class($query);
        if (!isset($this->handlers[$class])) {
            throw new Exception("No handler found for $class");
        }

        return $this->handlers[$class]->handle($query);
    }
}

4. Использование системы

Пример команды:

PHP
$commandBus = new CommandBus();
$commandBus->register(CreateUserCommand::class, new CreateUserHandler(new UserRepository()));

$command = new CreateUserCommand("Alice", "alice@example.com", "securepassword");
$commandBus->dispatch($command);

Пример запроса:

PHP
$queryBus = new QueryBus();
$queryBus->register(GetUserByEmailQuery::class, new GetUserByEmailHandler(new UserRepository()));

$query = new GetUserByEmailQuery("alice@example.com");
$user = $queryBus->dispatch($query);

Сравнение CQRS и традиционного CRUD

Когда использовать CQRS в PHP?

Отличные кейсы использования:

  • Крупные корпоративные PHP-приложения
  • Приложения с сильным дисбалансом чтения/записи
  • Приложения, требующие сложной бизнес-логики на каждое действие
  • Архитектура микросервисов
  • Системы с event sourcing

Возможно, не лучший выбор, если:

  • Вы создаете небольшое CRUD-приложение
  • Вы хотите, чтобы все было просто
  • У вас нет команды для разработки сложных паттернов

Запомните: CQRS – это не серебряная пуля.

Бонус: CQRS + Event Sourcing = 💥

В паре с Event Sourcing CQRS становится еще более мощным.

Вместо того чтобы хранить текущее состояние, вы храните каждое изменение (событие) и восстанавливаете состояние из истории.

Пример:

UserCreatedEvent

UserEmailChangedEvent

Каждая команда приводит к одному или нескольким событиям в домене. Вы сохраняете их вместо прямого обновления строк.

Инструменты и библиотеки для CQRS в PHP

Если вы не хотите изобретать велосипед, то вот отличные варианты:

  • 🔧 Broadway — CQRS + Event Sourcing (https://github.com/broadway/broadway)
  • 🧩 Prooph Components — Event-sourced и CQRS компоненты for PHP
  • 🧱 Tactician — Простая шина команд (https://github.com/thephpleague/tactician)
  • 🚀 Laravel-QueryBus — Для приложений на Laravel
  • 🔍 Symfony Messenger — Может быть адаптировано для шины в стиле CQRS

Заключительные мысли

CQRS в PHP – это переломный момент для растущих приложений. Хотя поначалу это может показаться пугающим, отдача в виде ясности, тестируемости и масштабируемости огромна.

Не стоит с первого дня полностью переходить на CQRS. Начните с малого. Добавьте разделение команд и запросов там, где это имеет смысл.

Совет профессионала: можно сочетать CQRS с традиционными подходами. Это не все или ничего!