--- name: ddd-core description: Règles DDD du projet Mangarr — Aggregates, Value Objects immutables, Domain Events, invariants. Utiliser quand on crée ou modifie un Model, Value Object, Event ou Exception dans src/Domain/*/Domain/. allowed-tools: Read, Grep, Glob --- # Règles DDD — Couche Domain ## Emplacement ``` src/Domain/{DomainName}/Domain/ Model/ {AggregateName}.php ValueObject/ {VoName}.php Event/ {SomethingHappened}.php Exception/ {Something}Exception.php Contract/ Repository/ {Name}RepositoryInterface.php Service/ {Name}Interface.php Client/ {Name}ClientInterface.php ``` ## Aggregates - Classe normale (pas `readonly`), propriétés `private`. - Le constructeur prend des **Value Objects**, jamais des scalaires bruts pour les identifiants et concepts métier. - **Aucune annotation Doctrine** dans le Model — c'est la responsabilité du Repository (Infrastructure). - Les méthodes métier protègent les invariants et lèvent des **Domain Exceptions** (jamais des exceptions génériques). - Les setters publics sont interdits. Exposer des méthodes métier explicites (`updateImageUrls()`, `enableMonitoring()`, etc.). ```php // ✅ Correct class Manga { public function __construct( private MangaId $id, private MangaTitle $title, private MangaSlug $slug, // ... ) {} public function updateImageUrls(ImageUrls $imageUrls): void { $this->imageUrls = $imageUrls; } } // ❌ Interdit class Manga { public string $title; // propriété publique #[ORM\Column] // annotation Doctrine dans le Domain private string $title; public function setTitle(string $title): void {} // setter générique } ``` ## Value Objects - Toujours `readonly class`. - Valider dans le constructeur, lever une **Domain Exception** si invalide. - Exposer `getValue()` pour récupérer la valeur primitive. - Jamais de dépendance externe (pas de Symfony, pas de Doctrine). ```php readonly class MangaTitle { public function __construct(public readonly string $value) { if (empty(trim($value))) { throw new InvalidMangaTitleException('Title cannot be empty'); } } public function getValue(): string { return $this->value; } } ``` Valeurs composées (ex: chemins d'images) → Value Object avec plusieurs propriétés : ```php readonly class ImageUrls { public function __construct( private string $full, private string $thumbnail, ) {} public function getFull(): string { return $this->full; } public function getThumbnail(): string { return $this->thumbnail; } } ``` ## Domain Events - Nommés au **passé** : `MangaCreated`, `ChapterImported`, `MonitoringEnabled`. - `readonly class`, transportent uniquement des scalaires (pas d'objets du Domain). - Placés dans `Domain/Event/`. - Dispatchés depuis le **CommandHandler** (Application), jamais depuis le Domain lui-même. - Le bus utilisé est `MessageBusInterface` de Symfony Messenger (autorisé dans Application, pas dans Domain). ```php readonly class MangaCreated { public function __construct( public string $mangaId, public string $externalId, ) {} } ``` ## Domain Exceptions - Étendent `DomainException` ou `\RuntimeException` selon le cas. - Nommées avec le suffixe `Exception` ou `NotFoundException`. - Localisées dans `Domain/Exception/`. ```php class MangaNotFoundException extends \DomainException { public function __construct() { parent::__construct('Manga not found'); } } ``` ## Règles d'invariants PHPArkitect (enforced automatiquement) - `App\Domain\{X}\Domain` → **aucune dépendance** en dehors de son propre namespace. - Exceptions autorisées : `DateTimeImmutable`, `RuntimeException`, `Exception`, `DomainException`, `InvalidArgumentException`, `Throwable`, `Symfony\Component\HttpKernel\Exception`. - `Ramsey\Uuid` et `Symfony\Component\Messenger` : autorisés uniquement en **Application**, pas en Domain. Vérification : `make phparkitect`