# Yii2 application architecture Self-contained AGENTS.md template accompanying “PHP application architecture: add complexity for a reason” by Vasiliy Krivoplyas. Before adoption, merge this with the project's existing instructions, verify conventions against the repository and record its real verification commands. This file does not define project-specific paths or authorize unrelated refactoring. ## Working in this repository - Honor explicit task requirements. The architectural preferences below do not authorize ignoring requested behavior or expanding the task scope. - This is an architecture template, not a mandate to reorganize the application. Preserve existing behavior and public contracts unless the task explicitly changes them. - Read existing project instructions, README, composer.json, CI configuration and nearby code before choosing paths, dependencies or commands. Use the installed PHP/framework versions and the project's actual build and test tooling. - Follow the project's established conventions. If they conflict with a recommendation below, explain the concrete trade-off rather than silently migrating the project to another style. - Keep changes within the task. Do not rename neighboring classes, add packages or introduce a new layer just to make the repository resemble this document. ## Add complexity for a reason - Start with the smallest design that expresses the input, operation and result clearly. A small CRUD operation may use framework validation and the ORM directly. - Before adding a DTO, Value Object, Action, Repository, event or queue, identify the problem in the current code, what the new construct ensures and its maintenance cost. File length, argument count and a possible future requirement are signals to investigate, not automatic thresholds. - Introduce constructs independently. A second adapter does not require a DTO; a DTO does not require an Action; calling code from CLI does not require a queue. - Prefer a cohesive service method until a use case needs separate dependencies or changes independently from the rest of the service. An application Action is an ordinary class for one use case, not an HTTP or CLI handler. - Simplify when requirements disappear, after checking callers and behavior. Do not retain a layer solely because it already exists. ## Input validation and application input - At each entry point, validate required fields, structure, allowed keys, formats, ranges and size limits. Then normalize and convert types explicitly. Successful validation does not necessarily change a numeric string into an integer. - Pass explicit arguments or plain application input objects to services. Under this convention, keep request/form objects at the boundary; do not make a service read request data or globals. - Introduce an input DTO when related parameters form a changing composite input, nested structures need a contract, or adapters repeat meaningful assembly work. A clear short signature can stay as arguments. - A DTO describes data; its type alone does not establish authorization or domain validity. Use constructors/factories for the guarantees the DTO actually promises. Avoid copying every validation rule into multiple identical containers. - Prefer immutable input where useful and supported by the project's PHP version. Do not raise the minimum PHP version merely to use readonly or enums. ## Value Objects and business rules - Use a Value Object for a meaningful value whose valid state and operations deserve a separate type. Repeated validation may reveal that need, but repetition alone can also be solved by a shared validator. One use case can justify a Value Object. - Define its semantics first: units, precision, boundaries, normalization and equality as applicable. Enforce invariants at construction and prevent later mutation from breaking them. - Build values from external input before starting side effects. Values computed during an operation can also be constructed inside application or domain code. - Distinguish malformed input, invalid values, authorization failures, state conflicts and infrastructure failures. Map expected errors to the existing HTTP/CLI contract at the boundary. The location where an exception is thrown does not determine its HTTP status. - Do not catch every Throwable or broad exception category and convert it to a user validation error. Preserve unexpected failures for diagnosis. - Enforce operation rules for every entry point. A service may delegate to entities, policies or domain objects instead of collecting every rule in one method. ## Data access, authorization and concurrency - Start with the ORM or Query Builder. Extract repeated conditions into an appropriate scope/query object; introduce a Repository for a useful query/persistence contract, not an automatic clone of ORM methods. - Identify the actual caller and the requested resource/account separately. A validated resource ID is not permission. Apply required account and access restrictions to reads, lists and writes. - A caller ID in a Job or CLI argument does not authorize impersonation. Define the permission to act for that identity; system work may use an explicit service role. - Avoid mutable current-user/current-account state on shared services and repositories. Pass context explicitly or use a context with a lifecycle appropriate to each request/task. - Put changes that must succeed together in a transaction. A prior availability check plus a transaction is not a general race-prevention strategy. Use database constraints, conditional writes with affected-row checks, or locking as required by the operation. - Enforce uniqueness in the database and handle the conflict; a preflight query is insufficient under concurrency. ## Responses and background work - Explicitly choose public response fields. Do not leak a new database column through automatic serialization. - Separate response representation when the API must evolve independently from storage or combines sources. A simple explicit response does not need another DTO by default. - Keep CLI commands and Jobs as entry points to shared operations. Do not rely on an HTTP session for identity or request context. - Queue work when deferred completion fits the requirement. Define delivery/retry behavior, partial success, concurrency and how the caller learns the result. Capture the required invocation data and correlation ID. - For deferred authorization, explicitly choose checks at scheduling, execution or both, including what happens when access is revoked. Do not assume recorded identity freezes permissions. - Design retries around possible repeated execution. A lock alone does not make external effects exactly once. Use operation identities, database constraints or downstream idempotency mechanisms as appropriate. - Dispatch after commit when a consumer needs committed data. If losing publication is unacceptable, a transactional outbox needs both a record committed with the business change and a recoverable dispatcher; consumers must tolerate duplicates. - Introduce events for independent reactions. Keep required ordering and failure behavior visible instead of hiding a use case inside a chain of handlers. - Add caching for measured repeated cost with a defined freshness requirement. Scope keys correctly; coordinate invalidation with committed changes and consider invalidation failure and concurrent stale repopulation. ## Verification and handoff - Run the relevant checks using commands discovered in this repository. Do not invent scripts or claim a skipped check passed. - Test pure value rules separately; query semantics, account isolation, constraints and transactions against the appropriate database. Check HTTP/CLI errors at their boundaries. - For changed background work, cover relevant repeats, partial failure and concurrent delivery. A mocked queue does not prove broker delivery or worker behavior. - In the handoff, state what changed, why extra constructs were needed, what was checked and which assumptions or limitations remain. Do not claim architectural independence while returning framework models through the new boundary. ## Yii2-specific conventions - Check composer.json and the existing basic/advanced/custom application layout. Use the installed Yii2/PHP versions; do not invent common/, frontend/ or backend/ directories if the project uses another structure. - A Form based on yii\base\Model can validate operation input with rules(), load() and validate(). For simple persistence, existing ActiveRecord rules may be sufficient. - Model is not HTTP-specific: a Form can validate CLI input too. Passing it to a service would couple the contract to the validation model and its state, not necessarily to HTTP. This template keeps Forms at entry points and uses arguments/plain input DTOs for services. - Match load() to the actual payload: default formName() expects a nested key; load($data, '') accepts a flat payload. Check loading/validation as required by the contract and do not silently execute with defaults when input was not accepted. - A Search model may validate list filters and construct an ActiveDataProvider. Reuse access restrictions consistently; it is distinct from a Form for a write operation. - Use ActiveRecord/Query or a purposeful Repository. Do not bypass account restrictions when building a separate list or background query. Use existing save/validation conventions; save(false) requires an explicit justification. - An application Action is distinct from yii\base\Action, actionCreate() and a console controller action. Extract one for a use-case responsibility, not merely to match names across layers. - Use fields()/extraFields(), Serializer or an explicit response for public output. Configure the response format and status: returning an array containing errors does not itself produce HTTP 422. - Follow existing access filters/RBAC conventions for entry-point checks and keep required shared-operation authorization available to services. - With yiisoft/yii2-queue, implement the installed JobInterface: execute() receives the queue, not an injected application service. Resolve execution dependencies through the project's configured container/application integration. Store invocation data in the Job, not service instances. - Do not assume every extension/driver provides after-commit dispatch or identical retry/serialization behavior. Check the installed queue configuration and coordinate publication with the database transaction explicitly. - Use the repository's actual test commands. Do not assume Codeception, PHPUnit or a particular suite layout is installed.