Implementace DDD v Symfony 7

Struktura projektu

Při implementaci DDD s vertikální slice architekturou v Symfony 7 je důležité zvolit vhodnou strukturu projektu, která respektuje principy DDD a zejména koncept Bounded Contexts (ohraničených kontextů). Zde je příklad správné struktury projektu pro DDD s vertikální slice architekturou v Symfony 7:

Příklad: Správná struktura projektu pro DDD s vertikální slice architekturou v Symfony 7

src/
├── UserManagement/             # Bounded Context: Správa uživatelů
│   ├── Domain/                 # Doménová vrstva pro UserManagement
│   │   ├── 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/         # Infrastruktura pro UserManagement
│   │   └── Repository/         # Implementace repozitářů
│   │       └── DoctrineUserRepository.php
│   ├── Registration/           # Feature: Registrace uživatelů
│   │   ├── Command/            # Commands
│   │   │   ├── RegisterUser.php
│   │   │   └── RegisterUserHandler.php
│   │   ├── Controller/         # Controllers
│   │   │   └── RegistrationController.php
│   │   ├── Form/               # Forms
│   │   │   └── RegistrationFormType.php
│   │   └── View/               # Views
│   │       └── registration.html.twig
│   └── Profile/                # Feature: Profil uživatele
│       ├── Query/              # Queries
│       │   ├── GetUserProfile.php
│       │   └── GetUserProfileHandler.php
│       ├── Controller/         # Controllers
│       │   └── ProfileController.php
│       ├── Form/               # Forms
│       │   └── ProfileFormType.php
│       └── View/               # Views
│           └── profile.html.twig
├── OrderManagement/           # Bounded Context: Správa objednávek
│   ├── Domain/                # Doménová vrstva pro OrderManagement
│   │   ├── Model/             # Doménové modely
│   │   │   ├── Order.php
│   │   │   └── OrderItem.php
│   │   ├── ValueObject/       # Hodnotové objekty
│   │   │   ├── OrderId.php
│   │   │   └── Money.php
│   │   ├── Event/             # Doménové události
│   │   │   └── OrderCreated.php
│   │   └── Repository/        # Repozitáře (rozhraní)
│   │       └── OrderRepository.php
│   ├── Infrastructure/        # Infrastruktura pro OrderManagement
│   │   └── Repository/        # Implementace repozitářů
│   │       └── DoctrineOrderRepository.php
│   ├── Checkout/              # Feature: Pokladna
│   │   ├── Command/           # Commands
│   │   │   ├── CreateOrder.php
│   │   │   └── CreateOrderHandler.php
│   │   ├── Controller/        # Controllers
│   │   │   └── CheckoutController.php
│   │   ├── Form/              # Forms
│   │   │   └── CheckoutFormType.php
│   │   └── View/              # Views
│   │       └── checkout.html.twig
│   └── OrderHistory/          # Feature: Historie objednávek
│       ├── Query/             # Queries
│       │   ├── GetOrderHistory.php
│       │   └── GetOrderHistoryHandler.php
│       ├── Controller/        # Controllers
│       │   └── OrderHistoryController.php
│       └── View/              # Views
│           └── order_history.html.twig
└── Shared/                    # Skutečně sdílené komponenty
    ├── Domain/                # Sdílená doménová logika
    │   └── ValueObject/       # Sdílené hodnotové objekty
    │       └── Id.php         # Abstraktní ID
    └── Infrastructure/        # Sdílená infrastruktura
        └── Persistence/       # Sdílené komponenty pro persistenci
            └── Doctrine/
                └── Mapping/
                    └── MappingTrait.php

Tato struktura projektu organizuje kód podle ohraničených kontextů (Bounded Contexts) a funkcí (features). Každý ohraničený kontext má svou vlastní doménovou vrstvu, která obsahuje modely, hodnotové objekty, události a repozitáře specifické pro danou doménu. Tím je zajištěna izolace domén a respektování principů DDD.

