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.

Cart Bounded Context Domain Model ValueObject Event Repository Infrastructure Repository Application Command Query Presentation Controller ViewModel Order Bounded Context Domain Model ValueObject Event Repository Shared Infrastructure Bus Cart id(): CartId userId(): string addItem(ProductId, Quantity, Money): void removeItem(ProductId): void items(): array totalAmount(): Money CartItem productId(): ProductId quantity(): Quantity price(): Money totalPrice(): Money CartId ProductId Quantity Money ItemAddedToCart CartRepository findById(CartId): ?Cart save(Cart): void DoctrineCartRepository findById(CartId): ?Cart save(Cart): void AddItemToCart cartId: string productId: string quantity: int price: float AddItemToCartHandler __invoke(AddItemToCart): void GetCart cartId: string GetCartHandler __invoke(GetCart): ?CartViewModel CartController addToCart(Request): Response CheckoutController CartViewModel Order OrderItem OrderId OrderCreated OrderRepository MessengerCommandBus MessengerQueryBus 1 many uses publishes uses uses uses implements uses manipulates handles uses creates handles creates dispatches creates dispatches

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.

Blog Bounded Context Domain Model ValueObject Event Repository Infrastructure Repository Application Command Query Presentation Controller ViewModel Shared Infrastructure Bus Post id: string title: string content: string author: string createdAt: \DateTimeImmutable updatedAt: ?\DateTimeImmutable events: array id(): PostId title(): string content(): string author(): string updateTitle(string): void updateContent(string): void createdAt(): \DateTimeImmutable updatedAt(): ?\DateTimeImmutable Comment PostId CommentId PostCreated PostRepository findById(PostId): ?Post save(Post): void DoctrinePostRepository findById(PostId): ?Post save(Post): void CreatePost title: string content: string author: string CreatePostHandler __invoke(CreatePost): string GetPost postId: string GetPostHandler __invoke(GetPost): ?PostViewModel GetPosts GetPostsHandler __invoke(GetPosts): PostListViewModel CreatePostController PostsController PostController PostViewModel PostListViewModel MessengerCommandBus MessengerQueryBus contains 1 many uses publishes implements uses creates handles uses creates handles uses creates handles creates dispatches creates dispatches creates dispatches

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í.

UserManagement Registration Authentication Profile Shared Domain Model ValueObject Event Repository Infrastructure Repository Bus RegisterUser name: string email: string password: string RegisterUserHandler __invoke(RegisterUser): void RegistrationController register(Request): Response SecurityController login(Request): Response logout(): Response GetUserProfile userId: string GetUserProfileHandler __invoke(GetUserProfile): UserProfileViewModel ProfileController show(Request): Response edit(Request): Response update(Request): Response User id: string name: string email: string password: string roles: array createdAt: \DateTimeImmutable events: array id(): UserId name(): string email(): Email setPassword(string): void changeName(string): void changeEmail(Email): void createdAt(): \DateTimeImmutable getRoles(): array eraseCredentials(): void getUserIdentifier(): string getPassword(): string UserId Email UserRegistered UserRepository findById(UserId): ?User findByEmail(Email): ?User save(User): void DoctrineUserRepository findById(UserId): ?User findByEmail(Email): ?User save(User): void MessengerCommandBus MessengerQueryBus uses uses publishes implements uses creates handles uses handles creates dispatches creates dispatches dispatches uses

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);
    }
}

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