# Laravel 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. ## Laravel-specific conventions - Check composer.json and the existing application layout. Use the project's Laravel/PHP versions; do not assume a particular bootstrap or exception-configuration file. - Short HTTP validation can use $request->validate(); use FormRequest when validation/authorization warrants a dedicated class. Use validated()/safe() for the intended fields, constrain nested array keys, and convert types explicitly. - FormRequest is HTTP-specific. CLI and Jobs can use Validator or shared validation components; do not fabricate an HTTP request to reuse a use case. - FormRequest::authorize(), policies and gates can reject HTTP calls early. Required checks for a shared operation must also protect CLI and Job execution. Reuse policies rather than copying authorization logic. - Eloquent, scopes and Query Builder are valid persistence choices. Do not add a Repository merely to wrap every model. Decide whether returning Eloquent objects is part of the intended contract. - Use explicit serialization or API Resources for HTTP output; an API Resource is not an input DTO. A framework-independent response object is optional, not a replacement required for every Resource. - A Laravel queue Job uses handle(); the container can inject execution dependencies there. Follow the repository's existing ShouldQueue/traits conventions. Do not serialize service instances into a payload. - Use transaction-aware dispatch appropriate to the installed version. afterCommit prevents early execution but is not a durable publication guarantee by itself. - Preserve the application's error format. Default validation returns 422 for requests expecting JSON and redirects with errors for ordinary HTML forms; do not assume all requests get 422. - Use the repository's actual test suite and static-analysis commands. Do not assume Pest, PHPUnit, PHPStan or a specific composer script is installed.