Prezentační vrstva Aplikační vrstva Doménová vrstva Infrastrukturní vrstva Controller action(Request request): Response Form buildForm() handleRequest(Request request) ViewModel data CommandBus dispatch(Command command) Command properties CommandHandler __invoke(Command command) QueryBus dispatch(Query query) Query properties QueryHandler __invoke(Query query): ViewModel Entity EntityId id methods() ValueObject value equals(ValueObject vo): bool AggregateRoot EntityId id Collection entities array events applyDomainEvent() releaseEvents(): array DomainEvent occurredAt properties Repository findById(EntityId id): Entity save(AggregateRoot ar): void DomainService performDomainOperation() DoctrineRepository EntityManagerInterface entityManager findById(EntityId id): Entity save(AggregateRoot ar): void EventDispatcher dispatch(DomainEvent event) Bounded Contexts: - UserManagement - Domain/Model/User.php - OrderManagement - Domain/Model/Order.php - Domain/Model/OrderItem.php ValueObjects: - UserId.php - Email.php - OrderId.php - Money.php Implementace: - DoctrineUserRepository - DoctrineOrderRepository Struktura projektu: src/ ├── UserManagement/ │ ├── Domain/ │ │ ├── Model/ │ │ ├── ValueObject/ │ │ ├── Event/ │ │ └── Repository/ │ ├── Infrastructure/ │ ├── Registration/ │ └── Profile/ ├── OrderManagement/ │ ├── Domain/ │ ├── Infrastructure/ │ ├── Checkout/ │ └── OrderHistory/ └── Shared/ dispatch dispatch vytváří mapuje na vrací směruje ke používá používá směruje ke používá dědí obsahuje vytváří používá spravuje operuje nad používá implementuje používá

Klíčové principy správné struktury DDD projektu:

  • Izolace domén - Každá doména (Bounded Context) má svůj vlastní model, který odráží její specifické potřeby a jazyk.
  • Ubiquitous Language - Každá doména může mít svůj vlastní jazyk, který je konzistentně používán v kódu.
  • Jasné hranice - Jasně definované hranice mezi doménami pomáhají vývojářům lépe pochopit, kde končí jedna doména a začíná druhá.
  • Minimalizace závislostí - Domény by měly být co nejvíce nezávislé, aby změna v jedné doméně neovlivnila jinou doménu.

Časté chyby při implementaci DDD

Při implementaci DDD v Symfony se vyvarujte těchto častých chyb:

  • Umístění všech doménových modelů do sdílené složky - Každá doména by měla mít své vlastní modely.
  • Sdílení doménových modelů mezi doménami - Pokud potřebujete sdílet data mezi doménami, použijte Anti-Corruption Layer nebo Domain Events.
  • Příliš mnoho závislostí mezi doménami - Domény by měly být co nejvíce nezávislé.
  • Ignorování Ubiquitous Language - Používejte konzistentní jazyk v kódu, dokumentaci a komunikaci.

Implementace entit

Entity v DDD jsou objekty, které jsou definovány svou identitou. V Symfony 7 můžete implementovat entity jako běžné PHP třídy:

Příklad: Implementace entity v Symfony 7

<?php

namespace App\UserManagement\Domain\Model;

use App\UserManagement\Domain\Event\UserRegistered;
use App\UserManagement\Domain\ValueObject\Email;
use App\UserManagement\Domain\ValueObject\UserId;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
#[ORM\Table(name: 'users')]
class User
{
    #[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)]
    private string $email;

    #[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->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 changeName(string $name): void
    {
        $this->name = $name;
    }

    public function changeEmail(Email $email): void
    {
        $this->email = $email->value();
    }

    public function createdAt(): \DateTimeImmutable
    {
        return $this->createdAt;
    }

    private function recordEvent(object $event): void
    {
        $this->events[] = $event;
    }

    public function releaseEvents(): array
    {
        $events = $this->events;
        $this->events = [];

        return $events;
    }
}

V tomto příkladu je User entita, která je definována svou identitou (UserId). Entity mohou také generovat doménové události, které jsou uloženy v poli $events a mohou být později uvolněny a zpracovány.

Implementace hodnotových objektů

Hodnotové objekty v DDD jsou objekty, které jsou definovány svými atributy. V Symfony 7 můžete implementovat hodnotové objekty jako neměnné PHP třídy:

Příklad: Implementace hodnotového objektu v Symfony 7

<?php

namespace App\UserManagement\Domain\ValueObject;

use Doctrine\ORM\Mapping as ORM;

#[ORM\Embeddable]
class Email
{
    #[ORM\Column(type: 'string', length: 255)]
    private string $value;

    public function __construct(string $value)
    {
        if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
            throw new \InvalidArgumentException('Invalid email address');
        }

        $this->value = $value;
    }

    public function value(): string
    {
        return $this->value;
    }

    public function equals(Email $other): bool
    {
        return $this->value === $other->value;
    }

    public function __toString(): string
    {
        return $this->value;
    }
}

V tomto příkladu je Email hodnotový objekt, který je definován svou hodnotou. Hodnotové objekty jsou neměnné a nemají žádnou identitu. Dva e-maily jsou považovány za stejné, pokud mají stejnou hodnotu.

Implementace repozitářů

