Modular Hexagonal DDD — ONE-SHOT APPLY (yii)
Self-contained. An AI agent should fetch this file only and execute it. Core version:
1.0.0-draftShort HTML entry:/yii· This file:/apply/yii.full.mdAI-neutral: assistant files are optional; architecture does not require any AI vendor. Host baseline researched: see Part C header (versions as of 2026-08).
0. Agent mandate
You are bootstrapping a yii application repository (not the docs site).
- Treat this document as the complete working standard for this session.
- Execute Part D in order after reading Parts A–C.
- When writing module code, obey Part B (Core) and Part C (host).
- Do not create a git commit unless the human explicitly asks.
- Rename illustrative contexts
Ordering,Warehouse,Directory,Identityto the human’s domains. - If AI vendor is
none, skip Part E.
Programmer prompt
Apply Modular Hexagonal DDD from <ORIGIN>/apply/yii.full.md
AI assistant: <cursor|claude|gemini|copilot|generic|none>
Bounded contexts: <list>
Do not commit unless I ask.
Part A — Design test
If the peer module is deleted and replaced by HTTP, does this module’s Application and Domain still compile?
Part B — Complete Core architecture
B1. Purpose
Build applications as replaceable bounded-context modules using Hexagonal Architecture (ports & adapters) + DDD. This docs site is the sole canon; consuming repos keep thin pointers, not a forked essay.
B2. Module layout (MUST)
{Module}/
Application/ # UseCases, Application DTO, Providers / composition root
Domain/ # Entities, Ports, Events, Enums, Domain DTOs
Infrastructure/ # Persistence adapters, ExternalServices (ACL), Config, listeners
UI/ # Controllers, Routes, validation, admin UI, console
Shared/ # Promoted technical ports/adapters only (after promote rule)
Host roots vary (app/Modules, src/Modules, packages). Roles do not.
B3. Strictness ladder (MUST)
| Strictness | Area | Allowed | Forbidden |
|---|---|---|---|
| Hardest | Domain | Own Domain types + Shared kernel ports | Other modules; host facades/helpers; ORM; framework traits on Domain Events |
| Strict | Application Use Cases / App DTOs | Own Domain + Shared ports | Facades, ORM, foreign Application/Domain, peer *ModuleInterface |
| Relaxed | Composition root | Container binds, routes, config merge | Business rules |
| Outer | Infrastructure & UI | Framework, ORM, HTTP, ACL adapters, admin | Leaking ORM/foreign Domain into Domain/Use Cases |
Dependency direction: Domain ← Application ← Infrastructure/UI. Ports in Domain; adapters in Infrastructure.
B4. Golden flow (MUST)
UI → Application DTO → first-level *UseCase::__invoke
→ Domain (Entity / Domain service)
→ Domain Port
→ Infrastructure adapter (ORM / ACL / HTTP)
→ back as Entity / result
→ UI presents
B5. Use Cases & DTOs (MUST)
- First-level:
Application/UseCases/{Capability}/SomethingUseCasewith single public__invoke. - Nested helpers: no
UseCasesuffix; not called from UI. - Mirror
{Capability}/for DTOs and Ports. - ACL need ports:
Domain/Ports/Acl/. Module façades:Domain/Ports/Module/. - Never create a literal
Feature/directory. - Application DTO = UI/CLI input. Domain DTO = intra-module only.
- Other modules never call Use Cases — only ACL / Events.
B6. Cross-module bridges (MUST)
Application/Domain of A never import Application/Domain of B.
| Need | Bridge |
|---|---|
| Peer answer now | Sync ACL: local {Need}PortInterface → Infra ACL adapter → thin peer {This}ModuleInterface |
| Peer reacts later | Async Domain Event → consumer Infra translation listener → inbound Use Case |
- Mapping only in ACL adapter; façades thin + HTTP-mappable; Prefer ACL validate before local commit.
- Events: pure Domain class; Shared
EventDispatcherInterface;eventId+occurredAt+schemaVersion+ rich happy-path payload; idempotent consumers; no Domain re-fetch on happy path; prefer outbox. - No cross-module write transaction. Reports: ACL reads or owned projections.
B7. Shared promote rule (MUST)
| Situation | Where |
|---|---|
| Tech used by one module | That module’s Infrastructure |
| Same tech used by ≥2 modules | Promote to Shared |
| Cross-module business concept | Never Shared — ACL / Events |
Typical Shared: DomainException, DatabaseTransactionInterface, EventDispatcherInterface, ClockInterface, ConfigReaderInterface, Ensure.
B8–B9. Persistence & UI (MUST)
- Map persistence records ↔ Domain Entities in Infrastructure; never leak ORM into Use Cases/Domain.
- UI: authorize → DTO → Use Case → present.
B10. Decision tree
Only this module? → Golden flow
Need peer now? → ACL
Peer later? → Event
Multi-step? → Orchestration (still ACL+Events)
Report across contexts? → Projection / ACL read
Tech in 2+ modules? → Shared
Business in 2+ modules? → ACL/Events — never Shared
B11. Anti-patterns
Peer imports in Use Cases; god façades; thin-ID events; Shared business enums; ORM in Domain; Entity extends ORM; literal Feature/; cross-module DB TX; ORM joins across modules for policy.
B12. Examples
Ordering, Warehouse, Directory, Identity only — rename to your domains.
B13. Quality assertions
Boundary tests must fail CI if: foreign Application/Domain imports; peer *ModuleInterface in Use Cases; ORM/facades in Domain/Use Cases; literal Feature/ folders. Also: behaviour tests, formatter, static analysis, event idempotency.
Part C — Yii host mapping
| Target (new apps) | Yii3 (yiisoft/* packages, production-ready ecosystem; PHP ≥ 8.2, through 8.5) — no single metapackage version; pin packages individually |
| Legacy | Yii 2.0.55+ still maintained for existing apps — map the same Core roles; prefer Yii3 for greenfield |
| Docs | https://yii3.yiiframework.com/ · https://yiisoft.github.io/docs/ · Yii2: https://www.yiiframework.com/doc/guide/2.0/en |
| Adapter HTML | <ORIGIN>/v1/adapters/yii |
Yii3 treats a module as a design boundary (not necessarily a base class): group Domain/Application/Infra/UI + config/common/di/*.php bindings (Yii3 modules guide).
| Core | Yii3 place |
|---|---|
| Module root | src/{Module}/ (or package) with Application/Domain/Infrastructure/UI |
| Shared | src/Shared/ |
| Composition root | config/common/di/{module}.php (+ web/console splits) — wiring only |
| Persistence | Infrastructure/Persistence/ using Cycle ORM via yiisoft/yii-cycle (recommended) — map Cycle entities ↔ Domain Entities in repositories |
| ACL adapter | Infra service bound in DI to local port interface |
| Translation listener | Queue/event consumer in Infra (e.g. Yii console worker / queue package) → inbound Use Case |
| UI | HTTP controllers / handlers under UI/ calling Use Cases |
Cycle ORM / ActiveRecord quarantine
- Cycle entities and
yiisoft/yii-cycleconfig stay in Infrastructure. - Domain ports return Domain Entities/DTOs only.
- Do not inject Cycle
ORMInterfaceinto Use Cases.
DI (Yii3)
// config/common/di/ordering.php
return [
\App\Ordering\Domain\Ports\Acl\WarehouseAvailabilityPortInterface::class
=> \App\Ordering\Infrastructure\ExternalServices\WarehouseAvailabilityAclAdapter::class,
];
Yii 2 note (brownfield)
If stuck on Yii2: put modules under custom PSR-4 roots; use Yii::$container bindings in Module init() / bootstrap — still no ActiveRecord in Domain/Use Cases; AR only in Infra repositories.
Tooling
- Psalm/PHPStan (Yii3 packages are strict); PHPUnit; composer scripts.
- Prefer package-per-module when a BC becomes reusable (
extra.config-pluginfor di/routes).
Part D — Apply procedure (Yii)
D1 — Confirm host
Greenfield: Yii3 app template with yiisoft/di + router. Brownfield Yii2: state version explicitly.
D2 — README
One-shot: <ORIGIN>/apply/yii.full.md
Host: Yii3 (yiisoft/*) or Yii2.0.x brownfield
Modules: src/{Module}/ · DI: config/common/di/
D3–D5 — ROADMAP / PRD / copy ARCHITECTURE.full.md into .ai/
D6 — PSR-4 + first module DI file
D7 — Optional Cycle schema paths per module Infra
D8 — Static analysis + boundary conventions for Part B13
D9 — Part E — no commit unless asked
Part F — Sketch
Ordering port → Warehouse module service via DI; async handler in Warehouse Infra.
Part G — Done checklist (standard)
Part E — Assistant files (optional)
Skip if AI vendor is none.
Create shared folder:
.ai/modular-hexagonal-ddd/
ARCHITECTURE.full.md # copy of the host one-shot you fetched
RULES.core.md
SKILL.md
CHECKLIST.md
yii-host.md # host-only notes from Part C
E0 — RULES.core.md
# Modular Hexagonal DDD — Core rules
Canon: <ORIGIN>/v1/core (1.0.0-draft)
One-shot: <ORIGIN>/apply/yii.full.md
Local: .ai/modular-hexagonal-ddd/ARCHITECTURE.full.md
1. Domain hardest: no host framework, ORM, other modules.
2. Use Cases: own Domain + Shared ports only.
3. Cross-module: ACL or Domain Events only.
4. Thin façades; rich events (eventId, schemaVersion); idempotent consumers.
5. No cross-module write TX; ACL validate-before-commit.
6. Shared = technical ≥2 modules only.
7. *UseCase + __invoke; no literal Feature/ folders.
8. Design test: peer→HTTP, Application/Domain still compile.
E1 — SKILL.md
---
name: modular-hexagonal-ddd
description: Enforce Modular Hexagonal DDD. Modules, ACL, Events, Shared, Use Cases.
---
Follow .ai/modular-hexagonal-ddd/ARCHITECTURE.full.md
E2 — CHECKLIST.md
- [ ] First-level *UseCase entry
- [ ] App DTO → __invoke
- [ ] I/O via Domain ports
- [ ] No ORM/facades in Application/Domain
- [ ] Sync peer → local ACL port → thin façade
- [ ] Async → rich event + translation listener → inbound UC
- [ ] Idempotent eventId
- [ ] No cross-module DB TX
- [ ] No foreign Application/Domain imports
- [ ] Shared only technical ≥2
- [ ] No Feature/ directories
- [ ] Replaceability test = yes
E3 — Cursor
.cursor/rules/modular-hexagonal-ddd.mdc← E0 + path to ARCHITECTURE.full.md.cursor/rules/yii-host.mdc← Part C summary.cursor/skills/modular-hexagonal-ddd/SKILL.md← E1
E4 — Claude
CLAUDE.md← pointer to ARCHITECTURE.full.md + E0- Shared
.ai/modular-hexagonal-ddd/*
E5 — Gemini
GEMINI.mdor.gemini/instructions.md← pointer + E0- Shared
.ai/*
E6 — GitHub Copilot
.github/copilot-instructions.md← E0 + short host section
E7 — Generic
AGENTS.mdpointing at.ai/modular-hexagonal-ddd/ARCHITECTURE.full.md
E8 — Multiple vendors
One shared .ai/modular-hexagonal-ddd/; vendor entry files only point to it.