Praktické příklady
Příklad: E-commerce aplikace
V této části si ukážeme, jak implementovat část e-commerce aplikace pomocí vertikální slice architektury a CQRS v Symfony 7. Zaměříme se na funkcionalitu košíku a objednávek.
Struktura projektu
src/
├── Cart/ # Bounded Context: Košík
│ ├── Domain/ # Doménová vrstva
│ │ ├── Model/ # Doménové modely
│ │ │ ├── Cart.php # Entita košíku (Aggregate Root)
│ │ │ └── CartItem.php # Entita položky košíku
│ │ ├── ValueObject/ # Hodnotové objekty
│ │ │ ├── CartId.php # Identifikátor košíku
│ │ │ ├── ProductId.php # Identifikátor produktu
│ │ │ ├── Quantity.php # Množství
│ │ │ └── Money.php # Peněžní částka
│ │ ├── Event/ # Doménové události
│ │ │ └── ItemAddedToCart.php # Událost přidání položky
│ │ └── Repository/ # Repozitáře (rozhraní)
│ │ └── CartRepository.php # Rozhraní pro práci s košíkem
│ ├── Infrastructure/ # Infrastrukturní vrstva
│ │ └── Repository/ # Implementace repozitářů
│ │ └── DoctrineCartRepository.php # Doctrine implementace
│ ├── Application/ # Aplikační vrstva
│ │ ├── Command/ # Příkazy
│ │ │ ├── AddItemToCart.php # Příkaz pro přidání položky
│ │ │ └── AddItemToCartHandler.php # Handler příkazu
│ │ └── Query/ # Dotazy
│ │ ├── GetCart.php # Dotaz pro získání košíku
│ │ └── GetCartHandler.php # Handler dotazu
│ └── Presentation/ # Prezentační vrstva
│ ├── Controller/ # Kontrolery
│ │ ├── CartController.php # Kontroler pro košík
│ │ └── CheckoutController.php # Kontroler pro pokladnu
│ └── ViewModel/ # View modely
│ └── CartViewModel.php # View model košíku
├── Order/ # Bounded Context: Objednávky
│ ├── Domain/ # Doménová vrstva
│ │ ├── Model/ # Doménové modely
│ │ │ ├── Order.php # Entita objednávky (Aggregate Root)
│ │ │ └── OrderItem.php # Entita položky objednávky
│ │ ├── ValueObject/ # Hodnotové objekty
│ │ │ └── OrderId.php # Identifikátor objednávky
│ │ ├── Event/ # Doménové události
│ │ │ └── OrderCreated.php # Událost vytvoření objednávky
│ │ └── Repository/ # Repozitáře (rozhraní)
│ │ └── OrderRepository.php # Rozhraní pro práci s objednávkami
│ ├── Infrastructure/ # Infrastrukturní vrstva
│ │ └── Repository/ # Implementace repozitářů
│ │ └── DoctrineOrderRepository.php # Doctrine implementace
│ ├── Application/ # Aplikační vrstva
│ │ └── Command/ # Příkazy
│ │ ├── CreateOrder.php # Příkaz pro vytvoření objednávky
│ │ └── CreateOrderHandler.php # Handler příkazu
│ └── Presentation/ # Prezentační vrstva
│ └── Controller/ # Kontrolery
│ └── OrderController.php # Kontroler pro objednávky
└── Shared/ # Sdílené komponenty
├── Domain/ # Sdílená doménová logika
│ └── Exception/ # Výjimky
│ └── DomainException.php # Základní doménová výjimka
└── Infrastructure/ # Sdílená infrastruktura
└── Bus/ # Implementace message bus
├── MessengerCommandBus.php # Implementace command bus
└── MessengerQueryBus.php # Implementace query bus
Doménový model: Košík
<?php
namespace App\Cart\Domain\Model;
use App\Cart\Domain\Event\ItemAddedToCart;
use App\Cart\Domain\ValueObject\CartId;
use App\Cart\Domain\ValueObject\ProductId;
use App\Cart\Domain\ValueObject\Quantity;
use App\Cart\Domain\ValueObject\Money;
class Cart
{
private CartId $id;
private string $userId;
private array $items = [];
private \DateTimeImmutable $createdAt;
private \DateTimeImmutable $updatedAt;
private array $events = [];
public function __construct(CartId $id, string $userId)
{
$this->id = $id;
$this->userId = $userId;
$this->createdAt = new \DateTimeImmutable();
$this->updatedAt = $this->createdAt;
}
public function id(): CartId
{
return $this->id;
}
public function userId(): string
{
return $this->userId;
}
public function addItem(ProductId $productId, Quantity $quantity, Money $price): void
{
// Kontrola, zda produkt již v košíku existuje
foreach ($this->items as $item) {
if ($item->productId()->equals($productId)) {
$item->increaseQuantity($quantity);
$this->updatedAt = new \DateTimeImmutable();
$this->recordEvent(new ItemAddedToCart(
$this->id,
$productId,
$quantity,
$price
));
return;
}
}
// Přidání nové položky do košíku
$this->items[] = new CartItem(
$this->id,
$productId,
$quantity,
$price
);
$this->updatedAt = new \DateTimeImmutable();
$this->recordEvent(new ItemAddedToCart(
$this->id,
$productId,
$quantity,
$price
));
}
public function removeItem(ProductId $productId): void
{
$this->items = array_filter($this->items, function (CartItem $item) use ($productId) {
return !$item->productId()->equals($productId);
});
$this->updatedAt = new \DateTimeImmutable();
}
public function items(): array
{
return $this->items;
}
public function isEmpty(): bool
{
return empty($this->items);
}
public function totalAmount(): Money
{
$total = new Money(0);
foreach ($this->items as $item) {
$total = $total->add($item->totalPrice());
}
return $total;
}
public function createdAt(): \DateTimeImmutable
{
return $this->createdAt;
}
public function updatedAt(): \DateTimeImmutable
{
return $this->updatedAt;
}
private function recordEvent(object $event): void
{
$this->events[] = $event;
}
public function releaseEvents(): array
{
$events = $this->events;
$this->events = [];
return $events;
}
}
Command: Přidání položky do košíku
<?php
namespace App\Cart\Application\Command;
use Symfony\Component\Validator\Constraints as Assert;
class AddItemToCart
{
public function __construct(
#[Assert\NotBlank]
#[Assert\Uuid]
public readonly string $cartId,
#[Assert\NotBlank]
#[Assert\Uuid]
public readonly string $productId,
#[Assert\NotBlank]
#[Assert\GreaterThan(0)]
public readonly int $quantity,
#[Assert\NotBlank]
#[Assert\GreaterThan(0)]
public readonly float $price
) {
}
}
Command Handler: Zpracování přidání položky do košíku
<?php
namespace App\Cart\Application\Command;
use App\Cart\Domain\Repository\CartRepository;
use App\Cart\Domain\ValueObject\CartId;
use App\Cart\Domain\ValueObject\ProductId;
use App\Cart\Domain\ValueObject\Quantity;
use App\Cart\Domain\ValueObject\Money;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
#[AsMessageHandler]
class AddItemToCartHandler
{
public function __construct(
private CartRepository $cartRepository
) {
}
public function __invoke(AddItemToCart $command): void
{
$cart = $this->cartRepository->findById(new CartId($command->cartId));
if (!$cart) {
throw new \DomainException('Cart not found');
}
$cart->addItem(
new ProductId($command->productId),
new Quantity($command->quantity),
new Money($command->price)
);
$this->cartRepository->save($cart);
}
}
Controller: Přidání položky do košíku
<?php
namespace App\Cart\Presentation\Controller;
use App\Cart\Application\Command\AddItemToCart;
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 CartController extends AbstractController
{
public function __construct(
private MessageBusInterface $commandBus
) {
}
#[Route('/cart/add', name: 'cart_add', methods: ['POST'])]
public function addToCart(Request $request): Response
{
$cartId = $request->getSession()->get('cart_id');
if (!$cartId) {
// Vytvoření nového košíku by mělo být implementováno v jiném handleru
throw new \RuntimeException('Cart not initialized');
}
$command = new AddItemToCart(
$cartId,
$request->request->get('product_id'),
(int) $request->request->get('quantity', 1),
(float) $request->request->get('price')
);
try {
$this->commandBus->dispatch($command);
$this->addFlash('success', 'Product added to cart');
return $this->redirectToRoute('cart_view');
} catch (\DomainException $e) {
$this->addFlash('error', $e->getMessage());
return $this->redirectToRoute('product_detail', [
'id' => $request->request->get('product_id')
]);
}
}
}
Query: Získání košíku
<?php
namespace App\Cart\Application\Query;
use Symfony\Component\Validator\Constraints as Assert;
class GetCart
{
public function __construct(
#[Assert\NotBlank]
#[Assert\Uuid]
public readonly string $cartId
) {
}
}
Query Handler: Zpracování získání košíku
<?php
namespace App\Cart\Application\Query;
use App\Cart\Domain\Repository\CartRepository;
use App\Cart\Domain\ValueObject\CartId;
use App\Cart\Presentation\ViewModel\CartViewModel;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
#[AsMessageHandler]
class GetCartHandler
{
public function __construct(
private CartRepository $cartRepository
) {
}
public function __invoke(GetCart $query): ?CartViewModel
{
$cart = $this->cartRepository->findById(new CartId($query->cartId));
if (!$cart) {
return null;
}
$items = [];
foreach ($cart->items() as $item) {
$items[] = new CartItemViewModel(
$item->productId()->value(),
$item->quantity()->value(),
$item->price()->value(),
$item->totalPrice()->value()
);
}
return new CartViewModel(
$cart->id()->value(),
$items,
$cart->totalAmount()->value(),
$cart->updatedAt()
);
}
}
Příklad: Blog
V této části si ukážeme, jak implementovat jednoduchý blog pomocí vertikální slice architektury a CQRS v Symfony 7.
Struktura projektu
src/
├── Blog/ # Bounded Context: Blog
│ ├── Domain/ # Doménová vrstva
│ │ ├── Model/ # Doménové modely
│ │ │ ├── Post.php # Entita příspěvku (Aggregate Root)
│ │ │ └── Comment.php # Entita komentáře
│ │ ├── ValueObject/ # Hodnotové objekty
│ │ │ ├── PostId.php # Identifikátor příspěvku
│ │ │ └── CommentId.php # Identifikátor komentáře
│ │ ├── Event/ # Doménové události
│ │ │ └── PostCreated.php # Událost vytvoření příspěvku
│ │ └── Repository/ # Repozitáře (rozhraní)
│ │ └── PostRepository.php # Rozhraní pro práci s příspěvky
│ ├── Infrastructure/ # Infrastrukturní vrstva
│ │ └── Repository/ # Implementace repozitářů
│ │ └── DoctrinePostRepository.php # Doctrine implementace
│ ├── Application/ # Aplikační vrstva
│ │ ├── Command/ # Příkazy
│ │ │ ├── CreatePost.php # Příkaz pro vytvoření příspěvku
│ │ │ └── CreatePostHandler.php # Handler příkazu
│ │ └── Query/ # Dotazy
│ │ ├── GetPost.php # Dotaz pro získání příspěvku
│ │ ├── GetPostHandler.php # Handler dotazu
│ │ ├── GetPosts.php # Dotaz pro získání příspěvků
│ │ └── GetPostsHandler.php # Handler dotazu
│ └── Presentation/ # Prezentační vrstva
│ ├── Controller/ # Kontrolery
│ │ ├── CreatePostController.php # Kontroler pro vytvoření příspěvku
│ │ ├── PostsController.php # Kontroler pro seznam příspěvků
│ │ └── PostController.php # Kontroler pro zobrazení příspěvku
│ └── ViewModel/ # View modely
│ ├── PostViewModel.php # View model příspěvku
│ └── PostListViewModel.php # View model seznamu příspěvků
└── Shared/ # Sdílené komponenty
├── Domain/ # Sdílená doménová logika
│ └── Exception/ # Výjimky
│ └── DomainException.php # Základní doménová výjimka
└── Infrastructure/ # Sdílená infrastruktura
└── Bus/ # Implementace message bus
├── MessengerCommandBus.php # Implementace command bus
└── MessengerQueryBus.php # Implementace query bus
Doménový model: Příspěvek
<?php
namespace App\Blog\Domain\Model;
use App\Blog\Domain\Event\PostCreated;
use App\Blog\Domain\ValueObject\PostId;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
#[ORM\Table(name: 'posts')]
class Post
{
#[ORM\Id]
#[ORM\Column(type: 'string', length: 36)]
private string $id;
#[ORM\Column(type: 'string', length: 255)]
private string $title;
#[ORM\Column(type: 'text')]
private string $content;
#[ORM\Column(type: 'string', length: 255)]
private string $author;
#[ORM\Column(type: 'datetime_immutable')]
private \DateTimeImmutable $createdAt;
#[ORM\Column(type: 'datetime_immutable', nullable: true)]
private ?\DateTimeImmutable $updatedAt = null;
private array $events = [];
public function __construct(PostId $id, string $title, string $content, string $author)
{
$this->id = $id->value();
$this->title = $title;
$this->content = $content;
$this->author = $author;
$this->createdAt = new \DateTimeImmutable();
$this->recordEvent(new PostCreated($id, $title, $author));
}
public function id(): PostId
{
return new PostId($this->id);
}
public function title(): string
{
return $this->title;
}
public function content(): string
{
return $this->content;
}
public function author(): string
{
return $this->author;
}
public function updateTitle(string $title): void
{
$this->title = $title;
$this->updatedAt = new \DateTimeImmutable();
}
public function updateContent(string $content): void
{
$this->content = $content;
$this->updatedAt = new \DateTimeImmutable();
}
public function createdAt(): \DateTimeImmutable
{
return $this->createdAt;
}
public function updatedAt(): ?\DateTimeImmutable
{
return $this->updatedAt;
}
private function recordEvent(object $event): void
{
$this->events[] = $event;
}
public function releaseEvents(): array
{
$events = $this->events;
$this->events = [];
return $events;
}
}
Command: Vytvoření příspěvku
<?php
namespace App\Blog\CreatePost;
use Symfony\Component\Validator\Constraints as Assert;
class CreatePost
{
public function __construct(
#[Assert\NotBlank]
#[Assert\Length(min: 3, max: 255)]
public readonly string $title,
#[Assert\NotBlank]
public readonly string $content,
#[Assert\NotBlank]
public readonly string $author
) {
}
}
Command Handler: Zpracování vytvoření příspěvku
<?php
namespace App\Blog\CreatePost;
use App\Shared\Domain\Model\Post;
use App\Shared\Domain\Repository\PostRepository;
use App\Shared\Domain\ValueObject\PostId;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
#[AsMessageHandler]
class CreatePostHandler
{
public function __construct(
private PostRepository $postRepository
) {
}
public function __invoke(CreatePost $command): string
{
$postId = new PostId();
$post = new Post(
$postId,
$command->title,
$command->content,
$command->author
);
$this->postRepository->save($post);
return $postId->value();
}
}
Příklad: Správa uživatelů
V této části si ukážeme, jak implementovat správu uživatelů pomocí DDD a CQRS v Symfony 7, kde společná doména budou uživatelé a DDD použijeme pro oddělení funkcí.
Struktura projektu
src/
├── UserManagement/ # Feature: Správa uživatelů
│ ├── Registration/ # Sub-feature: Registrace
│ │ ├── RegisterUser.php # Command
│ │ ├── RegisterUserHandler.php # Command Handler
│ │ └── RegistrationController.php # Controller
│ ├── Authentication/ # Sub-feature: Autentizace
│ │ └── SecurityController.php # Controller
│ └── Profile/ # Sub-feature: Profil
│ ├── GetUserProfile.php # Query
│ ├── GetUserProfileHandler.php # Query Handler
│ └── ProfileController.php # Controller
└── Shared/ # Sdílené komponenty
├── Domain/ # Sdílená doménová logika
│ ├── Model/ # Doménové modely
│ │ └── User.php
│ ├── ValueObject/ # Hodnotové objekty
│ │ ├── UserId.php
│ │ └── Email.php
│ ├── Event/ # Doménové události
│ │ └── UserRegistered.php
│ └── Repository/ # Repozitáře (rozhraní)
│ └── UserRepository.php
└── Infrastructure/ # Sdílená infrastruktura
└── Repository/ # Implementace repozitářů
└── DoctrineUserRepository.php
Doménový model: Uživatel
<?php
namespace App\Shared\Domain\Model;
use App\Shared\Domain\Event\UserRegistered;
use App\Shared\Domain\ValueObject\Email;
use App\Shared\Domain\ValueObject\UserId;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;
#[ORM\Entity]
#[ORM\Table(name: 'users')]
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
#[ORM\Id]
#[ORM\Column(type: 'string', length: 36)]
private string $id;
#[ORM\Column(type: 'string', length: 255)]
private string $name;
#[ORM\Column(type: 'string', length: 255, unique: true)]
private string $email;
#[ORM\Column(type: 'string', length: 255)]
private string $password;
#[ORM\Column(type: 'json')]
private array $roles = [];
#[ORM\Column(type: 'datetime_immutable')]
private \DateTimeImmutable $createdAt;
private array $events = [];
public function __construct(UserId $id, string $name, Email $email)
{
$this->id = $id->value();
$this->name = $name;
$this->email = $email->value();
$this->roles = ['ROLE_USER'];
$this->createdAt = new \DateTimeImmutable();
$this->recordEvent(new UserRegistered($id, $email));
}
public function id(): UserId
{
return new UserId($this->id);
}
public function name(): string
{
return $this->name;
}
public function email(): Email
{
return new Email($this->email);
}
public function setPassword(string $password): void
{
$this->password = $password;
}
public function changeName(string $name): void
{
$this->name = $name;
}
public function changeEmail(Email $email): void
{
$this->email = $email->value();
}
public function createdAt(): \DateTimeImmutable
{
return $this->createdAt;
}
// Implementace UserInterface
public function getRoles(): array
{
return $this->roles;
}
public function eraseCredentials(): void
{
// Pokud ukládáte dočasné, citlivé údaje o uživateli, vymažte je zde
}
public function getUserIdentifier(): string
{
return $this->email;
}
// Implementace PasswordAuthenticatedUserInterface
public function getPassword(): string
{
return $this->password;
}
private function recordEvent(object $event): void
{
$this->events[] = $event;
}
public function releaseEvents(): array
{
$events = $this->events;
$this->events = [];
return $events;
}
}
Command: Registrace uživatele
<?php
namespace App\UserManagement\Registration;
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
) {
}
}
Command Handler: Zpracování registrace uživatele
<?php
namespace App\UserManagement\Registration;
use App\Shared\Domain\Model\User;
use App\Shared\Domain\Repository\UserRepository;
use App\Shared\Domain\ValueObject\Email;
use App\Shared\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);
}
}
Důležité poznámky
Při implementaci praktických příkladů je důležité:
- Používat hodnotové objekty pro validaci a enkapsulaci doménových konceptů.
- Používat doménové události pro komunikaci mezi různými částmi aplikace.
- Oddělovat příkazy a dotazy podle CQRS principů.
- Používat Symfony Messenger pro implementaci command a query busů.
- Používat Doctrine ORM pro persistenci doménových objektů.
- Používat validaci pro validaci příkazů a dotazů.
V další kapitole se podíváme na případovou studii implementace DDD a CQRS v Symfony 7.