CQRS v Symfony 7

Co je CQRS?

CQRS (Command Query Responsibility Segregation) je architektonický vzor, který odděluje operace čtení (queries) od operací zápisu (commands). Tento vzor byl poprvé představen Gregem Youngem jako rozšíření vzoru Command-Query Separation (CQS) od Bertranda Meyera.

Základní principy CQRS:

  • Commands - Příkazy, které mění stav systému, ale nevracejí žádná data.
  • Queries - Dotazy, které vrací data, ale nemění stav systému.
  • Oddělené modely - Oddělené modely pro čtení a zápis, které mohou být optimalizovány pro své specifické úkoly.
  • Oddělené databáze - V pokročilých implementacích mohou být použity oddělené databáze pro čtení a zápis.

CQRS je často používán v kombinaci s Event Sourcing, což je vzor, který ukládá změny stavu jako sekvenci událostí místo ukládání aktuálního stavu.

Výhody CQRS

CQRS přináší mnoho výhod:

  • Oddělení zodpovědností - CQRS odděluje operace čtení od operací zápisu, což vede k čistšímu a udržitelnějšímu kódu.
  • Optimalizace pro specifické úkoly - Modely pro čtení a zápis mohou být optimalizovány pro své specifické úkoly.
  • Škálovatelnost - CQRS umožňuje nezávislé škálování operací čtení a zápisu.
  • Flexibilita - CQRS umožňuje použití různých databází pro čtení a zápis.
  • Testovatelnost - CQRS usnadňuje testování, protože příkazy a dotazy jsou jasně odděleny.

Výzvy a omezení CQRS

CQRS má také své výzvy a omezení:

  • Složitost - CQRS přidává složitost do systému, což může být zbytečné pro jednoduché aplikace.
  • Konzistence - Při použití oddělených databází pro čtení a zápis může být obtížné zajistit konzistenci dat.
  • Latence - Při použití Event Sourcingu může být latence mezi zápisem a čtením.
  • Učební křivka - CQRS může mít strmou učební křivku pro vývojáře, kteří s ním nemají zkušenosti.

Symfony Messenger

Symfony Messenger je komponenta, která usnadňuje implementaci CQRS v Symfony 7. Messenger poskytuje infrastrukturu pro odesílání a zpracování zpráv, což je ideální pro implementaci příkazů a dotazů v CQRS.

CQRS v Symfony 7 CQRS v Symfony 7 Prezentační vrstva Aplikační vrstva - Commands Aplikační vrstva - Queries Doménová vrstva Infrastrukturní vrstva Controller action() injectCommandBus(MessageBusInterface) injectQueryBus(MessageBusInterface) ViewModel data Command properties __construct() CommandHandler __construct(dependencies) __invoke(Command): void CommandBus dispatch(Command) Query properties __construct() QueryHandler __construct(dependencies) __invoke(Query): ?ViewModel QueryBus dispatch(Query) Entity methods() Repository «interface» save(Entity) findById(id) DomainEvent properties DoctrineRepository EntityManagerInterface entityManager save(Entity) findById(id) MessengerComponent handle(message) route(message) Transport sync async RegisterUser CreateOrder UpdateProfile GetUserProfile GetOrderHistory FindProducts Asynchronní zpracování (možné) Mění stav systému Nevrací data Synchronní zpracování Nemodifikuje data Vrací ViewModel sync:// - Synchronní transport doctrine:// - Doctrine transport redis:// - Redis transport amqp:// - RabbitMQ transport dispatch dispatch creates creates routes to routes to modifies uses uses returns implements uses extends extends creates

Konfigurace Symfony Messenger pro CQRS

# config/packages/messenger.yaml
framework:
    messenger:
        # Konfigurace transportů
        transports:
            async: '%env(MESSENGER_TRANSPORT_DSN)%'
            sync: 'sync://'

        # Konfigurace busů
        buses:
            command.bus:
                middleware:
                    - validation
                    - doctrine_transaction

            query.bus:
                middleware:
                    - validation

        # Směrování zpráv
        routing:
            # Příkazy jsou zpracovány asynchronně
            'App\*\*\Command\*': async

            # Dotazy jsou zpracovány synchronně
            'App\*\*\Query\*': sync

V této konfiguraci jsou definovány dva transporty: async pro asynchronní zpracování a sync pro synchronní zpracování. Jsou také definovány dva busy: command.bus pro příkazy a query.bus pro dotazy. Příkazy jsou směrovány na asynchronní transport, zatímco dotazy jsou zpracovány synchronně.

Implementace Commands

Commands v CQRS jsou příkazy, které mění stav systému. V Symfony 7 můžete implementovat příkazy jako jednoduché PHP třídy:

Příklad: Implementace příkazu v Symfony 7

<?php

namespace App\UserManagement\Application\Command;

use Symfony\Component\Validator\Constraints as Assert;

class RegisterUser
{
    public function __construct(
        #[Assert\NotBlank]
        #[Assert\Length(min: 2, max: 255)]
        public readonly string $name,

        #[Assert\NotBlank]
        #[Assert\Email]
        public readonly string $email,

        #[Assert\NotBlank]
        #[Assert\Length(min: 8)]
        public readonly string $password
    ) {
    }
}

V tomto příkladu je RegisterUser příkaz, který obsahuje data potřebná pro registraci uživatele. Příkaz používá atributy pro validaci dat.

Implementace Queries

Queries v CQRS jsou dotazy, které vrací data. V Symfony 7 můžete implementovat dotazy jako jednoduché PHP třídy:

Příklad: Implementace dotazu v Symfony 7

<?php

namespace App\UserManagement\Application\Query;

use Symfony\Component\Validator\Constraints as Assert;

