PHP standards (modern, Laravel, Symfony)
1. Scope and triggers
Applies to all work on PHP code: .php files, composer.json/composer.lock,
artisan/bin/console commands, Blade/Twig templates, PHPStan/Psalm/PHPUnit/Pest configuration,
CI pipelines of PHP projects. Covers writing, reviewing, refactoring and testing.
Not applicable: see api-design-standards (HTTP/GraphQL contract design — here only its
implementation in Laravel/Symfony), appsec-standards (threat modelling and stack-agnostic
vulnerability classes; here only PHP's concrete sinks and flags),
microservices-architecture-standards (service boundaries, events, distributed queues, sagas),
data-platform-standards (modelling, indexes and engine tuning; here only Eloquent/Doctrine and their
migrations), cicd-standards (the pipeline that runs the gates), kubernetes-standards (OCI
image, PHP-FPM in a container and deployment), onprem-standards (the web server/PHP-FPM on the host and its
hardening), observability-standards (OTel pipeline; here only the instrumentation),
git-workflow-standards (branch, commits and SemVer tagging; publishing to Packagist is this
skill's), identity-access-management-standards (IdP design; here only how the app consumes it),
sql-standards (the SQL that Doctrine or Eloquent generate, and the SQL written by hand).
Language choice (the skill for the chosen language wins): python-standards,
typescript-standards, go-standards, jvm-spring-standards, dotnet-standards,
ruby-standards (the most direct comparison: Laravel and Rails occupy the same slot; the
choice is one of team and ecosystem, not of performance), elixir-erlang-standards.
Rule zero: detect the project context first (composer.json → PHP version, framework,
tools already present) and respect its conventions. These standards set the criteria for
new code and for flagging debt; do not rewrite what exists outside the requested scope.
2. Default toolchain
Verification note: versions checked via the web on 2026-08-02 (php.net, laravel.com,
symfony.com/releases, phpstan.org, pestphp.com). Before pinning a version in a project,
verify the current state on the web — this document expires.
- PHP: new projects on PHP 8.5 (current, EOL 2029-12-31) or 8.4 (active, EOL 2028-12-31).
8.3 and 8.2 are in security-only (EOL 2027-12-31 and 2026-12-31); maintenance only, not new projects.
≤8.1 is EOL: treat it as a security incident, not as a preference.
- Laravel: Laravel 13 (March 2026, requires PHP ≥8.3). There are no LTS releases: each major
gets 18 months of bugfixes and 2 years of security — plan the annual upgrade.
- Symfony: 7.4 LTS (supported until Nov 2029) for long-lived products;
8.x (currently 8.1, requires PHP ≥8.4) if you take on the six-monthly upgrade cadence.
- Composer: 2.x always.
composer.lock versioned in applications; in libraries it is not versioned
but is tested against --prefer-lowest and latest in CI.
- Testing: Pest 5 (on top of PHPUnit 13) by default in new projects; plain PHPUnit is
just as valid if the project already uses it. Do not mix styles in the same suite.
- Static analysis: PHPStan 2.x with
level: max (today level 10) + phpstan-strict-rules
(+ Larastan on Laravel, phpstan-symfony on Symfony). Psalm (errorLevel="1") as an alternative
if the project already uses it; one of the two is mandatory, not optional.
- Style: PSR-12 (and PER Coding Style) with PHP-CS-Fixer or Laravel Pint (Laravel projects).
- Auxiliaries: Rector for automated upgrades;
composer audit for SCA.
3. Structure and conventions
declare(strict_types=1); mandatory in EVERY new PHP file, as the first statement. In legacy
code without it, flag it; enable it only with test coverage that backs the change.
- Explicit types everywhere: parameters, returns (incl.
void/never), typed properties.
No mixed except at a real boundary (deserialisation, reflection) and always narrowed immediately.
- Immutability by default:
readonly on properties and DTO/VO classes, final by default on
classes not designed for inheritance, enums (enum) instead of loose class constants,
promoted constructor properties.
- PSR-4 for autoloading (
src/ → App\ or the vendor namespace); one type per file; revealing
names (StudlyCaps classes, camelCase methods, SCREAMING_SNAKE constants).
- Laravel: framework conventions first — Form Requests for validation, Eloquent with
typed casts/relations, queues for deferred work,
config() only from config files
(never env() outside config/), policies for authorisation. Domain logic outside
controllers (actions/services); thin controllers.
- Symfony: constructor injection with autowiring, private services by default, PHP
attributes (
#[Route], #[AsMessageHandler]) over YAML for what is local to the code, Messenger for
asynchrony, symfony/validator at the edges.
- Errors: domain-specific exceptions, never
@ nor an empty catch; try/finally or
equivalents to release resources. No half-done states.
4. Quality: formatting, lint, static analysis, testing
Gates in CI, all blocking — nothing is merged with any of them red:
- Formatting: Pint (
pint --test) or PHP-CS-Fixer (--dry-run --diff) against PSR-12/PER.
- Static:
phpstan analyse --level=max (or psalm --show-info=false at level 1) with no errors.
- The baseline (
phpstan-baseline.neon) only for adoption in legacy: it is frozen and only shrinks;
adding new entries to the baseline is forbidden.
- A one-off
@phpstan-ignore requires a comment with the reason; without a reason, it is an error to fix.
- Tests:
pest / phpunit complete, deterministic, in parallel once the suite grows
(pest --parallel, paratest).
- SCA:
composer audit with no known vulnerabilities left untriaged.
- Coherent lock:
composer validate --strict and composer install --dry-run clean.
Testing criteria:
- Observable behaviour, not implementation. Cover the happy path, edges and errors (invalid
inputs, limits, dependency failures) — a suite with no error tests is incomplete.
- Pyramid: fast unit tests as the majority; integration (HTTP kernel, DB with transaction and
rollback or RefreshDatabase) just enough; E2E minimal.
- Mock boundaries (HTTP, queues, clock, filesystem), not your own classes. In Laravel use the framework's
fakes (
Queue::fake(), Http::fake(), Event::fake()); in Symfony, clock-mock/test
services. Never a real network in tests.
- Every bugfix lands with a regression test that first reproduces the failure.
- Arch tests (Pest
arch()) for structural invariants: no dd()/dump()/var_dump in
production, dependencies between layers, strict_types present.
- Mutation testing (Infection) recommended in libraries and critical domains; coverage as a signal
(critical lines covered), never as a numeric target.
5. Stack security
- OWASP Top 10 as an active checklist: queries ALWAYS parameterised (Eloquent/Doctrine/PDO
prepared) — concatenating input into SQL is forbidden, even in
whereRaw/DQL: use bindings.
Context-aware escaping: Blade {{ }} / Twig autoescape; {!! !!}/|raw only with sanitised
content and justified in writing.
- Deserialisation: never
unserialize() on external input (use json_decode with validation
or allowed_classes: false if there is no alternative). Watch out for SSRF in HTTP clients that receive
user-supplied URLs: validate scheme/host against an allowlist.
- Authentication/authorisation: framework primitives (Laravel
Auth/policies/Sanctum;
Symfony Security/voters), never home-made. password_hash() with Argon2id/bcrypt; comparisons
with hash_equals(). Authorise every action server-side, do not just hide UI.
- Mass assignment: strict
$fillable (not $guarded = []) in Eloquent; DTOs/Form Requests
as the input boundary. ALWAYS validate at the edge (Form Request / Validator / Symfony
Validator), not ad hoc in the controller.
- Secrets: only in environment variables / a secrets manager;
.env outside VCS; nothing in
logs or exceptions. APP_DEBUG=false in production (Laravel exposes secrets with debug on).
- Headers and session:
Secure, HttpOnly, SameSite cookies; CSRF enabled on forms;
HSTS; rate limiting on authentication endpoints.
- Continuous SCA:
composer audit in CI + Dependabot/Renovate; abandoned dependencies are
replaced, not ignored. No extensions/packages with open CVEs without documented mitigation.
- Crypto:
random_bytes/random_int, sodium or OpenSSL AES-GCM. MD5/SHA-1 for
security, mt_rand/rand for tokens and home-made encryption are forbidden.
6. Performance and operability
- OPcache always enabled in production;
composer install --no-dev --optimize-autoloader
(+ --classmap-authoritative on an immutable deploy). Preloading only with measurement to back it.
- N+1 is a bug: eager loading (
with(), joins, Model::preventLazyLoading() in non-production;
Doctrine fetch join). Pagination mandatory in listings; never all() without a limit.
- Heavy work onto queues/Messenger with retries + backoff and monitored
failed_jobs/failure
transport; idempotent jobs (redelivery happens). Cache with explicit invalidation
(tags/TTL), not "just in case".
- PHP-FPM/worker mode: size
pm.max_children with data; if you use Octane/FrankenPHP/
RoadRunner, check for state leaks between requests (statics, container).
- Observability: structured logs (Monolog JSON) with context and without sensitive data; metrics and
traces (OpenTelemetry) in services; health checks (
/up, liveness/readiness) for the orchestrator.
- Backward-compatible DB migrations (expand/contract); never a destructive migration in the
same deploy as the code that stops using the column.
7. Sustainability: cadence and prohibitions
Upgrade cadence:
- PHP: move up a minor version within <6 months of release; abandon a line BEFORE it enters
security-only. Cite the EOL in the plan (endoflife.date/php).
- Laravel: annual major — budget the upgrade every year (Shift/Rector help); do not fall more
than one major behind the current one. Symfony: jump from LTS to LTS (7.4 → 8.4) or follow the six-monthly cadence,
an explicit project decision.
- Dependencies: Renovate/Dependabot weekly; security patches in <72 h.
LIST OF PROHIBITIONS (they block review):
- A new file without
declare(strict_types=1).
eval(), extract(), $$variables variable variables, @ (error suppression), goto.
exec/shell_exec/system/proc_open with unsanitised input; backticks.
- SQL/commands concatenating input;
unserialize() of external data.
env() outside config/ (Laravel); hardcoded secrets; APP_DEBUG=true in production.
mixed without justification; array without a shape/generic docblock in public APIs.
- Suppressing PHPStan/Psalm errors without a comment giving the reason; growing the baseline.
dd()/dump()/var_dump()/print_r() in production code.
- Tests that depend on a real network, the system clock without a fake, or execution order.
- New dependencies without justification (does the framework or the stdlib solve it?); abandoned packages.
- Inheritance as code reuse (use composition); service location (
app()->make in the
domain) instead of constructor injection.
- Commits that mix mass reformatting with functional changes.
8. Mandatory web verification
Before pinning ANY version, flag or API in a real project, search for it on the web — do not
take it as good from this file or from memory:
- Supported PHP versions and EOL dates: php.net/supported-versions, endoflife.date/php.
- Version and support policy of Laravel (laravel.com/docs/releases) and Symfony
(symfony.com/releases — confirm which is the current LTS).
- Compatibility of PHPStan/Psalm/Pest/PHPUnit with the project's PHP version (Packagist).
- Dependency CVEs:
composer audit + GitHub Advisories before recommending a package.
If the web's data contradicts this document, the web wins — mention the discrepancy.
1---2name: php-standards3description: PHP engineering standards (modern PHP, Laravel, Symfony). Use when working with .php files, composer.json/composer.lock, artisan commands, phpunit.xml, phpstan.neon, psalm.xml, Blade/Twig templates, or any Laravel/Symfony project task (code, review, refactor, tests, CI).4---56# PHP standards (modern, Laravel, Symfony)78## 1. Scope and triggers910Applies to all work on PHP code: `.php` files, `composer.json`/`composer.lock`,11`artisan`/`bin/console` commands, Blade/Twig templates, PHPStan/Psalm/PHPUnit/Pest configuration,12CI pipelines of PHP projects. Covers writing, reviewing, refactoring and testing.1314**Not applicable**: see `api-design-standards` (HTTP/GraphQL contract design — here only its15implementation in Laravel/Symfony), `appsec-standards` (threat modelling and stack-agnostic16vulnerability classes; here only PHP's concrete sinks and flags),17`microservices-architecture-standards` (service boundaries, events, distributed queues, sagas),18`data-platform-standards` (modelling, indexes and engine tuning; here only Eloquent/Doctrine and their19migrations), `cicd-standards` (the pipeline that runs the gates), `kubernetes-standards` (OCI20image, PHP-FPM in a container and deployment), `onprem-standards` (the web server/PHP-FPM on the host and its21hardening), `observability-standards` (OTel pipeline; here only the instrumentation),22`git-workflow-standards` (branch, commits and SemVer tagging; publishing to Packagist *is* this23skill's), `identity-access-management-standards` (IdP design; here only how the app consumes it),24`sql-standards` (the SQL that Doctrine or Eloquent generate, and the SQL written by hand).25**Language choice** (the skill for the chosen language wins): `python-standards`,26`typescript-standards`, `go-standards`, `jvm-spring-standards`, `dotnet-standards`,27`ruby-standards` (**the most direct comparison**: Laravel and Rails occupy the same slot; the28choice is one of team and ecosystem, not of performance), `elixir-erlang-standards`.2930**Rule zero**: detect the project context first (`composer.json` → PHP version, framework,31tools already present) and respect its conventions. These standards set the criteria for32new code and for flagging debt; do not rewrite what exists outside the requested scope.3334## 2. Default toolchain3536> **Verification note**: versions checked via the web on 2026-08-02 (php.net, laravel.com,37> symfony.com/releases, phpstan.org, pestphp.com). Before pinning a version in a project,38> **verify the current state on the web** — this document expires.3940- **PHP**: new projects on **PHP 8.5** (current, EOL 2029-12-31) or **8.4** (active, EOL 2028-12-31).41 8.3 and 8.2 are in *security-only* (EOL 2027-12-31 and 2026-12-31); maintenance only, not new projects.42 ≤8.1 is EOL: treat it as a security incident, not as a preference.43- **Laravel**: **Laravel 13** (March 2026, requires PHP ≥8.3). There are no LTS releases: each major44 gets 18 months of bugfixes and 2 years of security — plan the annual upgrade.45- **Symfony**: **7.4 LTS** (supported until Nov 2029) for long-lived products;46 **8.x** (currently 8.1, requires PHP ≥8.4) if you take on the six-monthly upgrade cadence.47- **Composer**: 2.x always. `composer.lock` versioned in applications; in libraries it is not versioned48 but is tested against `--prefer-lowest` and latest in CI.49- **Testing**: **Pest 5** (on top of PHPUnit 13) by default in new projects; plain **PHPUnit** is50 just as valid if the project already uses it. Do not mix styles in the same suite.51- **Static analysis**: **PHPStan 2.x** with `level: max` (today level 10) + `phpstan-strict-rules`52 (+ Larastan on Laravel, phpstan-symfony on Symfony). Psalm (`errorLevel="1"`) as an alternative53 if the project already uses it; one of the two is mandatory, not optional.54- **Style**: PSR-12 (and PER Coding Style) with **PHP-CS-Fixer** or **Laravel Pint** (Laravel projects).55- **Auxiliaries**: Rector for automated upgrades; `composer audit` for SCA.5657## 3. Structure and conventions5859- **`declare(strict_types=1);`** mandatory in EVERY new PHP file, as the first statement. In legacy60 code without it, flag it; enable it only with test coverage that backs the change.61- **Explicit types everywhere**: parameters, returns (incl. `void`/`never`), typed properties.62 No `mixed` except at a real boundary (deserialisation, reflection) and always narrowed immediately.63- **Immutability by default**: `readonly` on properties and DTO/VO classes, `final` by default on64 classes not designed for inheritance, enums (`enum`) instead of loose class constants,65 promoted constructor properties.66- PSR-4 for autoloading (`src/` → `App\` or the vendor namespace); one type per file; revealing67 names (`StudlyCaps` classes, `camelCase` methods, `SCREAMING_SNAKE` constants).68- **Laravel**: framework conventions first — Form Requests for validation, Eloquent with69 typed casts/relations, queues for deferred work, `config()` only from config files70 (never `env()` outside `config/`), policies for authorisation. Domain logic outside71 controllers (actions/services); thin controllers.72- **Symfony**: constructor injection with autowiring, private services by default, PHP73 attributes (`#[Route]`, `#[AsMessageHandler]`) over YAML for what is local to the code, Messenger for74 asynchrony, `symfony/validator` at the edges.75- Errors: domain-specific exceptions, never `@` nor an empty catch; `try/finally` or76 equivalents to release resources. No half-done states.7778## 4. Quality: formatting, lint, static analysis, testing7980Gates in CI, all blocking — nothing is merged with any of them red:81821. **Formatting**: Pint (`pint --test`) or PHP-CS-Fixer (`--dry-run --diff`) against PSR-12/PER.832. **Static**: `phpstan analyse --level=max` (or `psalm --show-info=false` at level 1) with no errors.84 - The baseline (`phpstan-baseline.neon`) only for adoption in legacy: it is frozen and **only shrinks**;85 adding new entries to the baseline is forbidden.86 - A one-off `@phpstan-ignore` requires a comment with the reason; without a reason, it is an error to fix.873. **Tests**: `pest` / `phpunit` complete, deterministic, in parallel once the suite grows88 (`pest --parallel`, `paratest`).894. **SCA**: `composer audit` with no known vulnerabilities left untriaged.905. **Coherent lock**: `composer validate --strict` and `composer install --dry-run` clean.9192Testing criteria:93- Observable behaviour, not implementation. Cover the happy path, **edges and errors** (invalid94 inputs, limits, dependency failures) — a suite with no error tests is incomplete.95- Pyramid: fast unit tests as the majority; integration (HTTP kernel, DB with transaction and96 rollback or RefreshDatabase) just enough; E2E minimal.97- Mock boundaries (HTTP, queues, clock, filesystem), not your own classes. In Laravel use the framework's98 fakes (`Queue::fake()`, `Http::fake()`, `Event::fake()`); in Symfony, `clock-mock`/test99 services. Never a real network in tests.100- Every bugfix lands with a regression test that first reproduces the failure.101- Arch tests (Pest `arch()`) for structural invariants: no `dd()`/`dump()`/`var_dump` in102 production, dependencies between layers, `strict_types` present.103- Mutation testing (Infection) recommended in libraries and critical domains; coverage as a signal104 (critical lines covered), never as a numeric target.105106## 5. Stack security107108- **OWASP Top 10 as an active checklist**: queries ALWAYS parameterised (Eloquent/Doctrine/PDO109 prepared) — concatenating input into SQL is forbidden, even in `whereRaw`/DQL: use bindings.110 Context-aware escaping: Blade `{{ }}` / Twig autoescape; `{!! !!}`/`|raw` only with sanitised111 content and justified in writing.112- **Deserialisation**: never `unserialize()` on external input (use `json_decode` with validation113 or `allowed_classes: false` if there is no alternative). Watch out for SSRF in HTTP clients that receive114 user-supplied URLs: validate scheme/host against an allowlist.115- **Authentication/authorisation**: framework primitives (Laravel `Auth`/policies/Sanctum;116 Symfony Security/voters), never home-made. `password_hash()` with Argon2id/bcrypt; comparisons117 with `hash_equals()`. Authorise every action server-side, do not just hide UI.118- **Mass assignment**: strict `$fillable` (not `$guarded = []`) in Eloquent; DTOs/Form Requests119 as the input boundary. ALWAYS validate at the edge (Form Request / Validator / Symfony120 Validator), not ad hoc in the controller.121- **Secrets**: only in environment variables / a secrets manager; `.env` outside VCS; nothing in122 logs or exceptions. `APP_DEBUG=false` in production (Laravel exposes secrets with debug on).123- **Headers and session**: `Secure`, `HttpOnly`, `SameSite` cookies; CSRF enabled on forms;124 HSTS; rate limiting on authentication endpoints.125- **Continuous SCA**: `composer audit` in CI + Dependabot/Renovate; abandoned dependencies are126 replaced, not ignored. No extensions/packages with open CVEs without documented mitigation.127- Crypto: `random_bytes`/`random_int`, sodium or OpenSSL AES-GCM. MD5/SHA-1 for128 security, `mt_rand`/`rand` for tokens and home-made encryption are forbidden.129130## 6. Performance and operability131132- **OPcache** always enabled in production; `composer install --no-dev --optimize-autoloader`133 (+ `--classmap-authoritative` on an immutable deploy). Preloading only with measurement to back it.134- **N+1 is a bug**: eager loading (`with()`, joins, `Model::preventLazyLoading()` in non-production;135 Doctrine `fetch join`). Pagination mandatory in listings; never `all()` without a limit.136- Heavy work onto **queues/Messenger** with retries + backoff and monitored `failed_jobs`/failure137 transport; idempotent *jobs* (redelivery happens). Cache with explicit invalidation138 (tags/TTL), not "just in case".139- **PHP-FPM/worker mode**: size `pm.max_children` with data; if you use Octane/FrankenPHP/140 RoadRunner, check for state leaks between requests (statics, container).141- Observability: structured logs (Monolog JSON) with context and without sensitive data; metrics and142 traces (OpenTelemetry) in services; health checks (`/up`, liveness/readiness) for the orchestrator.143- Backward-compatible DB migrations (*expand/contract*); never a destructive migration in the144 same deploy as the code that stops using the column.145146## 7. Sustainability: cadence and prohibitions147148**Upgrade cadence**:149- PHP: move up a minor version within <6 months of release; abandon a line BEFORE it enters150 *security-only*. Cite the EOL in the plan (endoflife.date/php).151- Laravel: annual major — budget the upgrade every year (Shift/Rector help); do not fall more152 than one major behind the current one. Symfony: jump from LTS to LTS (7.4 → 8.4) or follow the six-monthly cadence,153 an explicit project decision.154- Dependencies: Renovate/Dependabot weekly; security patches in <72 h.155156**LIST OF PROHIBITIONS** (they block review):157- A new file without `declare(strict_types=1)`.158- `eval()`, `extract()`, `$$variables` variable variables, `@` (error suppression), `goto`.159- `exec`/`shell_exec`/`system`/`proc_open` with unsanitised input; backticks.160- SQL/commands concatenating input; `unserialize()` of external data.161- `env()` outside `config/` (Laravel); hardcoded secrets; `APP_DEBUG=true` in production.162- `mixed` without justification; `array` without a shape/generic docblock in public APIs.163- Suppressing PHPStan/Psalm errors without a comment giving the reason; growing the baseline.164- `dd()`/`dump()`/`var_dump()`/`print_r()` in production code.165- Tests that depend on a real network, the system clock without a fake, or execution order.166- New dependencies without justification (does the framework or the stdlib solve it?); abandoned packages.167- Inheritance as code reuse (use composition); *service location* (`app()->make` in the168 domain) instead of constructor injection.169- Commits that mix mass reformatting with functional changes.170171## 8. Mandatory web verification172173Before pinning ANY version, flag or API in a real project, **search for it on the web** — do not174take it as good from this file or from memory:175- Supported PHP versions and EOL dates: php.net/supported-versions, endoflife.date/php.176- Version and support policy of Laravel (laravel.com/docs/releases) and Symfony177 (symfony.com/releases — confirm which is the current LTS).178- Compatibility of PHPStan/Psalm/Pest/PHPUnit with the project's PHP version (Packagist).179- Dependency CVEs: `composer audit` + GitHub Advisories before recommending a package.180181If the web's data contradicts this document, **the web wins** — mention the discrepancy.