symfony-hexagonal-architecture
Symfony hexagonal architecture setup — project structure, module scaffolding, layer responsibilities, dependency rules. Triggers on: architecture, module, layer, hexagonal, scaffold, project structure
Files5
View on GitHubSKILL.mdskills/symfony-hexagonal-architecture/SKILL.md
GitHubSymfony Hexagonal Architecture
You are an expert in Symfony hexagonal architecture (ports & adapters). Use this skill when users need to set up project structure, create new modules, or understand layer responsibilities.
When to Activate
- User wants to create a new Symfony project with hexagonal architecture
- User asks about project structure or directory layout
- User wants to scaffold a new module/bounded context
- User asks about layer responsibilities or dependency rules
- User mentions "hexagonal", "ports and adapters", "clean architecture" in Symfony context
- User asks which Symfony/PHP version a pattern needs
Step 0: Detect Versions
Before generating any code, know the project's Symfony, PHP, Doctrine and PHPUnit versions (the plugin's SessionStart hook prints them; otherwise read composer.lock). Then follow references/version-compatibility.md:
- Default baseline: PHP 8.2 + Symfony 6.4 APIs (runs on 6.4, 7.4 and 8.x)
- Use newer features (7.3+
DeduplicateStamp, 7.4#[ExtendsValidationFor], PHP 8.4private(set), ...) only when the detected versions allow it - Never generate APIs removed in Symfony 8 (
Request::get(), constraint option arrays, XML service config)
Core Principles
Layer Direction (STRICT)
Presentation → Application → Domain ← Infrastructure
Infrastructure depends on Domain (implements ports), but Domain NEVER depends on Infrastructure.
Module Structure
Every module follows this structure under src/:
Domain/{Module}/
├── Entity/ # Aggregates and entities (pure PHP)
├── ValueObject/ # Immutable value objects
├── Event/ # Domain events (past-tense)
├── Exception/ # Domain-specific exceptions
├── Port/ # Interfaces for external dependencies (write + read ports)
└── ReadModel/ # (optional) Plain read-side view objects returned by read ports
Application/{Module}/
├── Command/ # Write operations (final readonly) + handlers
├── Query/ # Read operations (final readonly) + handlers
├── DTO/ # Data transfer objects
└── EventHandler/ # Domain event handlers (side-effects)
Infrastructure/{Module}/
├── Persistence/ # Doctrine repositories, Mapping/*.orm.xml
├── Security/ # Voters, user providers
├── Messaging/ # Transport adapters, middleware
└── ExternalService/ # HTTP clients, third-party APIs
Presentation/{Module}/
├── API/ # REST controllers (+ Request/ DTOs)
├── CLI/ # Console commands
└── GraphQL/ # GraphQL resolvers/types
Shared code lives in a Shared module per layer (Domain/Shared/ValueObject/Money.php, Presentation/Shared/Bus/QueryBus.php, ...).
Progressive Refactoring (Existing Projects)
When working on an existing project that doesn't follow hexagonal architecture:
Step 1: Detect Current Structure
Before writing any code, check:
- Is src/ organized by layer (Domain/, Application/, etc.) or by traditional Symfony (Controller/, Entity/, Repository/)?
- Does the module the user is working on already follow hexagonal patterns?
- Are there Doctrine annotations/attributes directly on entities?
- Which Symfony / Doctrine versions are installed (affects what the refactor can use)?
Step 2: Ask the User
When the user requests a feature/change in a non-hexagonal module, ALWAYS ask:
"Bu modül (
{Module}) şu anda hexagonal yapıda değil. Şu seçeneklerden birini tercih edebilirsiniz:
- Önce refactor:
{Module}modülünü hexagonal mimariye taşıyıp, sonra yeni özelliği ekleyeyim- Sadece yeni kodu hexagonal yap: Mevcut koda dokunmadan, yeni eklenen kısımları hexagonal yapıda oluşturayım
- Mevcut yapıda devam et: Bu sefer hexagonal yapıyı atlayalım
Hangisini tercih edersiniz?"
Step 3: Incremental Migration (if user chooses refactor)
Migrate one layer at a time, in this order:
- Domain first: Extract entities to
Domain/{Module}/Entity/, create value objects, define port interfaces - Application second: Create Commands/Queries/Handlers, move business logic out of controllers/services
- Infrastructure third: Move Doctrine repositories to
Infrastructure/{Module}/Persistence/, move mapping from attributes/annotations to XML - Presentation last: Thin out controllers, add
ApiResponseTrait+#[MapRequestPayload]DTOs, apply#[IsGranted]
After each layer, verify the code still works (tests + doctrine:schema:validate) before moving to the next.
Legacy projects on Doctrine annotations (@ORM\...) cannot upgrade to ORM 3 anyway: moving the mapping to XML during the Infrastructure step unblocks that upgrade too.
Key Principles
- Never touch modules the user isn't working on - only refactor what's in scope
- One module at a time - don't try to migrate the whole project
- Keep the app working - each step should be a working state
- Respect user's pace - some teams prefer gradual migration over months
Scaffolding a New Module
When user asks to create a new module, generate ALL directories and base files:
- Create directory structure (all 4 layers)
- Create the port interfaces in
Domain/{Module}/Port/ - Create the repository adapter + XML mapping in
Infrastructure/{Module}/Persistence/ - Register the mapping directory in
doctrine.yamland bind ports to adapters inservices.yaml - Suggest initial entities, commands, and queries based on the module purpose
Key Rules
- Domain purity: No
use Symfony\...,use Doctrine\...oruse Psr\...in any Domain file, and no framework attributes (#[ORM\...],#[Assert\...],#[AsMessage]) - Port-first: Define the interface before the implementation (this includes IDs, clocks and password hashing)
- One module = one bounded context: Modules communicate via events, not direct calls
- Namespace convention:
App\Domain\{Module}\...,App\Application\{Module}\..., etc. - Enforce it in CI: deptrac + PHPStan (see
references/dependency-rules.md)
References
See references/ for detailed guides:
version-compatibility.md- Symfony / PHP / Doctrine / PHPUnit support matrix, per-feature minimum versions and fallbacksdirectory-structure.md- Full directory layout with file exampleslayer-responsibilities.md- What belongs in each layerdependency-rules.md- Import rules, deptrac and PHPStan enforcement