style
esc
  • Loading…

For agentsLaravel Style Guide

Conventions for writing, structuring, testing and shipping Laravel 13 apps on PHP 8.5, with attribute-first models and a Livewire or Inertia frontend.

Laravel 13PHP 8.5Livewire · Inertia

The same guide as plain Markdown, built from the same source as the web edition. Rules stay machine-readable: each one starts with its stable ID and level, such as `LARAVEL-02.1` [MUST], so an agent can cite exactly the rule it means.

Rules digest

~10k tokens

246 rules, grouped by chapter, no prose or examples. Small enough to keep in an agent's context on every task.

Full guide

~18.1k tokens

Every chapter, table, code sample and drop-in config as one Markdown file. Best when the agent can read files on demand.

Pin a revision

The URLs above always serve the latest text, so an edit to the guide changes what your agent follows. Add @2026.10 after the guide name for a copy that never changes; the snippets below already do. To upgrade, read the changelog for the rules that were added, removed or changed, then bump the revision.

# Revision 2026.10: these files never change
curl -fsSL https://style.chopper.dev/laravel@2026.10/rules.md
curl -fsSL https://style.chopper.dev/laravel@2026.10/llms.txt

# What changed between revisions
curl -fsSL https://style.chopper.dev/laravel/changelog.md

Template project

A ready-to-run Laravel 13 / PHP 8.5 app on Livewire 4 and Flux with a sample invoices feature (attribute-first UUIDv7 models, actions, a DTO, a form request, a policy and a versioned API resource), the appendix configs and Composer scripts, Pest unit, feature and arch tests, and a CI workflow. Both Markdown files above describe it, so an agent asked to start a new Laravel codebase can fetch a renamed copy instead of scaffolding one by hand.

curl -fsSL "https://style.chopper.dev/laravel/template/download?name=Project" \
  -o project.zip && unzip project.zip
ParameterDefaultMeaning
nameProjectPascalCase. Becomes APP_NAME; lowercased, the Composer package and database names.

Use it with your tools

MCP server / Claude Code plugin

Connect the remote MCP server, or install the plugin, which adds the server plus a laravel-style skill that loads these rules whenever Claude writes or reviews Laravel.

claude mcp add --transport http chopper-style https://style.chopper.dev/mcp

# or the plugin: MCP server + one skill per guide
claude plugin marketplace add carson-hopper/chopper.dev
claude plugin install chopper-style@chopper-dev

Claude Code / AGENTS.md

Save the guide into the repo, then point the project instructions file (CLAUDE.md or AGENTS.md) at it.

curl -fsSL https://style.chopper.dev/laravel@2026.10/llms.txt -o docs/LARAVEL_STYLE.md

# CLAUDE.md / AGENTS.md
## Laravel style
All Laravel code follows docs/LARAVEL_STYLE.md (rev. 2026.10). Rules marked
[MUST] / [MUST NOT] are non-negotiable; [SHOULD] rules need a comment when broken.
Cite rules by ID (LARAVEL-02.1) in reviews and comments.

Cursor

A project rule that applies the digest whenever a Laravel file is in play.

