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