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.
Kdy nepoužívat CQRS
CQRS nemusí být vhodný pro všechny projekty. Nepoužívejte CQRS, pokud:
- Vyvíjíte jednoduchou aplikaci s minimální doménovou logikou.
- Nemáte potřebu oddělovat operace čtení a zápisu.
- Nemáte potřebu škálovat operace čtení a zápisu nezávisle.
- Váš tým nemá zkušenosti s CQRS a nemá čas se ho naučit.
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.
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.
Důležité poznámky
Při implementaci CQRS v Symfony 7 je důležité:
- Používat Messenger komponentu pro implementaci command a query busů.
- Oddělovat příkazy a dotazy do samostatných tříd.
- Používat handlery pro zpracování příkazů a dotazů.
- Používat validaci pro validaci příkazů a dotazů.
- Používat asynchronní zpracování pro příkazy, které mohou být zpracovány asynchronně.
- Používat synchronní zpracování pro dotazy, které vyžadují okamžitou odpověď.
V další kapitole se podíváme na praktické příklady implementace DDD a CQRS v Symfony 7.