Skip to content
Development
Skill

/symfony-domain-modeling

Symfony DDD domain modeling in pure PHP: entities, aggregates, value objects, domain events, domain exceptions, ID generation ports, PHP 8.2-8.5 modeling features (readonly, private(set), clone with). Use when: entity, value object, domain event, aggregate, domain exception,

From plugin
symfony-hexagonal
3510 skills2 agents1 hook

Claude Code

$ npx -y skills add aligundogdu/symfony-hexagonal-skill --skill symfony-domain-modeling --agent claude-code

SKILL.md

symfony-domain-modeling.SKILL.md
name: symfony-domain-modeling
description: "Symfony DDD domain modeling in pure PHP: entities, aggregates, value objects, domain events, domain exceptions, ID generation ports, PHP 8.2-8.5 modeling features (readonly, private(set), clone with). Use when: entity, value object, domain event, aggregate, domain exception, domain model, domain logic, business rule, invariant, enum status"

Symfony Domain Modeling

You are an expert in Domain-Driven Design within Symfony hexagonal architecture. Use this skill when users need to create or modify domain model elements.

When to Activate

  • User wants to create an entity or aggregate root
  • User asks about value objects
  • User needs domain events
  • User mentions domain exceptions or business rules
  • User discusses aggregates or invariants

Version Notes

| Feature | Requires | Fallback | |---|---|---| | `final readonly class` | PHP 8.2 | PHP 8.1: `final class` + `public readonly` properties | | `public private(set)` properties, property hooks | PHP 8.4 + Doctrine ORM 3.4 | private property + getter | | `clone($this, ['amount' => $x])` | PHP 8.5 | `new self(...)` | | `array_find()`, `array_any()`, `array_all()` | PHP 8.4 | `foreach` loop |

Generate the fallback form unless the detected PHP constraint allows the newer syntax.

Entity Patterns

Rules

  • Private constructor + static factory method(s)
  • No Doctrine annotations/attributes, mapping is XML in Infrastructure
  • Record domain events on state changes
  • Protect invariants in methods, not in external validators
  • Use value objects for typed properties (no primitive obsession)
  • Receive IDs and "now" from the caller (generated via ports), never call UUID libraries or `time()` inside the entity
  • Type the event list for PHPStan: `/** @var list<DomainEvent> */`

Template

namespace App\Domain\{Module}\Entity;

use App\Domain\Shared\Event\DomainEvent;

final class {Entity}
{
    /** @var list<DomainEvent> */
    private array $domainEvents = [];

    private function __construct(
        private {Entity}Id $id,
        // typed properties using value objects
    ) {
    }

    public static function create({Entity}Id $id /*, params */): self
    {
        // validate invariants
        $entity = new self($id /*, params */);
        $entity->recordEvent(new {Entity}Created($id->value));

        return $entity;
    }

    // Business methods that protect invariants
    public function doAction(/* params */): void
    {
        // guard clause / invariant check
        // state change
        $this->recordEvent(new {ActionDone}($this->id->value));
    }

    /** @return list<DomainEvent> */
    public function pullDomainEvents(): array
    {
        $events = $this->domainEvents;
        $this->domainEvents = [];

        return $events;
    }

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

Value Object Patterns

Rules

  • Always `final readonly class` (PHP 8.2+)
  • Self-validating in constructor (throw domain exception if invalid)
  • Immutable, no setters; transformations return new instances
  • Implement `equals()` for comparison
  • Use for: IDs, email, money, dates, status enums

Template

namespace App\Domain\{Module}\ValueObject;

use App\Domain\{Module}\Exception\Invalid{ValueObject}Exception;

final readonly class {ValueObject}
{
    public function __construct(
        public string $value,
    ) {
        if (/* validation fails */) {
            throw Invalid{ValueObject}Exception::forValue($value);
        }
    }

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

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

Domain Event Patterns

Rules

  • Past-tense naming: `UserRegistered`, `OrderPlaced`, `PaymentFailed`
  • Immutable (`final readonly class`) implementing `App\Domain\Shared\Event\DomainEvent`
  • Carry only the data needed by handlers (IDs and scalars, not entities), they get serialized on async transports
  • Created inside entities on state changes
  • No `#[AsMessage]` on domain events (it is a Symfony attribute): route them in `messenger.yaml`

Template

namespace App\Domain\{Module}\Event;

use App\Domain\Shared\Event\DomainEvent;

final readonly class {PastTenseEvent} implements DomainEvent
{
    public function __construct(
        public string $aggregateId,
        public \DateTimeImmutable $occurredOn,
        // additional event-specific data
    ) {
    }

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

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

Domain Exception Patterns

namespace App\Domain\{Module}\Exception;

final class {DomainException} extends \DomainException
{
    public static function because{Reason}(string $context): self
    {
        return new self(sprintf('Descriptive message: %s', $context));
    }
}

References

See `references/` for detailed patterns:

  • `entity-patterns.md` - Full entity examples with aggregates
  • `value-object-patterns.md` - Common value object implementations, ID generation port
  • `domain-event-patterns.md` - Event design and dispatch patterns
Read more
@aligundogdu
Ships withsymfony-hexagonal

A Claude Code plugin that enforces hexagonal architecture (ports & adapters) in Symfony projects. Works with both new projects (full scaffolding) and existing projects (progressive, module-by-module refactoring).

3510 skills2 agents
Get the whole plugin

Stars on GitHub

Momentum

stars in total
35
Star history of aligundogdu/symfony-hexagonal-skill on GitHub, a chart by star-history.com

Other skills on symfony-hexagonal.

More skills from other plugins.

brainstorming
Auto-invoked

brainstorming

You MUST use this before any creative work - creating features, building components, adding functionality, modifying behavior, or…

@obra@obraInstall Skill
ask-matt

ask-matt

Ask which skill or flow fits your situation. A router over the skills in this repo.

skills2w309k
@mattpocock@mattpocockInstall Skill
accessibility
Auto-invoked

accessibility

Design, implement, and audit accessible UI to WCAG 2.2 Level AA across Web, iOS, and Android — semantic ARIA roles and labels,…

ecc2w293k
@affaan-m@affaan-mInstall Skill
compiler-commit

compiler-commit

Use when you want to verify compiler changes and commit with the correct convention. Runs tests, lint, and format, then commits…

react5d252k
@facebook@facebookInstall Skill

Questions about symfony-domain-modeling.

Which coding agents can use symfony-domain-modeling?
symfony-domain-modeling installs in Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, OpenCode and Windsurf with the skills CLI, using each agent's flag shown on this page. Flowy's auto-invocation, which fires a skill when your prompt matches, runs in Claude Code only.