Repozitáře v DDD poskytují rozhraní pro přístup k agregátům. V Symfony 7 můžete implementovat repozitáře jako rozhraní a jejich implementace:

Příklad: Implementace repozitáře v Symfony 7

<?php

namespace App\UserManagement\Domain\Repository;

use App\UserManagement\Domain\Model\User;
use App\UserManagement\Domain\ValueObject\Email;
use App\UserManagement\Domain\ValueObject\UserId;

interface UserRepository
{
    public function save(User $user): void;

    public function findById(UserId $id): ?User;

    public function findByEmail(Email $email): ?User;
}
<?php

namespace App\UserManagement\Infrastructure\Repository;

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 Doctrine\ORM\EntityManagerInterface;

class DoctrineUserRepository implements UserRepository
{
    private EntityManagerInterface $entityManager;

    public function __construct(EntityManagerInterface $entityManager)
    {
        $this->entityManager = $entityManager;
    }

    public function save(User $user): void
    {
        $this->entityManager->persist($user);
        $this->entityManager->flush();

        foreach ($user->releaseEvents() as $event) {
            $this->entityManager->getEventManager()->dispatchEvent(
                'onDomainEvent',
                new DomainEventArgs($event)
            );
        }
    }

    public function findById(UserId $id): ?User
    {
        return $this->entityManager->find(User::class, $id->value());
    }

    public function findByEmail(Email $email): ?User
    {
        return $this->entityManager->getRepository(User::class)
            ->findOneBy(['email' => $email->value()]);
    }
}

V tomto příkladu je UserRepository rozhraní, které definuje metody pro ukládání a načítání uživatelů. DoctrineUserRepository je implementace tohoto rozhraní, která používá Doctrine ORM pro persistenci.

Implementace doménových služeb

Doménové služby v DDD poskytují doménovou logiku, která nepatří přirozeně do žádné entity nebo hodnotového objektu. V Symfony 7 můžete implementovat doménové služby jako běžné PHP třídy:

Příklad: Implementace doménové služby v Symfony 7

<?php

namespace App\OrderManagement\Checkout\Service;

use App\OrderManagement\Domain\Model\Order;
use App\OrderManagement\Domain\Model\Payment;
use App\OrderManagement\Domain\Repository\PaymentRepository;
use App\OrderManagement\Domain\ValueObject\Money;
use App\OrderManagement\Domain\ValueObject\PaymentId;
use App\OrderManagement\Domain\ValueObject\PaymentMethod;

class PaymentService
{
    private PaymentRepository $paymentRepository;

    public function __construct(PaymentRepository $paymentRepository)
    {
        $this->paymentRepository = $paymentRepository;
    }

    public function processPayment(Order $order, PaymentMethod $paymentMethod): Payment
    {
        if ($order->status() !== OrderStatus::CONFIRMED) {
            throw new \DomainException('Cannot process payment for a non-confirmed order');
        }

        $payment = new Payment(
            new PaymentId(),
            $order->id(),
            $this->calculateTotalAmount($order),
            $paymentMethod
        );

        $this->paymentRepository->save($payment);

        return $payment;
    }

    private function calculateTotalAmount(Order $order): Money
    {
        $total = new Money(0);

        foreach ($order->items() as $item) {
            $total = $total->add($item->price()->multiply($item->quantity()));
        }

        return $total;
    }
}

V tomto příkladu je PaymentService doménová služba, která poskytuje logiku pro zpracování plateb. Tato logika nepatří přirozeně do žádné entity nebo hodnotového objektu.

Implementace doménových událostí

Doménové události v DDD reprezentují něco, co se stalo v doméně. V Symfony 7 můžete implementovat doménové události jako neměnné PHP třídy:

Příklad: Implementace doménové události v Symfony 7

<?php

namespace App\UserManagement\Domain\Event;

use App\UserManagement\Domain\ValueObject\Email;
use App\UserManagement\Domain\ValueObject\UserId;

class UserRegistered
{
    private string $userId;
    private string $email;
    private \DateTimeImmutable $occurredAt;

    public function __construct(UserId $userId, Email $email)
    {
        $this->userId = $userId->value();
        $this->email = $email->value();
        $this->occurredAt = new \DateTimeImmutable();
    }

    public function userId(): UserId
    {
        return new UserId($this->userId);
    }

    public function email(): Email
    {
        return new Email($this->email);
    }

    public function occurredAt(): \DateTimeImmutable
    {
        return $this->occurredAt;
    }
}

V tomto příkladu je UserRegistered doménová událost, která reprezentuje registraci nového uživatele. Tato událost obsahuje informace o tom, který uživatel byl registrován, jaký má e-mail a kdy se to stalo.

Implementace aplikačních služeb

