PHP / Laravel Core
Shared model for the php-laravel cluster. The patterns, TDD, verification, and security spokes
all lean on these conventions — keep them consistent here so no spoke contradicts another.
1. The decision this cluster turns on: thin controllers, layered flow
Every request crosses the same one-way pipeline. Each layer has exactly one job, and logic
flows down, never back up:
HTTP Request ─> Route (model-bound) ─> Form Request (validate + authorize)
─> Controller (thin: translate I/O) ─> Service (orchestrate)
─> Action (single use case) ─> Model / Repository (persist)
─> API Resource (shape) ─> JSON envelope
- Controller — thin. Receives the validated request, calls one service/action, returns a
resource. No business logic, no queries. →
laravel-patterns
- Service — coordinates a multi-step use case across actions/models.
- Action — a single-purpose use case (
CreateOrderAction::handle()); the smallest reusable unit.
- Model — typed:
$fillable, $casts (enums/value objects), named scopes; never unguard().
- Form Request — the only place inputs are validated and authorization is asserted
(
authorize() + rules()); transform to a DTO before passing inward. → laravel-security
Rule: if logic lives in a controller, it's in the wrong place. Push it down one layer.
2. Validation & the trust boundary
The HTTP request is untrusted. Nothing reaches a service un-validated.
- Validate in Form Requests (
rules()), authorize in the same class (authorize() via a
policy/gate). Never derive privileged fields from the raw payload.
- Mass-assignment is guarded by
$fillable; prefer DTOs/explicit mapping over Model::unguard().
- Output is escaped by Blade (
{{ }}); queries use Eloquent/binding, never string-built SQL.
- Full hardening surface (CSRF, uploads, rate limiting, signed URLs, headers, CORS) →
laravel-security.
3. Standard JSON response envelope
Every API response — success or error — uses the same four-key shape so clients (and tests)
can rely on it:
return response()->json([
'success' => true,
'data' => OrderResource::make($order), // or ::collection(...) for lists
'error' => null,
'meta' => null, // pagination block on lists
], 201);
Tests assert this with assertJsonStructure(['success', 'data', 'error', 'meta']). Lists put
page/per_page/total under meta. → laravel-patterns (resources), laravel-tdd (assertions).
4. Test layers & database strategy
| Layer |
Covers |
Tool |
| Unit |
pure PHP: value objects, services, actions |
Pest / PHPUnit |
| Feature |
HTTP, auth, validation, response envelope |
actingAs + JSON asserts |
| Integration |
DB + queue + external boundaries together |
RefreshDatabase + fakes |
- Default to Pest for new tests; use PHPUnit only if the project already standardizes on it.
RefreshDatabase is the default DB trait (migrate once, transaction per test on supported
drivers); use DatabaseTransactions when the schema is already migrated.
- Isolate side effects with fakes:
Bus::fake(), Queue::fake(), Mail::fake(),
Notification::fake(), Http::fake(). Target 80%+ coverage (unit + feature). → laravel-tdd
5. The verification gate (sequential)
Phases run in order; an earlier failure blocks the rest. This is the contract laravel-verification
enforces before any PR or deploy:
env (php/composer/artisan) ─> composer validate + dump-autoload
─> pint --test + phpstan analyse # lint/static must be clean
─> php artisan test (+ --coverage in CI)
─> composer audit # dependency CVEs
─> migrate --pretend / migrate:status # review destructive/irreversible
─> config|route|view:cache + queue/scheduler readiness
→ laravel-verification for the full phase list and commands.
6. Version / tooling matrix
| Concern |
Baseline |
Spoke |
| Framework |
Laravel 11/12 (target the project's version) |
laravel-patterns |
| Language |
PHP 8.2+ (typed props, enums, readonly) |
— |
| API auth |
Laravel Sanctum (Passport for OAuth) |
laravel-security |
| Tests |
Pest (default) / PHPUnit |
laravel-tdd |
| Lint / static |
Laravel Pint + PHPStan (or Psalm) |
laravel-verification |
| Deps audit |
composer audit |
laravel-verification |
| Package vetting |
LaraPlugins.io MCP (health + compat) |
laravel-plugin-discovery |
7. Shared guardrails
- Thin controllers: business logic lives in services/actions, never the controller or route.
- Validate everything in a Form Request; the HTTP payload is untrusted; never derive
privileged fields from it.
- Default-deny authorization: policies/gates +
$fillable; never unguard(); scoped route
bindings to prevent cross-tenant access.
- Stable envelope: every API response is
{ success, data, error, meta }.
- Sequential gate: env/composer failures stop the pipeline; lint clean before tests; security
- migration review precede release steps.
- State every widening (mass-assignment field, CORS origin, rate-limit, scope) explicitly — it's
a security change.
1---2name: php-laravel-core3description: Shared reference for the Laravel cluster: the layered request flow (controller → service → action → model), typed Eloquent + Form Request validation, the standard JSON response envelope, the test/CI matrix, and the version/tooling baseline. USE WHEN structuring controllers, writing validation, wiring the test/verify pipeline, or choosing tooling — the conventions every Laravel spoke shares.4---56# PHP / Laravel Core78Shared model for the `php-laravel` cluster. The patterns, TDD, verification, and security spokes9all lean on these conventions — keep them consistent here so no spoke contradicts another.1011## 1. The decision this cluster turns on: thin controllers, layered flow1213Every request crosses the **same one-way pipeline**. Each layer has exactly one job, and logic14flows down, never back up:1516```17HTTP Request ─> Route (model-bound) ─> Form Request (validate + authorize)18 ─> Controller (thin: translate I/O) ─> Service (orchestrate)19 ─> Action (single use case) ─> Model / Repository (persist)20 ─> API Resource (shape) ─> JSON envelope21```2223- **Controller** — thin. Receives the validated request, calls one service/action, returns a24 resource. No business logic, no queries. → `laravel-patterns`25- **Service** — coordinates a multi-step use case across actions/models.26- **Action** — a single-purpose use case (`CreateOrderAction::handle()`); the smallest reusable unit.27- **Model** — typed: `$fillable`, `$casts` (enums/value objects), named scopes; never `unguard()`.28- **Form Request** — the *only* place inputs are validated and authorization is asserted29 (`authorize()` + `rules()`); transform to a DTO before passing inward. → `laravel-security`3031**Rule:** if logic lives in a controller, it's in the wrong place. Push it down one layer.3233## 2. Validation & the trust boundary3435The HTTP request is **untrusted**. Nothing reaches a service un-validated.3637- Validate in **Form Requests** (`rules()`), authorize in the same class (`authorize()` via a38 policy/gate). Never derive privileged fields from the raw payload.39- Mass-assignment is guarded by `$fillable`; prefer DTOs/explicit mapping over `Model::unguard()`.40- Output is escaped by Blade (`{{ }}`); queries use Eloquent/binding, never string-built SQL.41- Full hardening surface (CSRF, uploads, rate limiting, signed URLs, headers, CORS) → `laravel-security`.4243## 3. Standard JSON response envelope4445Every API response — success or error — uses the same four-key shape so clients (and tests)46can rely on it:4748```php49return response()->json([50 'success' => true,51 'data' => OrderResource::make($order), // or ::collection(...) for lists52 'error' => null,53 'meta' => null, // pagination block on lists54], 201);55```5657Tests assert this with `assertJsonStructure(['success', 'data', 'error', 'meta'])`. Lists put58`page`/`per_page`/`total` under `meta`. → `laravel-patterns` (resources), `laravel-tdd` (assertions).5960## 4. Test layers & database strategy6162| Layer | Covers | Tool |63|---|---|---|64| **Unit** | pure PHP: value objects, services, actions | Pest / PHPUnit |65| **Feature** | HTTP, auth, validation, response envelope | `actingAs` + JSON asserts |66| **Integration** | DB + queue + external boundaries together | `RefreshDatabase` + fakes |6768- Default to **Pest** for new tests; use PHPUnit only if the project already standardizes on it.69- **`RefreshDatabase`** is the default DB trait (migrate once, transaction per test on supported70 drivers); use `DatabaseTransactions` when the schema is already migrated.71- Isolate side effects with fakes: `Bus::fake()`, `Queue::fake()`, `Mail::fake()`,72 `Notification::fake()`, `Http::fake()`. Target **80%+** coverage (unit + feature). → `laravel-tdd`7374## 5. The verification gate (sequential)7576Phases run in order; an earlier failure blocks the rest. This is the contract `laravel-verification`77enforces before any PR or deploy:7879```80env (php/composer/artisan) ─> composer validate + dump-autoload81 ─> pint --test + phpstan analyse # lint/static must be clean82 ─> php artisan test (+ --coverage in CI)83 ─> composer audit # dependency CVEs84 ─> migrate --pretend / migrate:status # review destructive/irreversible85 ─> config|route|view:cache + queue/scheduler readiness86```8788→ `laravel-verification` for the full phase list and commands.8990## 6. Version / tooling matrix9192| Concern | Baseline | Spoke |93|---|---|---|94| Framework | Laravel 11/12 (target the project's version) | `laravel-patterns` |95| Language | PHP 8.2+ (typed props, enums, readonly) | — |96| API auth | Laravel Sanctum (Passport for OAuth) | `laravel-security` |97| Tests | Pest (default) / PHPUnit | `laravel-tdd` |98| Lint / static | Laravel Pint + PHPStan (or Psalm) | `laravel-verification` |99| Deps audit | `composer audit` | `laravel-verification` |100| Package vetting | LaraPlugins.io MCP (health + compat) | `laravel-plugin-discovery` |101102## 7. Shared guardrails103104- **Thin controllers**: business logic lives in services/actions, never the controller or route.105- **Validate everything** in a Form Request; the HTTP payload is untrusted; never derive106 privileged fields from it.107- **Default-deny authorization**: policies/gates + `$fillable`; never `unguard()`; scoped route108 bindings to prevent cross-tenant access.109- **Stable envelope**: every API response is `{ success, data, error, meta }`.110- **Sequential gate**: env/composer failures stop the pipeline; lint clean before tests; security111 + migration review precede release steps.112- State every widening (mass-assignment field, CORS origin, rate-limit, scope) explicitly — it's113 a security change.