src/ is divided by domain, not by technical role. Each module owns its data, its rules and the plumbing that persists them, which keeps billing logic out of the AI code and vice versa.
A module
Presentation is a module of its own rather than a layer inside each one, so every HTTP concern lives in one place.
The domain layer
Entities are Doctrine-mapped classes that expose behavior rather than setters for every field:The application layer
Commands are data holders that convert scalars into value objects and name their handler:ReadPlanCommand, ListPlansCommand and CountPlansCommand are commands whose handlers return data.
The infrastructure layer
Module bootstrappers wire the module during boot: binding repository interfaces to implementations, registering navigation entries, and adding listeners. Doctrine repositories extend a shared base that is deliberately immutable: each filter returns a clone with a narrowed query, so a repository can be passed around without one caller’s filter leaking into another’s.- Soft deletes. Entities with a
deletedAtfield are filtered out automatically. - Cursor pagination. Results can be sliced relative to a known ID rather than an offset, which is what the API’s
starting_afterandending_beforeparameters use.
Layering rules
Treat this as the intended direction rather than an enforced rule: parts of the codebase reach across it. New code, including plugin code, is easier to maintain when it follows the direction.