ddd-symfony
Skill3 starsChanged 4 months agoMITView on GitHub
Domain Driven Design in Symfony, three modes. PLACE: decide which building block (entity, value object, aggregate, domain service, event, repository, command, application service), layer and folder a piece of logic belongs in. BUILD: implement a use case or aggregate through the layers in order (Domain → tests → Application → Infrastructure). AUDIT: scan a Symfony codebase for DDD violations (framework leaks in the Domain, anemic models, business rules in services, wrong repositories, events dispatched before commit) and prescribe fixes by rule id. Use whenever the user mentions DDD, aggregate, value object, bounded context, domain event, domain service, or asks "where does this go" / "DDD audit" in a Symfony project.
In this repository32 other files it gives its agents.
- api-contractSkill
- architectureSkill
- blast-radiusSkill
- code-standardsSkill
- deploymentSkill
- design-systemSkill
- information-architectureSkill
- llm-evalSkill
- log-analyzerSkill
- manual-test-casesSkill
- performance-auditSkill
- quality-auditSkill
- readability-sweepSkill
- red-firstSkill
- release-notesSkill
- requirement-intakeSkill
- requirementsSkill
- reviewSkill
- security-auditSkill
- symfony-ai-agentSkill
- symfony-ai-chatSkill
- symfony-ai-platformSkill
- system-designSkill
- system-modelingSkill
- system-oneSkill
- task-breakdownSkill
- task-workflowSkill
- technical-articleSkill
- understandSkill
- user-journey-mapSkill
- vsaSkill
- wireframeSkill
The file
We split this skill into the rules it gives an agent
5 rules. About 0 are common, 2 are distinctive, and 3 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
3
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 25
Versions: Read composer.lock for symfony/framework-bundle, doctrine/orm, doctrine/doctrine-bundle, doctrine/persistence and the PHP …
- Distinctive: line 57
Always in this order. Stop and show the plan first if the change touches more than one aggregate.
- Very specific: line 67
Gates: run the project's existing gates.
- Very specific: line 75
Read-only. Go through references/audit-checklist.md over the scope given (default: src/).
- Distinctive: line 95
Ask before introducing DDD into a context that doesn't use it.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
DDD in Symfony
Business rules live in plain PHP in the Domain. Use cases live in the Application. Symfony, Doctrine and Messenger live in Infrastructure as replaceable adapters. Dependencies point inward, always.
Building-block templates: references/building-blocks.md. Our default Symfony/Doctrine setup:
references/symfony-wiring.md. Audit rules: references/audit-checklist.md.
Step 0 — Read the project (every mode)
- Versions. Read
composer.lockforsymfony/framework-bundle,doctrine/orm,doctrine/doctrine-bundle,doctrine/persistenceand the PHP constraint. Generated code must fit the installed versions. If they're newer than the "Verified on" line insymfony-wiring.md, check the UPGRADE notes for what you're about to use. - Existing patterns. Detect, in this order:
- the context folders (
src/<Context>/{Domain,Application,Infrastructure}or similar) - the Doctrine mapping type and its location (
config/packages/doctrine.yaml) - how events are raised (an in-entity publisher, recorded events pulled from the aggregate, or none)
- the command/handler naming (
*Command+*Service::execute(), or Messenger handlers) - the id type, the test setup and the quality gates (PHPStan, PHPUnit/Pest, CS fixer)
- the context folders (
- Follow the project when a pattern exists; use our defaults only when none does. State which one you used in one line ("Following the project's XML mapping" / "No mapping found, using PHP closure mapping").
Consistency with the codebase beats our preference. Never mix two styles in one context.
PLACE mode
Answer with: building block → layer → path, plus one sentence of why. Decide in this order:
- Can the entity do it? A rule about one aggregate's own state → a method on the aggregate root.
- Is it a value with rules and no identity? → a Value Object (immutable, self-validating).
- A business rule that needs other aggregates or outside data? → a Domain Service in the Domain. It talks to ports only, and its name comes from the domain language.
- Running one use case (load, call the domain, save, transaction)? → an Application Service.
- Technical work (DB, mail, HTTP, queues)? → Infrastructure, behind a port.
Something another context must react to → a Domain Event. It crosses contexts through Messenger and carries ids, never entities.
BUILD mode
Always in this order. Stop and show the plan first if the change touches more than one aggregate.
- Domain: the aggregate/VO/service/event and the repository port. Rules are enforced in constructors and methods. Use the ubiquitous language from requirements or the user's words.
- Domain tests: plain PHPUnit/Pest, no kernel, no DB. One test per business rule, including the failure path.
- Application: a Command (primitives only) + an Application Service (
execute()), tested with an in-memory repository. - Infrastructure: the Doctrine adapter, the mapping, the wiring (
#[AsAlias]), the delivery (controller/console/handler) only if asked. - Gates: run the project's existing gates. Then run
grep -rE 'use (Symfony|Doctrine\\ORM)' src/<Context>/Domain, which must print nothing (DDD-1).
Generate core code only. No speculative ports, no factories with one product, no CQRS read models unless asked.
AUDIT mode
Read-only. Go through references/audit-checklist.md over the scope given (default: src/).
Report each finding as:
DDD-n · file:line · what's wrong · the fix
Rank by risk (broken invariants and event-before-commit first, style last). End with a short summary: the counts per rule and the 3 fixes with the best payoff. Don't edit code in this mode.
Hard rules
- The Domain imports no Symfony and no Doctrine ORM (
doctrine/collectionsis allowed). - Aggregate methods return
void. State changes only through intention-revealing methods: no setters, no public mutation. - One repository per aggregate root. No
flush()inside repositories; the transaction belongs to the application layer. - Application services hold no business
ifs and never return entities (a DTO, an id or nothing). - Domain events are raised inside the Domain and dispatched to other contexts only after commit.
- Commands carry primitives and have no
#[Assert]. The Domain validates. - Cross-context references are ids, never object references.
- Ask before introducing DDD into a context that doesn't use it. DDD is for real business rules; simple CRUD stays plain Symfony.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

