php-laravel · git:20260227.215f429 · 2026-02-27 · sha256 ceceb34a96b044a1

php-laravel git:20260227.215f429A

Immutable. This exact content is served forever at /api/v1/blob/ceceb34a96b044a1.

---
name: php-laravel
description: >-
  Modern PHP 8.2+ and Laravel patterns: architecture, Eloquent, queues, Pest
  testing. Use when asked to "write PHP", "build a Laravel app", "fix Eloquent
  query", "add a queue job", "write a Pest test", or mentions PHP, Laravel,
  Eloquent, Blade, artisan, or migrations.
---

# PHP & Laravel Development

## Code Style

- `declare(strict_types=1)` in every file
- Happy path last — handle errors/guards first, success at the end. Use early returns; avoid `else`.
- Comments only explain *why*, never *what*. Never comment tests. If code needs a "what" comment, rename or restructure instead.
- No single-letter variables — `$exception` not `$e`, `$request` not `$r`
- `?string` not `string|null`. Always specify `void`. Import classnames everywhere, never inline FQN.
- Validation uses array notation `['required', 'email']` for easier custom rule classes

## Modern PHP (8.2+)

Use these when applicable — do not explain them in comments (Claude and developers know them):
- Readonly classes and properties for immutable data
- Enums with methods and interfaces for domain constants
- Match expressions over switch
- Constructor promotion with readonly
- First-class callable syntax `$fn = $obj->method(...)`
- Fibers for cooperative async when Swoole/ReactPHP not available
- DNF types `(Stringable&Countable)|null` for complex constraints

## Laravel Architecture

- **Fat models, thin controllers** — controllers only: validate, call service/action, return response
- **Service classes** for business logic with readonly DI: `__construct(private readonly PaymentService $payments)`
- **Action classes** (single-purpose invokable) for operations that cross service boundaries
- **Form Requests** for all validation — never validate inline in controllers
- **Events + Listeners** for side effects (notifications, logging, cache invalidation). Do not put side effects in services.
- Feature folder organization over type-based when project exceeds ~20 models

## Eloquent

- `Model::preventLazyLoading(!app()->isProduction())` — catch N+1 during development
- Select only needed columns: `Post::with(['user:id,name'])->select(['id', 'title', 'user_id'])`
- Bulk operations at database level: `Post::where('status', 'draft')->update([...])` — do not load into memory to update
- `increment()`/`decrement()` for counters in a single query
- Composite indexes for common query combinations
- Chunking for large datasets (`chunk(1000)`), lazy collections for memory-constrained processing
- `$guarded = []` is a mass assignment vulnerability — always use explicit `$fillable`

## API Resources

- `whenLoaded()` for relationships — prevents N+1 in responses
- `when()` / `mergeWhen()` for permission-based field inclusion
- `whenPivotLoaded()` for pivot data
- `withResponse()` for custom headers, `with()` for metadata (version, pagination)

## Queues & Jobs

- Job batching with `Bus::batch([...])->then()->catch()->finally()->dispatch()`
- Job chaining for sequential ops: `Bus::chain([new Step1, new Step2])->dispatch()`
- Rate limiting: `Redis::throttle('api')->allow(10)->every(60)->then(fn() => ...)`
- `ShouldBeUnique` interface to prevent duplicate processing
- Always handle failures — implement `failed()` method on jobs

## Testing (Pest)

- RED → verify RED → GREEN → verify GREEN → REFACTOR
- `test()` / `it()` syntax with `RefreshDatabase` trait
- One assertion focus per test. Test name describes the behavior, not the method.
- `Sanctum::actingAs($user, ['ability'])` for API auth testing
- Run relevant tests first, offer full suite after

## Discipline

- For non-trivial changes, pause and ask: "is there a more elegant way?" Skip for obvious fixes.
- Simplicity first — every change as simple as possible, impact minimal code
- Only touch what's necessary — avoid introducing unrelated changes
- No hacky workarounds — if a fix feels wrong, step back and implement the clean solution

## Anti-Patterns

- Querying in loops — use eager loading or `whereIn()` instead
- Empty catch blocks — log or rethrow, never swallow
- Business logic in controllers — extract to service/action instead
- `protected $guarded = []` — use `$fillable` instead
- Inline validation in controllers — use Form Requests instead