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 —
$exceptionnot$e,$requestnot$r ?stringnotstring|null. Always specifyvoid. 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)|nullfor 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 responseswhen()/mergeWhen()for permission-based field inclusionwhenPivotLoaded()for pivot datawithResponse()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() => ...) ShouldBeUniqueinterface to prevent duplicate processing- Always handle failures — implement
failed()method on jobs
Testing (Pest)
- RED → verify RED → GREEN → verify GREEN → REFACTOR
test()/it()syntax withRefreshDatabasetrait- 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$fillableinstead- Inline validation in controllers — use Form Requests instead