Stack conventions
Laravel
Laravel 11+ applications: Eloquent, queues, Blade or Inertia front ends, validated with Form Requests and tested with Pest or PHPUnit.
Laravel 11+ applications: Eloquent, queues, Blade or Inertia front ends, validated with Form Requests and tested with Pest or PHPUnit.
Used for: laravel.
Principles
- Follow Laravel's conventions and the structure the application already uses; reach for framework features before custom infrastructure.
- Validate every request with Form Request classes (or validate() for trivial cases) and use only validated() data.
- Authorise with policies and gates on every action that touches a model; scope queries to the current user or tenant.
- Prevent N+1 queries with eager loading (with, load, withCount) and enable Model::preventLazyLoading outside production.
- Keep controllers thin; put reusable workflows in single-purpose action or service classes injected by the container.
- Make queued jobs idempotent, small and retry-aware; dispatch them after the database transaction commits.
- Guard mass assignment with explicit fillable attributes (or guarded) and never fill models from $request->all().
- Keep secrets in environment variables read only in config files; application code reads config(), never env().
- Test through HTTP feature tests with RefreshDatabase and the framework fakes (Queue, Mail, Http, Event, Storage).
- Declare strict types at the top of every PHP file (declare(strict_types=1)) unless the repository deliberately does not.
- Type everything - parameters, return types, typed and readonly properties, and precise PHPDoc generics where native types cannot express it.
- Follow PER Coding Style (the successor of PSR-12) as enforced by the repository's PHP-CS-Fixer or PHP_CodeSniffer configuration.
Conventions
Accessibility
- Use semantic HTML in Blade; every input has a label and validation errors (@error) are tied to fields with aria-describedby.
- Announce flash and validation summaries with role="status" or role="alert"; keep keyboard focus sensible after Livewire or Inertia updates.
Concurrency
- Jobs must be idempotent; use ShouldBeUnique, WithoutOverlapping, Cache::lock or database unique constraints against duplicates.
- Use DB::transaction with lockForUpdate for read-modify-write sequences on the same row.
- Dispatch jobs and events after commit so workers never read uncommitted data.
- PHP-FPM requests share nothing; persist shared state in the database or cache with explicit locking (SELECT ... FOR UPDATE, atomic cache operations).
- In long-running workers (queues, RoadRunner, FrankenPHP worker mode, Swoole), reset state between jobs and avoid static caches that leak across requests.
Dependencies
- Prefer first-party packages (Sanctum, Horizon, Pint, Pulse, Reverb, Cashier) before third-party ones.
- Upgrade Laravel one major version at a time following the official upgrade guide; keep PHP on a supported version.
- Commit composer.lock for applications; libraries declare constraints with caret (^) ranges and a php platform requirement.
- Use require-dev for tooling; run composer validate --strict in CI.
- Avoid abandoned packages (composer outdated / audit report them).
Documentation
- Keep README setup (composer install, php artisan key:generate, php artisan migrate --seed) accurate.
- Document API endpoints with the repository's tool (Scribe, Scramble or OpenAPI) when it has one.
- Rely on native types; add PHPDoc only where it adds information (generics like list
, array shapes, @throws). - Keep README setup and composer scripts (composer test, composer lint) documented and working.
Errors
- Customise exception rendering and reporting in bootstrap/app.php withExceptions (Laravel 11+) or the Handler class in older apps.
- Use abort_if / abort_unless and findOrFail for not-found and forbidden flows; never leak stack traces (APP_DEBUG=false in production).
- Use report() for handled exceptions that should still reach error tracking.
- Throw specific exception classes (extending a project base exception or SPL exceptions like InvalidArgumentException, DomainException).
- Catch Throwable only at the top-level handler; catch specific exceptions elsewhere and never silence with @.
- Chain exceptions with the previous parameter to keep the cause.
- Configure display_errors=Off in production and log errors instead.
Logging
- Use the Log facade or injected Psr\Log\LoggerInterface with context arrays; configure channels in config/logging.php.
- Use Log::withContext or Context (Laravel 11+) to attach request identifiers.
- Never log request bodies containing passwords or tokens.
- Log through a PSR-3 LoggerInterface (Monolog) injected into classes; no echo, var_dump or print_r left in code.
- Use message placeholders with a context array (logger->info('Order {id} paid', ['id' => $id])).
- Never log passwords, tokens, card data or full request bodies.
Naming
- Models singular StudlyCase (Order), tables plural snake_case (orders), pivot tables singular alphabetical (order_product).
- Controllers singular resource name plus Controller (OrderController); resource routes and route names (orders.show).
- Form requests named for the action (StoreOrderRequest, UpdateOrderRequest); jobs as imperative verbs (SendInvoice); events in past tense (OrderShipped).
- Classes, interfaces, traits and enums in StudlyCaps; methods and properties in camelCase; constants and enum cases per the repository (UPPER_SNAKE constants, StudlyCaps enum cases are common).
- Interfaces named for the role (Clock, OrderRepository); suffixes like Interface only if the codebase already uses them.
- Namespaces start with the vendor/application root declared in composer.json autoload.
Performance
- Eager load relationships (with, load, withCount, withSum); call Model::preventLazyLoading(! app()->isProduction()) in a service provider.
- Use chunkById or lazyById for large datasets; select only needed columns; add indexes in migrations.
- Cache expensive reads with Cache::remember / Cache::flexible; cache config, routes, events and views in deployment (php artisan optimize).
- Push slow work to queues and run workers under Horizon or a supervisor.
- Enable OPcache (and preloading where it helps) in production; use composer dump-autoload --optimize --classmap-authoritative for deployments.
- Avoid queries in loops; batch or eager load.
- Use generators (yield) to stream large datasets instead of loading arrays into memory.
Security
- Keep the CSRF middleware for web routes; use Sanctum or Passport for API authentication.
- Never pass user input to DB::raw, whereRaw or orderByRaw without bindings; validate column names against an allowlist for sorting.
- Use {{ }} in Blade (escaped); use {!! !!} only for trusted, sanitised HTML.
- Hash passwords with Hash::make (bcrypt/argon2); encrypt sensitive attributes with the encrypted cast.
- Rate limit authentication and expensive endpoints with RateLimiter and the throttle middleware.
- Validate file uploads (mimes, max size) and store them outside the public root unless they are meant to be public.
- Use PDO prepared statements with bound parameters (PDO::ATTR_EMULATE_PREPARES false where possible) or the ORM's query builder.
- Escape output with htmlspecialchars($s, ENT_QUOTES, 'UTF-8') or the template engine's auto-escaping (Twig, Blade).
Structure
- Use artisan make commands to generate classes so they land in the conventional place.
- Routes in routes/web.php and routes/api.php; use route model binding and resource controllers.
- Configuration only in config/*.php reading env(); cache config in production (php artisan config:cache), which makes env() outside config return null.
- Blade components or Livewire/Inertia components consistent with the existing front end; no queries in views.
- Source in src/ mapped by composer.json autoload psr-4; tests in tests/ mapped by autoload-dev.
- Public web root contains only the front controller (public/index.php) and assets.
- One class, interface, trait or enum per file; file name equals the class name.
- Configuration from environment variables or config files outside the web root; never commit secrets or .env with real values.
Testing
- Feature tests via HTTP ($this->postJson(...)->assertCreated()) with RefreshDatabase or DatabaseTransactions.
- Use model factories with states; avoid fixtures that depend on test order.
- Fake side effects (Queue::fake, Mail::fake, Notification::fake, Http::fake, Storage::fake, Event::fake) and assert on them; call Http::preventStrayRequests().
- Use Pest or PHPUnit as the repository does; run with php artisan test (parallel where supported).
- Done means tested: new or changed behaviour (a route, a job, a policy) ships with a feature or unit test in the same change.
- PHPUnit (or Pest if the repository uses it); test classes are final, named
Test, methods named for behaviour. - Use data providers (PHPUnit 10+ attributes: #[DataProvider], #[Test]) for input variations.
- Prefer fakes and in-memory implementations over deep mocking; mock only interfaces you own.
Review checklist
- Is input validated by a Form Request and is only validated() data used?
- Is every model access authorised by a policy or gate and scoped to the current user or tenant?
- Are relationships eager loaded where iterated, with no new N+1 queries?
- Are models protected against mass assignment (fillable/guarded) and is $request->all() avoided?
- Are jobs idempotent, dispatched after commit, with tries/backoff set?
- Is env() used only in config files?
- Are raw queries bound, and Blade output escaped with {{ }}?
- Is the controller thin with reusable logic in actions or services?
- Do feature tests cover the endpoint, including authorisation failures, using fakes for side effects?
- Are migrations reversible, with indexes for new foreign keys and filtered columns?
- Does Pint/Larastan report no new issues?
- Are API responses shaped with API resources rather than raw models?
Quality checks (run when your repository has the tool configured)
| Check | Kind | Tool | Blocks the pull request |
|---|---|---|---|
pint |
format | pint | no |
phpstan |
static | phpstan | no |
psalm |
static | psalm | no |
php-cs-fixer |
format | php-cs-fixer | no |
phpcs |
lint | PHP_CodeSniffer | no |
composer-audit |
security | composer audit | no |
composer-validate |
lint | composer validate | no |
php-syntax |
static | php | no |
gitleaks |
security | gitleaks | no |