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