Laravel Coding Standards
Comprehensive coding standards for Laravel applications. Follow them exactly. For detailed code examples, see REFERENCE.md.
When to Apply
Apply these standards to ALL Laravel work:
- Writing new classes, models, controllers, services, actions
- Creating or modifying Eloquent models, traits, relationships
- Writing tests (Pest PHP)
- Creating config files, service providers, migrations
- Writing exception classes, building package architecture
- Code review and refactoring
1. PHP Style Rules
declare(strict_types=1)at top of every PHP file- Typed properties, parameter types, and return types everywhere
- Docblocks only when PHP type system cannot express it
: voidreturn type on methods returning nothing- String interpolation over concatenation:
"Hello {$name}"not'Hello ' . $name - Happy path last — return early for error/guard cases
- No
elseblocks — use early returns instead match()overswitchstatements- PHP 8 constructor property promotion with
readonly - PHP 8.1 backed enums over class constants
- PHP 8 attributes for metadata decoration
- Named arguments for clarity with many parameters
- Always
static::class,static::query(),new static()— neverself::for subclassable code $guarded = []not$fillable— guard only primary keys in constructor- Minimal comments — code is self-documenting. Comments explain WHY, never WHAT
- Blank lines between logical statement groups
- Zero logger calls unless absolutely necessary
- No
finalon classes
2. Architecture
Interface + Config + Factory Triangle
Every swappable component has:
- Interface in
Contracts/ - Default implementation class
- Config key pointing to the class
- Factory that reads config, validates, resolves via container
Trait Composition (Concerns)
- Traits in
Concerns/are the primary user API - Single responsibility per trait
- Compose traits together — one uses another internally
- Users add one trait and get everything
boot{TraitName}()static method for Eloquent lifecycle hooks
Config-Driven Everything
- Model classes, table names, column names — all configurable
- Feature flags for optional features (disabled by default)
- kebab-case filenames, snake_case keys
- Never
env()outside config files
Container Bindings
scoped()— stateful per-request singletons (mappers, resolvers)singleton()— global registries and cachesbind()— stateless factories and generators
Domain-Based Directory Structure
src/
Actions/ Concerns/ Contracts/ Enums/
Events/ Exceptions/ Factories/ Jobs/
Middleware/ Models/Concerns/ Support/ Transformers/
Events/exceptions/jobs can live inside feature domain namespace.
3. Naming Conventions
Classes: Models=Singular PascalCase, Controllers=Plural+Controller (CRUD methods only), Exceptions=Descriptive condition, Actions=Verb, Resolvers=Xxx+Resolver, Jobs=Action-based, Commands=Action+Command, Mailables=Event+Mail
Events: Before=Present participle (SavingSettings), After=Past tense (SettingsSaved)
Methods:
- Finders:
findByName()/findById()/findOrCreate() - Assignment: verb-first (
assignRole(),syncRoles()) - Checkers: has-prefix (
hasRole(),hasAnyRole(),hasAllRoles()) - Scopes: positive + negative pairs (
scopeRole()/scopeWithoutRole()) - Getters: get-prefix (
getRoleNames()) - Cache: forget-prefix (
forgetCachedPermissions()) - Actions/Resolvers: single
execute()method
Properties: Boolean = is/has prefix. Config = always config('key').
Validation: Always array notation ['required', 'string'], never pipe-separated.
Routes: HTTP verb first, camelCase names, kebab-case URLs.
4. Design Patterns
Static Factory Constructors on Exceptions
- Never
new Exception('msg')— alwaysExceptionClass::create($param) - Each error type = own class in
Exceptions/ - Named constructors return
staticnotself - Messages use
__()for i18n - Config flags hide sensitive data from errors
Input Polymorphism
Accept string|int|array|Model|Collection|BackedEnum, normalize internally with match(true).
Fluent Builder
- Every setter returns
static - Terminal method performs the operation
- Deferred execution: if model doesn't exist, defer to
Model::createdcallback Macroabletrait for runtime extensibility
Pipeline Pattern
Complex creation/transformation = ordered pluggable stages. Each pipe implements handle().
Observer for Side Effects
File I/O, cache invalidation, cleanup — Eloquent Observers, NOT inside models.
Action Classes
Single-purpose in Actions/ with execute() method. Config-swappable.
Contract + Concern Pairs
Every capability = parallel Interface + Trait. Compose only what you need.
5. Model Patterns
$guarded = []with primary key guarded in constructor- Table names from config:
$this->table = config('pkg.table_names.x') ?: parent::getTable() - Defaults in constructor with
??= - Custom Eloquent Collection via
newCollection() - Computed attributes via
Attribute::get() - Related model classes from config
- Traits in
Models/Concerns/ @propertyPHPDoc on model interfacesstatic::query()->create()(notstatic::create()) to avoid recursion- Finder trio:
findByName(),findById(),findOrCreate() - Scopes in positive/negative pairs
6. Config, Service Provider, Events, Migrations
Config: Block comments with full sentences. Sensible defaults. Performance features disabled by default. Models/tables/columns configurable. Feature flags as booleans.
Service Provider: Declarative API via PackageServiceProvider: $package->name()->hasConfigFile()->hasMigrations()->hasCommands(). Bind contracts to config-driven classes.
Events: Simple data classes with SerializesModels + constructor promotion. Past tense for completed, present participle for pre-action. Opt-in via config flag. Place inside feature domain.
Migrations: Anonymous class migrations. Table names from config. .php.stub for publishable package migrations.
7. Testing Standards (Pest PHP)
- Pest PHP exclusively — no PHPUnit class-based tests
uses(TestCase::class)->in(__DIR__)in Pest.phpit('can do something')lowercase natural English.test()for edge cases.expect()fluent API with chaining,->not->for negation,->toThrow()for exceptions- In-memory SQLite, no
RefreshDatabase, manual schema insetUp() - Orchestra Testbench as base with
getPackageProviders(),getEnvironmentSetUp() beforeEach()for per-test setupDB::enableQueryLog()for performance assertions- Domain-based test directories:
Models/,Traits/,Commands/
8. Dependencies & Version Control
- Minimal dependencies — only what is strictly necessary
- No unnecessary external packages
- PHP 8.2+, current Laravel LTS + latest
- kebab-case repo and branch names
- Present tense, descriptive, granular commits
- Main branch always stable, feature branches squash on merge
Summary Checklist
Before submitting any Laravel code:
-
declare(strict_types=1)at top - All methods have typed parameters and return types
- No
elseblocks — early returns used -
staticnotselffor late static binding -
$guarded = []not$fillable - Exceptions use static factory constructors
- Config-driven model/table references
- Traits compose each other, users add one
- Scopes in positive/negative pairs
- Events are opt-in, past-tense naming
- Tests use Pest
expect()API - Zero unnecessary logging
- Minimal comments, self-documenting code