class GetUserProfile
{
    public function __construct(
        #[Assert\NotBlank]
        #[Assert\Uuid]
        public readonly string $userId
    ) {
    }
}

V tomto příkladu je GetUserProfile dotaz, který obsahuje ID uživatele, jehož profil chceme získat. Dotaz používá atributy pro validaci dat.

Implementace Handlers

Handlers v CQRS jsou objekty, které zpracovávají příkazy a dotazy. V Symfony 7 můžete implementovat handlery jako PHP třídy s atributem AsMessageHandler:

Příklad: Implementace command handleru v Symfony 7

<?php

namespace App\UserManagement\Application\Command;

use App\UserManagement\Domain\Model\User;
use App\UserManagement\Domain\Repository\UserRepository;
use App\UserManagement\Domain\ValueObject\Email;
use App\UserManagement\Domain\ValueObject\UserId;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;

#[AsMessageHandler]
class RegisterUserHandler
{
    public function __construct(
        private UserRepository $userRepository,
        private UserPasswordHasherInterface $passwordHasher
    ) {
    }

    public function __invoke(RegisterUser $command): void
    {
        $email = new Email($command->email);

        if ($this->userRepository->findByEmail($email)) {
            throw new \DomainException('User with this email already exists');
        }

        $user = new User(
            new UserId(),
            $command->name,
            $email
        );

        // Set password
        $hashedPassword = $this->passwordHasher->hashPassword($user, $command->password);
        $user->setPassword($hashedPassword);

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

Příklad: Implementace query handleru v Symfony 7

<?php

namespace App\UserManagement\Application\Query;

use App\UserManagement\Domain\Repository\UserRepository;
use App\UserManagement\Domain\ValueObject\UserId;
use App\UserManagement\Presentation\ViewModel\UserProfileViewModel;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;

#[AsMessageHandler]
class GetUserProfileHandler
{
    public function __construct(
        private UserRepository $userRepository
    ) {
    }

    public function __invoke(GetUserProfile $query): ?UserProfileViewModel
    {
        $user = $this->userRepository->findById(new UserId($query->userId));

        if (!$user) {
            return null;
        }

        return new UserProfileViewModel(
            $user->id()->value(),
            $user->name(),
            $user->email()->value(),
            $user->createdAt()
        );
    }
}

V těchto příkladech jsou RegisterUserHandler a GetUserProfileHandler handlery, které zpracovávají příkazy a dotazy. Handlery jsou označeny atributem AsMessageHandler, což umožňuje Symfony Messenger je automaticky registrovat.

Implementace Command a Query Buses

Command a Query Buses v CQRS jsou objekty, které směrují příkazy a dotazy na příslušné handlery. V Symfony 7 můžete použít Messenger komponentu jako command a query busy:

Příklad: Použití command a query busů v Symfony 7

<?php

namespace App\UserManagement\Registration\Controller;

use App\UserManagement\Registration\Command\RegisterUser;
use App\UserManagement\Registration\Form\RegistrationFormType;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Routing\Attribute\Route;

class RegistrationController extends AbstractController
{
    public function __construct(
        private MessageBusInterface $commandBus
    ) {
    }

    #[Route('/register', name: 'app_register')]
    public function register(Request $request): Response
    {
        $form = $this->createForm(RegistrationFormType::class);
        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            $data = $form->getData();

            $command = new RegisterUser(
                $data['name'],
                $data['email'],
                $data['password']
            );

            try {
                $this->commandBus->dispatch($command);

                $this->addFlash('success', 'Your account has been created. You can now log in.');

                return $this->redirectToRoute('app_login');
            } catch (\DomainException $e) {
                $this->addFlash('error', $e->getMessage());
            }
        }

        return $this->render('@UserManagement/Registration/View/registration.html.twig', [
            'form' => $form->createView(),
        ]);
    }
}
<?php

namespace App\UserManagement\Profile\Controller;

use App\UserManagement\Profile\Query\GetUserProfile;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Core\User\UserInterface;

class ProfileController extends AbstractController
{
    public function __construct(
        private MessageBusInterface $queryBus
    ) {
    }

    #[Route('/profile', name: 'app_profile')]
    public function profile(UserInterface $user): Response
    {
        $query = new GetUserProfile($user->getId());

        $profile = $this->queryBus->dispatch($query)->last(HandledStamp::class)->getResult();

        if (!$profile) {
            throw $this->createNotFoundException('User not found');
        }

        return $this->render('@UserManagement/Profile/View/profile.html.twig', [
            'profile' => $profile,
        ]);
    }
}

V těchto příkladech jsou commandBus a queryBus injektovány do kontrolerů a používány pro odesílání příkazů a dotazů. Busy směrují příkazy a dotazy na příslušné handlery.

Asynchronní zpracování

Jednou z výhod CQRS je možnost asynchronního zpracování příkazů. V Symfony 7 můžete použít Messenger komponentu pro asynchronní zpracování:

Konfigurace asynchronního zpracování v Symfony 7

# config/packages/messenger.yaml
framework:
    messenger:
        # Konfigurace transportů
        transports:
            async:
                dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
                options:
                    queue_name: commands
                retry_strategy:
                    max_retries: 3
                    delay: 1000
                    multiplier: 2
                    max_delay: 0

        # Směrování zpráv
        routing:
            # Příkazy jsou zpracovány asynchronně
            'App\*\*\Command\*': async

V této konfiguraci jsou příkazy směrovány na asynchronní transport, což znamená, že budou zpracovány asynchronně. Konfigurace také definuje strategii opakování pro případ selhání.

Spuštění Messenger workeru

$ php bin/console messenger:consume async

Pro zpracování asynchronních zpráv je potřeba spustit Messenger worker, který bude zprávy konzumovat a zpracovávat.

V další kapitole se podíváme na praktické příklady implementace DDD a CQRS v Symfony 7.