Aplikační služby v DDD koordinují aplikační aktivity a delegují práci doménové vrstvě. V Symfony 7 můžete implementovat aplikační služby jako command a query handlery:

Příklad: Implementace command handleru v Symfony 7

<?php

namespace App\UserManagement\Registration\Command;

class RegisterUser
{
    public function __construct(
        public readonly string $name,
        public readonly string $email,
        public readonly string $password
    ) {
    }
}
<?php

namespace App\UserManagement\Registration\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\Profile\Query;

class GetUserProfile
{
    public function __construct(
        public readonly string $userId
    ) {
    }
}
<?php

namespace App\UserManagement\Profile\Query;

use App\UserManagement\Domain\Repository\UserRepository;
use App\UserManagement\Domain\ValueObject\UserId;
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 aplikační služby, které zpracovávají příkazy a dotazy. Tyto služby koordinují aplikační aktivity a delegují práci doménové vrstvě.

Implementace kontrolerů

Kontrolery v DDD jsou součástí prezentační vrstvy a zodpovídají za interakci s uživatelem. V Symfony 7 můžete implementovat kontrolery jako běžné Symfony kontrolery:

Příklad: Implementace kontroleru 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
{
    #[Route('/register', name: 'app_register')]
    public function register(Request $request, MessageBusInterface $commandBus): 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 {
                $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(),
        ]);
    }
}

V tomto příkladu je RegistrationController kontroler, který zpracovává registraci uživatele. Kontroler vytváří formulář, zpracovává požadavek a odesílá příkaz RegisterUser přes command bus.

Dependency Injection

Dependency Injection je důležitou součástí DDD, protože umožňuje oddělení závislostí a usnadňuje testování. Symfony 7 poskytuje výkonný Dependency Injection Container, který můžete použít pro konfiguraci služeb:

Příklad: Konfigurace služeb v Symfony 7

# config/services.yaml
services:
    _defaults:
        autowire: true
        autoconfigure: true

    # Registrace všech služeb v adresáři src/
    App\:
        resource: '../src/'
        exclude:
            - '../src/Kernel.php'
            - '../src/*/Domain/Model/'
            - '../src/*/Domain/ValueObject/'
            - '../src/*/Domain/Event/'

    # Explicitní konfigurace repozitářů pro UserManagement doménu
    App\UserManagement\Domain\Repository\UserRepository:
        class: App\UserManagement\Infrastructure\Repository\DoctrineUserRepository

    # Explicitní konfigurace repozitářů pro OrderManagement doménu
    App\OrderManagement\Domain\Repository\OrderRepository:
        class: App\OrderManagement\Infrastructure\Repository\DoctrineOrderRepository

    # Konfigurace Messenger komponenty
    messenger.default_bus:
        class: Symfony\Component\Messenger\MessageBus
        arguments:
            - !tagged messenger.bus.middleware

    messenger.command_bus:
        class: Symfony\Component\Messenger\MessageBus
        arguments:
            - !tagged messenger.command_bus.middleware

    messenger.query_bus:
        class: Symfony\Component\Messenger\MessageBus
        arguments:
            - !tagged messenger.query_bus.middleware

V tomto příkladu je konfigurace služeb v Symfony 7. Služby jsou automaticky registrovány a autowired. Repozitáře jsou explicitně konfigurovány, aby bylo možné použít rozhraní místo konkrétních implementací. Messenger komponenta je konfigurována pro implementaci CQRS.

Důležité poznámky

Při implementaci DDD v Symfony 7 je důležité:

  • Používat Dependency Injection pro oddělení závislostí.
  • Používat Messenger komponentu pro implementaci CQRS.
  • Používat Doctrine ORM pro persistenci doménových objektů.
  • Používat atributy pro konfiguraci služeb a routování.
  • Používat formuláře pro zpracování vstupů od uživatele.
  • Používat validaci pro validaci doménových objektů.
  • Respektovat hranice mezi doménami a neumisťovat doménové modely do sdílené složky.
  • Používat Anti-Corruption Layer pro komunikaci mezi doménami, pokud je to nutné.

Co patří do sdílené složky (Shared)?

Do sdílené složky by měly patřit pouze skutečně sdílené komponenty, které nemají specifický doménový význam:

  • Abstraktní třídy pro ID, Entity, ValueObject
  • Utility pro práci s datem a časem
  • Obecné výjimky
  • Infrastrukturní komponenty používané napříč doménami

Doménové modely, hodnotové objekty a repozitáře by měly být umístěny v příslušných doménách, nikoli ve sdílené složce.

V další kapitole se podíváme na implementaci CQRS v Symfony 7.