mkdir -p .cursor/rules
{ printf -- '---\ndescription: Laravel Style Guide\nglobs: "**/*.{php,blade.php}"\nalwaysApply: false\n---\n\n'
  curl -fsSL https://style.chopper.dev/laravel@2026.10/rules.md; } > .cursor/rules/laravel-style.mdc

Any chat assistant

Paste this ahead of the request, or attach the digest file.

Follow the Laravel Style Guide at https://style.chopper.dev/laravel@2026.10/rules.md for all Laravel code you write or review. Treat MUST / MUST NOT as hard constraints and cite the rule's ID (such as LARAVEL-02.1) when you flag a violation.

Digest preview

# Laravel Style Guide: rules digest

> Every rule from the Laravel Style Guide (rev. 2026.10), without the prose, tables and examples. The full guide: https://style.chopper.dev/laravel/llms.txt

Revision 2026.10, plus edits not yet released. This URL always serves the latest text; pin a revision with https://style.chopper.dev/laravel@2026.10/rules.md. Changelog: https://style.chopper.dev/laravel/changelog.md.

MUST / MUST NOT are non-negotiable. SHOULD / SHOULD NOT are the default; deviate only with a short comment saying why. MAY is allowed when it helps. AVOID is allowed but a smell. Cite a rule by its ID (LARAVEL-02.1): IDs never change or get reused, and https://style.chopper.dev/laravel#LARAVEL-02.1 links to the rule. It is generic: swap `Project` for your application name and the `Invoice` examples for your own domain.

## Starter project

A ready-to-run Laravel 13 / PHP 8.5 app on Livewire 4 and Flux with a sample invoices feature (attribute-first UUIDv7 models, actions, a DTO, a form request, a policy and a versioned API resource), the appendix configs and Composer scripts, Pest unit, feature and arch tests, and a CI workflow. It is generated from this guide and follows it, so prefer it over scaffolding a new codebase by hand.

Download it as a zip, renamed to the user's names, with a GET request (query parameters are optional; missing ones take their defaults, invalid ones return HTTP 400 with a message):

```bash
curl -fsSL "https://style.chopper.dev/laravel/template/download?name=Project" -o project.zip && unzip project.zip
```

Parameters:

- `name`: Project name. PascalCase. Becomes APP_NAME; lowercased, the Composer package and database names. Must match `^(?:[A-Z][A-Za-z0-9]{1,31})$`. Default `Project`.

After unzipping:

1. **Install.** Run composer setup to install dependencies, create .env and the SQLite database, migrate with demo data and build assets.
2. **Run.** Start the app with composer dev, log in as test@example.com, then check with composer lint and composer test.

## 02. Baseline and toolchain

- `LARAVEL-02.1` **MUST** pin `"php": "^8.5"` and `"laravel/framework": "^13.0"` in `composer.json` and commit `composer.lock`.
- `LARAVEL-02.2` **MUST** use `declare(strict_types=1);` at the top of every PHP file in `app/`, `database/`, `routes/` and `tests/`.
- `LARAVEL-02.3` **MUST** keep the Laravel 11+ slim skeleton: middleware, exceptions and routing are configured in `bootstrap/app.php`, not in a `Kernel` class.
- `LARAVEL-02.4` **SHOULD** follow each Laravel upgrade guide within one minor release; Laravel 13 has minimal breaking changes, so stay current.
- `LARAVEL-02.5` **MUST NOT** add a package that duplicates a first-party feature (for example, a separate UUID package — `HasUuids` already generates UUIDv7).
- `LARAVEL-02.6` **MAY** use first-party packages freely: Horizon, Pulse, Reverb, Scout, Sanctum, Socialite, Pennant, Cashier, the AI SDK and Boost.

## 03. Project layout

- `LARAVEL-03.1` **MUST** keep one class per file, with the file named after the class.
- `LARAVEL-03.2` **MUST** mirror `app/` inside `tests/Feature` and `tests/Unit` so each class's tests are easy to find (`app/Actions/Invoices/CreateInvoice.php` → `tests/Unit/Actions/Invoices/CreateInvoiceTest.php`).
- `LARAVEL-03.3` **MUST NOT** create `Helpers`, `Utils`, `Misc` or `Common` dumping-ground folders.
- `LARAVEL-03.4` **SHOULD** split a model's relationships, scopes and accessors into traits under `app/Models/Concerns/<Model>/` once the model grows past about 150 lines (see §7).
- `LARAVEL-03.5` **SHOULD** keep `routes/web.php` short by grouping with `Route::controller()` and `Route::prefix()`; split into `routes/web/*.php` files only past about 150 lines.
- `LARAVEL-03.6` **AVOID** deep module systems (`Modules/Billing/...`) unless the team is large enough to own modules independently.

## 04. Naming

- `LARAVEL-04.1` **MUST** name methods for what they do from the caller's view: `$invoice->markPaid()`, not `$invoice->updateStatusToPaid()`.
- `LARAVEL-04.2` **MUST** name booleans so they read as a question: `$isOverdue`, `canBeSent()`.
- `LARAVEL-04.3` **MUST NOT** use abbreviations other than well-known ones (`id`, `url`, `pdf`, `api`).
- `LARAVEL-04.4` **SHOULD** name collections in the plural and single items in the singular: `$invoices`, `$invoice`.
- `LARAVEL-04.5` **AVOID** suffixing classes with their pattern when the folder already says it (`Actions\CreateInvoice`, not `CreateInvoiceAction`).

## 05. Formatting and static analysis

- `LARAVEL-05.1` **MUST** run Laravel Pint with the committed `pint.json` (Appendix A). CI runs `pint --test` and fails on any diff.
- `LARAVEL-05.2` **MUST** use **4-space** indentation in PHP (PSR-12, which Pint enforces) and **2-space** indentation in Blade, TypeScript, CSS, JSON and YAML (`.editorconfig` in Appendix A).
- `LARAVEL-05.3` **SHOULD** keep lines under **120** characters; break long method chains one call per line.
- `LARAVEL-05.4` **MUST** use short array syntax, trailing commas in multi-line arrays, arguments and parameters, and single quotes unless interpolating.
- `LARAVEL-05.5` **MUST** order class members: constants, properties, constructor, public methods, protected methods, private methods. Attributes sit directly above the element they configure, one attribute per line.
- `LARAVEL-05.6` **MUST** import every class with `use`; no fully qualified names inline.
- `LARAVEL-05.7` **MUST** run Larastan at **level 8** or higher (Appendix B). New code adds no baseline entries; the baseline only shrinks.
- `LARAVEL-05.8` **MUST** give every parameter, property and return a native type. Use PHPDoc only for what PHP cannot express: generics (`Collection<int, Invoice>`), array shapes (`array{id: string, total: int}`) and `@property-read` for model columns.
- `LARAVEL-05.9` **MUST NOT** use `@phpstan-ignore` without a trailing reason: `// @phpstan-ignore argument.type (vendor stub is wrong)`.
- `LARAVEL-05.10` **SHOULD** run Rector with the Laravel and PHP 8.5 sets in dry-run mode in CI; apply its changes in separate refactor commits.
- `LARAVEL-05.11` **MUST** expose `composer lint` (Pint, Larastan, Rector dry run) and `composer test` (Pest in parallel). Both run in CI on every pull request (Appendix D).

## 06. Modern PHP usage

- `LARAVEL-06.1` **MUST** declare classes `final` by default. Remove `final` only when a class is designed for extension (models extended by a package, abstract bases).
- `LARAVEL-06.2` **MUST** use constructor property promotion and `readonly` for injected dependencies and DTOs; a DTO is a `final readonly class`.
- `LARAVEL-06.3` **SHOULD** use PHP 8.4 asymmetric visibility (`public private(set)`) instead of a getter for a value that is read publicly but written only internally.
- `LARAVEL-06.4` **MAY** use PHP 8.4 property hooks on plain objects; [MUST NOT] use them on Eloquent models (they bypass Eloquent's attribute system — use casts and accessors there).
- `LARAVEL-06.5` **SHOULD** chain on `new` without parentheses (PHP 8.4): `new InvoiceNumber($raw)->formatted()`.
- `LARAVEL-06.6` **MUST** use backed enums for every fixed set of values (statuses, types, roles) and cast model columns to them. Put labels and colours on the enum as methods.
- `LARAVEL-06.7` **MUST** use `match` instead of `switch`; let an unhandled case throw `UnhandledMatchError` rather than adding a silent default.
- `LARAVEL-06.8` **MUST** use strict comparison (`===`, `in_array(..., strict: true)`).
- `LARAVEL-06.9` **SHOULD** use named arguments when a call has a boolean or more than three positional arguments: `Number::currency($total, in: 'EUR')`.
- `LARAVEL-06.10` **MUST** store money as integer minor units (cents) or with `brick/money`; [MUST NOT] use floats for money.
- `LARAVEL-06.11` **SHOULD** use `CarbonImmutable` (set with `Date::use(CarbonImmutable::class)` in a service provider).
- `LARAVEL-06.12` **SHOULD** return early instead of nesting `if` / `else`.
- `LARAVEL-06.13` **SHOULD** use first-class callable syntax: `array_map($this->format(...), $lines)`.
- `LARAVEL-06.14` **MUST NOT** use `compact()`, `extract()`, variable variables or `@` error suppression.
- `LARAVEL-06.15` **AVOID** static helper classes; prefer an injected service or a method on the model or value object.
- `LARAVEL-06.16` **SHOULD** use Laravel helpers (`str()`, `Str`, `Arr`, `Number`, `collect()`) over raw PHP string and array functions when they read more clearly.
- `LARAVEL-06.17` **SHOULD** use the pipe operator `|>` for short linear transformations of one value: `$slug = $title |> trim(...) |> Str::slug(...);`. [AVOID] pipes longer than four steps or with side effects; use a collection or a named method instead.
- `LARAVEL-06.18` **SHOULD** use `clone($dto, ['dueAt' => $newDate])` (clone with) to derive a modified copy of a readonly DTO or value object instead of writing `with*()` methods by hand.
- `LARAVEL-06.19` **SHOULD** mark methods whose return value must not be ignored (results, new immutable instances) with `#[\NoDiscard]`.
- `LARAVEL-06.20` **SHOULD** use `array_first()` / `array_last()` over `reset()` / `end()` or `$array[array_key_first($array)]`.
- `LARAVEL-06.21` **MAY** use attributes on constants and `#[\Override]` on properties; [MUST] use `#[\Override]` on every method that overrides a parent or implements an interface method outside the framework's magic hooks.

## 07. Eloquent models: attributes for everything

- `LARAVEL-07.1` **MUST** configure every model through attributes wherever one exists. [MUST NOT] declare `$table`, `$fillable`, `$guarded`, `$hidden`, `$visible`, `$appends`, `$touches`, `$connection`, `$primaryKey`, `$keyType`, `$incrementing` or `$timestamps` properties.
- `LARAVEL-07.2` **MUST** list mass-assignable columns with `#[Fillable]`. [MUST NOT] use `#[Unguarded]` or an empty `#[Guarded([])]`.
- `LARAVEL-07.3` **MUST** declare `#[Table]` explicitly even when it matches the convention, so the table name is greppable.
- `LARAVEL-07.4` **MUST** wire the factory, policy and observers with `#[UseFactory]`, `#[UsePolicy]` and `#[ObservedBy]` rather than relying on name guessing or provider registration.
- `LARAVEL-07.5` **MUST** hide secrets and internal columns with `#[Hidden]`.
- `LARAVEL-07.6` **MUST** define local scopes with `#[Scope]` on a `protected` method that takes `Builder $query` and returns `void`. [MUST NOT] use the old `scopeX` prefix.
- `LARAVEL-07.7` **MUST** write casts in the `casts()` method and cast every enum, date, boolean, JSON and encrypted column.
- `LARAVEL-07.8` **SHOULD** order attributes: table and connection, mass assignment, serialization, relations (`Touches`), scopes, wiring (`ObservedBy`, `CollectedBy`, `UseFactory`, `UseEloquentBuilder`, `UsePolicy`, `UseResource`).
- `LARAVEL-07.9` **MUST** declare models `final`.
- `LARAVEL-07.10` **MUST** give every model a UUID primary key through the `HasUuids` trait. In Laravel 13, `HasUuids` generates **ordered UUIDv7** values, which index well and sort by creation time. No custom `newUniqueId()` is needed.
- `LARAVEL-07.11` **MUST** create the column with `$table->uuid('id')->primary()` and foreign keys with `foreignUuid()` (§8).
- `LARAVEL-07.12` **MUST NOT** expose integer IDs anywhere; route model binding uses the UUID.
- `LARAVEL-07.13` **MAY** override `uniqueIds()` to also generate a UUID for another column, such as a public `reference`.
- `LARAVEL-07.14` **MUST** move relationships into a `Has<Model>Relationships` trait for every model with more than two relationships.
- `LARAVEL-07.15` **MUST** give every relationship method a return type with generics in the PHPDoc: `/** @return BelongsTo<Customer, $this> */`.
- `LARAVEL-07.16` **SHOULD** mark trait boot logic with `#[Boot]` / `#[Initialize]` instead of relying on `bootTraitName()` naming.
- `LARAVEL-07.17` **MUST NOT** put queries that belong to other models, HTTP concerns or notifications in a model trait.

## 08. Migrations, factories and seeders

- `LARAVEL-08.1` **MUST** use anonymous migration classes (`return new class extends Migration`).
- `LARAVEL-08.2` **MUST** use `$table->uuid('id')->primary()` for primary keys and `$table->foreignUuid('customer_id')->constrained()` for foreign keys.
- `LARAVEL-08.3` **MUST** declare an explicit `onDelete` behaviour on every foreign key (`cascadeOnDelete()`, `restrictOnDelete()` or `nullOnDelete()`). Pick on purpose; there is no default answer.
- `LARAVEL-08.4` **MUST** index every column used in a `where`, `orderBy` or join that runs on a hot path, and add composite indexes in the order the query filters.
- `LARAVEL-08.5` **MUST NOT** edit a migration that has run in any shared environment. Write a new one.
- `LARAVEL-08.6` **SHOULD** write a working `down()` method; when a change cannot be reversed safely, throw an exception in `down()` that says so.
- `LARAVEL-08.7` **MUST NOT** use models in migrations. Use the query builder for data fixes, or move the fix into a one-off command.
- `LARAVEL-08.8` **SHOULD** split schema changes and data backfills into separate migrations, and run large backfills as queued jobs.
- `LARAVEL-08.9` **SHOULD** store money as `unsignedBigInteger('total_cents')` with a `char('currency', 3)` column, and enums as `string` columns (not native database enums).
- `LARAVEL-08.10` **SHOULD** use `$table->timestampsTz()` / `softDeletesTz()` on PostgreSQL.
- `LARAVEL-08.11` **MUST** have a factory for every model, bound to it with `#[UseModel(Invoice::class)]` (`Illuminate\Database\Eloquent\Factories\Attributes`) instead of a `$model` property.
- `LARAVEL-08.12` **MUST** produce a valid, saveable model from `definition()` with no extra state.
- `LARAVEL-08.13` **SHOULD** express variations as named states: `->paid()`, `->overdue()`, `->forCustomer($customer)`.
- `LARAVEL-08.14` **MUST NOT** hit external services or the filesystem from a factory.
- `LARAVEL-08.15` **MUST** keep `DatabaseSeeder` runnable on an empty database to give a usable local environment in one command (`php artisan migrate:fresh --seed`).
- `LARAVEL-08.16` **MUST NOT** seed production with demo data; production reference data goes in a migration or a dedicated, idempotent seeder.

## 09. Queries and performance

- `LARAVEL-09.1` **MUST** enable strict mode outside production in `AppServiceProvider::boot()`: `Model::shouldBeStrict(! app()->isProduction())`. This throws on lazy loading, on silently discarded fillable attributes and on missing attributes.
- `LARAVEL-09.2` **MUST** log lazy-loading violations in production instead of throwing: `Model::handleLazyLoadingViolationUsing(...)`.
- `LARAVEL-09.3` **MUST** eager load every relation a view, resource or Inertia page uses: `Invoice::with(['customer', 'lines'])`.
- `LARAVEL-09.4` **SHOULD** use `withCount()`, `withSum()` and `withExists()` instead of loading a relation just to count or sum it.
- `LARAVEL-09.5` **MUST** select only the columns you need on large tables and in exports.
- `LARAVEL-09.6` **MUST** use `chunkById()`, `lazyById()` or `cursor()` to iterate over more than about 1,000 rows. [MUST NOT] call `->get()` or `->all()` on an unbounded table.
- `LARAVEL-09.7` **MUST** paginate every list endpoint and screen; prefer `cursorPaginate()` for infinite scroll and APIs.
- `LARAVEL-09.8` **SHOULD** move reusable query logic into `#[Scope]` methods or a custom builder bound with `#[UseEloquentBuilder]`. A query used in more than one place with more than three clauses becomes a scope, builder method or `app/Queries` class.
- `LARAVEL-09.9` **MUST NOT** build SQL by concatenating input. Use bindings; `DB::raw()` and `whereRaw()` take only constants or bound values.
- `LARAVEL-09.10` **MUST** wrap multi-write operations in `DB::transaction()`, and dispatch side effects with `->afterCommit()`.
- `LARAVEL-09.11` **SHOULD** use `upsert()` or `insert()` for bulk writes instead of saving models in a loop (remember that bulk writes skip model events).
- `LARAVEL-09.12` **MAY** use `whereVectorSimilarTo()` (Laravel 13, PostgreSQL + pgvector) for semantic search; keep embeddings in their own column and index.
- `LARAVEL-09.13` **SHOULD** cache expensive reads with `Cache::flexible()` (stale-while-revalidate) and tagged keys; use `Cache::touch()` to extend a TTL without re-reading the value.
- `LARAVEL-09.14` **MUST** namespace cache keys by model and ID: `invoice:{id}:totals`.
- `LARAVEL-09.15` **MUST** clear or update caches from the observer or action that changes the data, never from a controller.

## 10. Routing and controllers

- `LARAVEL-10.1` **MUST** name every route, and generate URLs with `route()` (or Wayfinder / Ziggy on the Inertia stack). [MUST NOT] hard-code paths.
- `LARAVEL-10.2` **MUST** use resource routes (`Route::resource()`, `Route::apiResource()`) with `->only()` for CRUD, and invokable controllers for single actions.
- `LARAVEL-10.3` **MUST** use route model binding with the UUID key; scope nested bindings with `->scopeBindings()`.
- `LARAVEL-10.4` **MUST NOT** put closures with logic in route files; closures may only return a view.
- `LARAVEL-10.5` **SHOULD** rate-limit authentication, password reset and public API routes with named limiters defined in `AppServiceProvider`.
- `LARAVEL-10.6` **MUST** declare controller middleware with `#[Middleware]` on the class or method, using `only:` / `except:` where needed. [MUST NOT] implement `HasMiddleware` or call `$this->middleware()`.
- `LARAVEL-10.7` **SHOULD** declare per-action authorization with `#[Authorize]` instead of a `Gate::authorize()` call at the top of the method.
- `LARAVEL-10.8` **MUST** keep controllers thin: validate (form request), authorize, call one action, return a response. Target fewer than 15 lines per method.
- `LARAVEL-10.9` **MUST NOT** query or write models directly in a controller beyond a simple read for `index` / `show`.
- `LARAVEL-10.10` **MUST** return typed responses: `View`, `Response` (Inertia), `RedirectResponse`, `JsonResource`.
- `LARAVEL-10.11` **MUST** keep the same rule on both stacks: the entry point (component or controller) validates, authorizes and delegates to an action.

## 11. Validation and form requests

- `LARAVEL-11.1` **MUST** validate every write in a form request (controllers) or a Livewire form object (Livewire). [MUST NOT] call `$request->validate()` inline in a controller.
- `LARAVEL-11.2` **MUST** configure form-request behaviour with attributes from `Illuminate\Foundation\Http\Attributes`: `#[StopOnFirstFailure]`, `#[ErrorBag('invoice')]`, `#[RedirectTo('/invoices')]` or `#[RedirectToRoute('invoices.index')]`. [MUST NOT] set the matching `$stopOnFirstFailure`, `$errorBag`, `$redirect` or `$redirectRoute` properties.
- `LARAVEL-11.3` **MUST** write rules as arrays, not pipe strings, and use rule objects (`Rule::enum()`, `Rule::unique()`, `Password::defaults()`, `File::types()`).
- `LARAVEL-11.4` **MUST** read validated data only: `$request->validated()`, `$request->safe()`, or a typed DTO built from them. [MUST NOT] pass `$request->all()` to a model.
- `LARAVEL-11.5` **SHOULD** expose a `toData()` method that turns validated input into a readonly DTO for the action.
- `LARAVEL-11.6` **SHOULD** put authorization in the policy and leave `authorize()` out of the form request (it defaults to `true` when absent), so authorization lives in one place.
- `LARAVEL-11.7` **SHOULD** write custom rules as invokable rule classes in `app/Rules`; [AVOID] long closure rules.
- `LARAVEL-11.8` **MUST** translate messages through `lang/` files; no hard-coded English in `messages()`.
- `LARAVEL-11.9` **MUST** **Livewire:** validate with a form object (`Livewire\Form`) using `#[Validate]` attributes on its properties, or a `rules()` method for dynamic rules. Call `$this->form->validate()` before handing data to an action.
- `LARAVEL-11.10` **MUST** **Inertia:** rely on the form request; validation errors arrive in `useForm().errors` (or the `<Form>` component's `errors`) automatically. [MUST NOT] duplicate server rules as the only check on the client; client-side checks are for convenience only.

## 12. Authorization

- `LARAVEL-12.1` **MUST** authorize every action that reads or changes a model through a **policy**, bound to the model with `#[UsePolicy]`.
- `LARAVEL-12.2` **MUST** authorize at the entry point: `#[Authorize]` on controller methods, `$this->authorize()` in Livewire actions, and `Gate::authorize()` in jobs or commands that act for a user.
- `LARAVEL-12.3` **MUST** re-authorize in every Livewire action method. A component's `mount()` check does not cover later requests to the same component.
- `LARAVEL-12.4` **MUST** return `Response::denyAsNotFound()` from a policy when the user should not learn that the record exists (for example, another tenant's invoice).
- `LARAVEL-12.5` **SHOULD** use `before()` in a policy only for true super-admins, and log its use.
- `LARAVEL-12.6` **MUST NOT** check roles inline (`if ($user->role === 'admin')`) outside policies and gates.
- `LARAVEL-12.7` **SHOULD** pass abilities to the frontend instead of re-deriving rules there: `'can' => ['update' => $user->can('update', $invoice)]` in an Inertia prop or resource, and `@can` / `$user->can()` in Blade and Livewire.
- `LARAVEL-12.8` **MAY** inject the current user with the `#[CurrentUser]` container attribute instead of calling `auth()->user()`.

## 13. Actions, services and DTOs

- `LARAVEL-13.1` **MUST** give each action one public method, `handle()`, with typed parameters and a typed return.
- `LARAVEL-13.2` **MUST** inject dependencies through the constructor; resolve actions from the container (method injection or `app()`), never with `new`.
- `LARAVEL-13.3` **MUST** wrap multi-step writes in `DB::transaction()` inside the action.
- `LARAVEL-13.4` **SHOULD** compose actions by injecting one into another, not by inheritance.
- `LARAVEL-13.5` **MUST NOT** read the request, session or `auth()` inside an action; pass the user and data in.
- `LARAVEL-13.6` **MUST** pass structured data between layers as `final readonly class` DTOs in `app/Data`, not as arrays.
- `LARAVEL-13.7` **SHOULD** give each DTO a named constructor (`fromArray()`, `fromRequest()`) and keep validation out of it (the form request already did that).
- `LARAVEL-13.8` **MAY** use `spatie/laravel-data` when DTOs also need to serialize to TypeScript for the Inertia stack; keep the same naming.
- `LARAVEL-13.9` **SHOULD** use a service class only for an integration with state or configuration (a payment gateway client, a PDF renderer). Business rules still go in actions.
- `LARAVEL-13.10` **MUST** bind interfaces to implementations with class attributes — `#[Bind(StripeGateway::class)]` on the interface, `#[Singleton]` or `#[Scoped]` on the class — instead of registering them in a service provider, when the binding is static.
- `LARAVEL-13.11` **SHOULD** inject configuration and framework services with parameter attributes from `Illuminate\Container\Attributes`: `#[Config('billing.currency')] string $currency`, `#[Storage('invoices')] Filesystem $disk`, `#[Log('billing')] LoggerInterface $log`, `#[CurrentUser] User $user`, `#[Tag('exporters')] iterable $exporters`.
- `LARAVEL-13.12` **MUST NOT** call `config()`, `Storage::disk()` or `auth()` inside a constructor when a container attribute can inject the value.

## 14. Frontend: Livewire or Inertia

- `LARAVEL-14.1` **MUST** keep UI components **small**: one job per component, roughly under 150 lines of template. Split a screen into a page plus focused child components (`InvoiceTable`, `InvoiceFilters`, `InvoiceStatusBadge`).
- `LARAVEL-14.2` **MUST** keep business logic out of the view layer: components call actions; they never write models directly with complex rules.
- `LARAVEL-14.3` **MUST** localise every user-facing string (`__()` in PHP/Blade, a translation helper in React).
- `LARAVEL-14.4` **MUST** meet WCAG 2.2 AA: labelled inputs, visible focus, keyboard-reachable actions, colour contrast.
- `LARAVEL-14.5` **SHOULD** share layouts: one app layout, one guest layout.
- `LARAVEL-14.6` **SHOULD** write components as **single-file components** (`resources/views/components/⚡invoice-table.blade.php`), the Livewire 4 default. Switch to a **multi-file component** (`--mfc`) once the PHP part passes about 100 lines, and keep the colocated `*.test.php` file there.
- `LARAVEL-14.7` **MUST** put full-page components in the `pages::` namespace and route them with `Route::livewire()`.
- `LARAVEL-14.8` **MUST** use **Flux UI** components (`<flux:input>`, `<flux:button>`, `<flux:table>`, `<flux:modal>`) before writing custom markup; wrap repeated Flux combinations in an anonymous Blade component.
- `LARAVEL-14.9` **SHOULD** use plain Blade components (no Livewire) for anything without server state: badges, cards, empty states.
- `LARAVEL-14.10` **MUST** treat every public property as user input: anyone can change it from the browser. Mark IDs and anything the user must not change with `#[Locked]`.
- `LARAVEL-14.11` **MUST** store models in properties (Livewire locks the model ID) or as `#[Locked]` UUIDs, never a raw editable ID.
- `LARAVEL-14.12` **MUST** authorize inside every action method (`$this->authorize('update', $this->invoice)`), not only in `mount()`.
- `LARAVEL-14.13` **MUST** move form fields into a `Livewire\Form` object with `#[Validate]` attributes when a form has more than three fields.
- `LARAVEL-14.14` **SHOULD** use `#[Computed]` for derived data (queries, totals) instead of storing it in public properties; this keeps the payload small and the data fresh.
- `LARAVEL-14.15` **SHOULD** use `#[Url]` for filters, search and sort so screens are linkable.
- `LARAVEL-14.16` **SHOULD** listen for events with `#[On('invoice-paid')]`; [AVOID] long chains of component-to-component events — lift the state up or use a shared parent.
- `LARAVEL-14.17` **MUST** default to `wire:model` (sync on next request); use `wire:model.live.debounce.300ms` only where instant feedback matters (search).
- `LARAVEL-14.18` **SHOULD** wrap independently refreshing parts of a page in `@island` blocks (with `lazy: true` for slow widgets) instead of splitting into many nested components.
- `LARAVEL-14.19` **SHOULD** mark slow components lazy (`#[Lazy]` or `lazy` on the tag) and provide an `@placeholder`.
- `LARAVEL-14.20` **SHOULD** use the automatic `data-loading` attribute with Tailwind variants for loading states instead of hand-written `wire:loading` blocks.
- `LARAVEL-14.21` **MAY** use `wire:sort` for drag-and-drop ordering and `wire:transition` for page transitions.
- `LARAVEL-14.22` **MUST** write one page per file under `resources/js/pages`, matching the `Inertia::render()` name (`invoices/show` → `pages/invoices/show.tsx`).
- `LARAVEL-14.23` **MUST** keep pages thin: they receive props and compose small components from `resources/js/components`.
- `LARAVEL-14.24` **MUST** use TypeScript in strict mode; [MUST NOT] use `any`. Type page props with an interface per page.
- `LARAVEL-14.25` **SHOULD** generate route helpers and TypeScript types from PHP (Wayfinder for routes and actions; `spatie/laravel-typescript-transformer` or laravel-data for DTOs and enums) instead of hand-writing them.
- `LARAVEL-14.26` **SHOULD** use persistent layouts and `useLayoutProps` / `setLayoutProps` (v3) for layout state such as page titles and breadcrumbs, instead of context or event buses.
- `LARAVEL-14.27` **MUST** build props from API resources (`InvoiceResource::make($invoice)`), never pass raw models — everything in a prop is visible in the page source.
- `LARAVEL-14.28` **MUST** keep shared data in `HandleInertiaRequests::share()` minimal (auth user summary, flash, abilities) and lazy (`fn () => ...`).
- `LARAVEL-14.29` **SHOULD** use `Inertia::defer()` for slow, non-critical props and `Inertia::optional()` for props loaded only on partial reloads; use `Inertia::merge()` / `Inertia::scroll()` for infinite lists.
- `LARAVEL-14.30` **SHOULD** use partial reloads (`router.reload({ only: ['invoices'] })`) instead of full visits when only one prop changes.
- `LARAVEL-14.31` **MUST** submit forms with the `<Form>` component or `useForm`, and show `errors` from the server next to each field.
- `LARAVEL-14.32` **SHOULD** use v3 **optimistic updates** (`form.optimistic(...)`, `router.optimistic(...)`) for quick toggles and list changes; Inertia rolls them back on a non-2xx response.
- `LARAVEL-14.33` **MUST** use `useHttp` (v3's built-in client) for JSON endpoints that do not return an Inertia response. [MUST NOT] add Axios unless you need its interceptors.
- `LARAVEL-14.34` **MUST** redirect after a successful `POST` / `PUT` / `DELETE`; [MUST NOT] return `Inertia::render()` from a write route.
- `LARAVEL-14.35` **MUST** install the `@inertiajs/vite` plugin and call `createInertiaApp()` with `strictMode: true`.
- `LARAVEL-14.36` **SHOULD** enable SSR for public, indexable pages (`vite build --ssr` + `php artisan inertia:start-ssr`); authenticated app screens [MAY] skip it.

## 15. APIs and resources

- `LARAVEL-15.1` **MUST** return every API response (and every Inertia prop built from a model) through an API resource. [MUST NOT] return a model or `toArray()` directly.
- `LARAVEL-15.2` **MUST** configure resources with attributes from `Illuminate\Http\Resources\Attributes`: `#[Collects(InvoiceResource::class)]` on a resource collection, `#[PreserveKeys]` when keys matter. [MUST NOT] set the `$collects` or `$preserveKeys` properties.
- `LARAVEL-15.3` **SHOULD** use Laravel 13's first-party **JSON:API resources** for public or partner APIs, so filtering, sparse fieldsets, includes and links follow the spec without custom code. Internal APIs [MAY] use plain resources.
- `LARAVEL-15.4` **MUST** use `whenLoaded()`, `whenCounted()` and `whenAggregated()` so a resource never triggers a query.
- `LARAVEL-15.5` **MUST** format money, dates and enums consistently: money as integer minor units plus currency, dates as ISO 8601 in UTC, enums as their backing value.
- `LARAVEL-15.6` **MUST** version public APIs in the URL (`/api/v1/...`) and keep a version stable once published.
- `LARAVEL-15.7` **MUST** authenticate APIs with Sanctum (first-party SPA cookies or tokens with abilities) and rate-limit every route.
- `LARAVEL-15.8` **MUST** return errors in one shape (`message` + `errors`, or JSON:API `errors`) with correct status codes: 401, 403, 404, 409, 422, 429.
- `LARAVEL-15.9` **SHOULD** document public APIs with an OpenAPI spec generated from code (for example Scramble) and check it in CI.

## 16. Jobs, events, queues and commands

- `LARAVEL-16.1` **MUST** configure retries, timeouts, backoff, queue and connection with these attributes. [MUST NOT] declare the matching properties.
- `LARAVEL-16.2` **MUST** set `#[Tries]` and `#[Timeout]` on every job, and keep the timeout below the queue's `retry_after`.
- `LARAVEL-16.3` **MUST** make jobs **idempotent**: running twice must not charge twice or send two emails. Use `ShouldBeUnique`, a guard on the model state, or an idempotency key.
- `LARAVEL-16.4` **MUST** pass models or IDs, never large arrays or closures; add `#[WithoutRelations]` so queued payloads stay small.
- `LARAVEL-16.5` **MUST** implement `failed()` for jobs whose failure a person must hear about.
- `LARAVEL-16.6` **SHOULD** use `Queue::route()` (Laravel 13) in a service provider to route job classes to queues centrally when many jobs share a queue; use `#[Queue]` for one-off placement.
- `LARAVEL-16.7` **SHOULD** dispatch after commit (`->afterCommit()` or `after_commit` in config) when the job reads data written in the same transaction.
- `LARAVEL-16.8` **SHOULD** use `Bus::batch()` for fan-out work with progress, and `Bus::chain()` for ordered steps.
- `LARAVEL-16.9` **MUST** name events in the past tense and keep them as plain data carriers (public readonly properties).
- `LARAVEL-16.10` **SHOULD** make listeners that do I/O queued (`ShouldQueue`), and rely on event discovery instead of registering listeners by hand.
- `LARAVEL-16.11` **AVOID** using model events (`creating`, `saved`) for business workflows. Use them in observers for model housekeeping only (setting a number, clearing a cache); dispatch domain events from actions.
- `LARAVEL-16.12` **SHOULD** broadcast real-time updates with Reverb; on Livewire listen with `#[On('echo:...')]`, on Inertia with `useEcho` from `@laravel/echo-react`.
- `LARAVEL-16.13` **MUST** declare commands with attributes from `Illuminate\Console\Attributes`: `#[Signature('invoices:remind {--days=3}')]`, `#[Description('...')]`, and `#[Help]`, `#[Usage]` or `#[Hidden]` as needed. [MUST NOT] set `$signature` or `$description`.
- `LARAVEL-16.14` **MUST** keep commands thin: parse input, call an action, report the result. Return `self::SUCCESS` / `self::FAILURE`.
- `LARAVEL-16.15` **MUST** schedule tasks in `routes/console.php` with `->withoutOverlapping()` and `->onOneServer()` for anything that must not run twice.

## 17. Configuration and environment

- `LARAVEL-17.1` **MUST** call `env()` only inside `config/*.php`. Everywhere else read `config()` or inject with `#[Config('billing.currency')]`; `env()` returns `null` once config is cached.
- `LARAVEL-17.2` **MUST** keep `.env.example` complete and current, with safe placeholder values and a comment for anything unusual.
- `LARAVEL-17.3` **MUST NOT** commit `.env` or any secret. Production secrets live in the host's secret store or an encrypted `.env.production.encrypted` (`php artisan env:encrypt`).
- `LARAVEL-17.4` **SHOULD** group app-specific settings in their own files (`config/billing.php`) rather than adding keys to `config/app.php`.
- `LARAVEL-17.5` **MUST** run `php artisan optimize` on deploy (config, routes, views, events cached).
- `LARAVEL-17.6` **SHOULD** use Pennant feature flags for gradual rollouts instead of config booleans that need a deploy.
- `LARAVEL-17.7` **MUST** keep timezone `UTC` in config and the database; convert to the user's timezone only for display.

## 18. Errors, logging and observability

- `LARAVEL-18.1` **MUST** throw domain exceptions (`InvoiceAlreadyPaid extends DomainException`) for broken business rules, and give each a clear message.
- `LARAVEL-18.2` **SHOULD** let an exception render itself (`render()` returning a response) or register its rendering in `bootstrap/app.php` with `->withExceptions()`. [MUST NOT] catch an exception only to rethrow or ignore it.
- `LARAVEL-18.3` **MUST** catch exceptions only where you can recover or add context; otherwise let them reach the handler.
- `LARAVEL-18.4` **MUST NOT** leak stack traces or SQL to users: `APP_DEBUG=false` everywhere except local.
- `LARAVEL-18.5` **SHOULD** throttle noisy exceptions with `->throttle()` in `withExceptions()` and stop reporting expected ones with `->dontReport()`.
- `LARAVEL-18.6` **MUST** log with context arrays, not interpolated strings: `Log::info('Invoice sent', ['invoice_id' => $invoice->id])`.
- `LARAVEL-18.7` **SHOULD** add request-wide context with the `Context` facade (request ID, user ID, tenant); it flows into logs and queued jobs automatically.
- `LARAVEL-18.8` **MUST NOT** log secrets, tokens, passwords, full card data or personal data beyond IDs.
- `LARAVEL-18.9` **SHOULD** use dedicated channels for noisy areas (`#[Log('billing')]`) and JSON formatting in production.
- `LARAVEL-18.10` **SHOULD** run Pulse in production for slow queries, slow jobs, exceptions and usage, and Horizon for queue health.
- `LARAVEL-18.11` **SHOULD** send exceptions to an error tracker (Nightwatch, Sentry or Flare) with release and environment tags.
- `LARAVEL-18.12` **MUST** expose the built-in `/up` health route and monitor it.

## 19. Security

- `LARAVEL-19.1` **MUST** keep Laravel 13's `PreventRequestForgery` middleware (origin-aware CSRF protection) enabled for all web routes; exclude a route only for signed webhooks, and verify the signature.
- `LARAVEL-19.2` **MUST** mass-assign only through `#[Fillable]` lists and validated data (§7, §11).
- `LARAVEL-19.3` **MUST** escape output: `{{ }}` in Blade. `{!! !!}` is allowed only for HTML sanitised on the server, with a comment saying where. In React, [MUST NOT] use `dangerouslySetInnerHTML` without the same sanitising.
- `LARAVEL-19.4` **MUST** authorize every route, Livewire action and job that acts for a user (§12), and scope queries to the current tenant with a global scope (`#[ScopedBy]`).
- `LARAVEL-19.5` **MUST** use signed URLs (`URL::temporarySignedRoute()`) for links in emails and downloads.
- `LARAVEL-19.6` **MUST** store uploads on a private disk and serve them through an authorized route or temporary URL; validate type and size with `File::types()->max()`.
- `LARAVEL-19.7` **MUST** hash passwords with the default hasher and enforce `Password::defaults()` set in a provider (min length, mixed case, uncompromised in production).
- `LARAVEL-19.8` **MUST** cast secrets and personal data columns as `encrypted` where the app does not need to query them.
- `LARAVEL-19.9` **MUST** set security headers (CSP, HSTS, `X-Frame-Options`, `Referrer-Policy`) through middleware.
- `LARAVEL-19.10` **MUST** run `composer audit` and `npm audit` in CI and fix high or critical issues before release.
- `LARAVEL-19.11` **MUST NOT** expose Telescope, Horizon, Pulse or debug tools in production without an authorization gate.

## 20. Testing with Pest

- `LARAVEL-20.1` **MUST** write tests with Pest, using `it()` / `test()` descriptions that read as sentences.
- `LARAVEL-20.2` **MUST** cover every action, policy and form request; a bug fix starts with a failing test.
- `LARAVEL-20.3` **MUST** use `RefreshDatabase` (or `LazilyRefreshDatabase`) and factories. [MUST NOT] depend on seeded data or on test order.
- `LARAVEL-20.4` **MUST** configure test classes and hooks with attributes from `Illuminate\Foundation\Testing\Attributes`, such as `#[Seed]` / `#[Seeder(BillingSeeder::class)]` instead of `$seed` / `$seeder` properties, and `#[SetUp]` / `#[TearDown]` on hook methods in reusable testing traits.
- `LARAVEL-20.5` **MUST** fake the outside world: `Http::fake()` with `Http::preventStrayRequests()`, `Queue::fake()`, `Mail::fake()`, `Notification::fake()`, `Storage::fake()`, `Event::fake()` (only the events you assert).
- `LARAVEL-20.6` **SHOULD** freeze time with `$this->travelTo()` / `freezeTime()` when dates matter.
- `LARAVEL-20.7` **MUST** run tests in parallel in CI (`pest --parallel`) and keep the suite under five minutes.
- `LARAVEL-20.8` **SHOULD** add architecture tests with Pest's `arch()` to enforce this guide: strict types, final classes, no `env()` outside config, no debugging functions.
- `LARAVEL-20.9` **SHOULD** keep coverage of `app/Actions` and `app/Policies` above 90% and the whole app above 80%.

## 21. Git, commits and code review

- `LARAVEL-21.1` **MUST** follow **Conventional Commits**: `type(scope): summary` in the imperative, lower case, no trailing period, under 72 characters.
- `LARAVEL-21.2` **MUST** mark breaking changes with `!` and a `BREAKING CHANGE:` footer.
- `LARAVEL-21.3` **SHOULD** explain the *why* in the body when the change is not obvious from the summary.
- `LARAVEL-21.4` **MUST** keep each commit building and passing tests; squash fix-up commits before merging.
- `LARAVEL-21.5` **MUST** branch from `main` with a short typed name: `feat/invoice-reminders`, `fix/overdue-scope`.
- `LARAVEL-21.6` **SHOULD** keep pull requests under about 400 changed lines (excluding generated files and lock files); split larger work behind a Pennant flag.
- `LARAVEL-21.7` **MUST** pass `composer lint`, `composer test` and the frontend checks (`npm run lint`, `npm run types`, `npm run test`) before review.
- `LARAVEL-21.8` **MUST** include migrations, factories and tests in the same pull request as the feature they support.
- `LARAVEL-21.9` **MUST** describe what changed, why, how it was tested, and any deploy steps (migrations, config, queue restarts).