PHP static analysis is the highest-return investment most teams have not fully made. A type error that PHPStan catches during a pull request costs seconds; the same error caught by a customer costs an incident review. Because PHP is gradually typed, the runtime enforces only what you declare, and a static analyser can enforce vastly more: nullability you forgot to handle, array shapes you assumed, dead branches, impossible comparisons, and contracts the engine never checks. This post covers the practical adoption path: strict types as the foundation, PHPStan levels and baselines for existing codebases, PHPDoc generics for the types PHP cannot express natively, and the CI wiring that makes it permanent.
Start with declare(strict_types=1)
Without declare(strict_types=1), PHP coerces scalar arguments: pass the string "5" to an int parameter and the engine quietly converts it. With strict types, the same call throws a TypeError, which sounds harsher but is strictly better, because the coercion path hides bugs (think "5 apples" becoming 5 in older semantics, or floats silently truncating). Strict mode also gives static analysers firmer ground: a declared type is now a guarantee, not a suggestion.

<?php
declare(strict_types=1);
namespace App\Billing;
final class Vat
{
public function amountFor(int $netCents, float $rate): int
{
return (int) round($netCents * $rate);
}
} Add the declaration to every new file via your editor template and enforce it with a coding-standard rule. For existing files, add it opportunistically when touching them, and be alert the first time each file converts: strict types can surface real latent bugs, which is the point.
PHPStan levels: a ratchet, not an exam
PHPStan grades strictness from level 0 (only the grossest errors: unknown classes, unknown functions) to level 10, with each level adding checks: undefined properties, nullability violations, missing type hints, dead code, and eventually full mixed-type strictness. The level system is the adoption mechanism. Nobody drops level 9 onto a legacy codebase and survives the ten thousand errors; instead you find the highest level that passes today and ratchet upward:
# phpstan.neon
parameters:
level: 6
paths:
- src
- tests Level 6, which requires type annotations for parameters, properties and returns, including the iterable value types of arrays, is where analysis starts paying serious dividends, and is a realistic medium-term target for most teams. Greenfield projects should simply start at the top level from the first commit, where staying strict is nearly free.
Baselines: adopting analysis on a legacy codebase
The feature that makes PHPStan adoptable on ten-year-old code is the baseline. Running phpstan analyse --generate-baseline records every current error into a file that the analyser then ignores, so your existing debt is frozen while all new and modified code is held to the configured level. The build goes green today, and the codebase stops getting worse immediately.

Treat the baseline as a debt ledger, not a landfill. Two habits keep it honest: regenerate it only deliberately (never to sneak new errors past CI), and shrink it on a schedule, a recurring chore ticket to fix twenty baseline entries, or a rule that touching a file means clearing its baseline lines. Teams that do this watch thousands of entries evaporate over a year without a dedicated refactoring project.
Generics: the types PHP cannot say, PHPDoc can
PHP's native syntax cannot express "an array of Order objects" or "a repository of T", but PHPStan reads a rich type language in PHPDoc, and modern PHP libraries lean on it heavily. The two highest-value constructs are typed iterables and array shapes:
/**
* @param list<Order> $orders
* @return array<string, Money> totals keyed by currency code
*/
public function totalsByCurrency(array $orders): array { /* ... */ }
/**
* @param array{street: string, city: string, zip?: string} $address
*/
public function ship(array $address): void { /* ... */ } Once parameter and return collections are annotated, PHPStan follows values through foreach loops, closures and array functions, and an astonishing class of "expected object, got null" production errors becomes impossible to merge. Template annotations (@template T) extend the same power to reusable abstractions like repositories and collections. Extensions matter here too: phpstan/phpstan-strict-rules tightens general behaviour, and Larastan or the Symfony and Doctrine extensions teach the analyser your framework's magic so you are not drowning in false positives.
Make it permanent: CI and culture
Static analysis only changes outcomes when it blocks merges. The mechanics are a single CI step, with result caching to keep it fast on large codebases:

vendor/bin/phpstan analyse --no-progress --error-format=github The cultural mechanics matter more. A few norms keep the tool an ally rather than an adversary:
- Errors are fixed, not silenced. Inline
@phpstan-ignorecomments require a justification and stand out in review. - The level only moves up. Raising it is a small celebrated PR; lowering it needs a team decision.
- Annotations are code. A wrong PHPDoc is a lie the analyser believes; review generics with the same care as logic.
- Run it locally. Editor integration or a pre-push hook means CI failures are rare surprises, not routine friction.
Two companions round out the setup. Rector automates mechanical type-adding across a codebase, turning weeks of annotation drudgery into a reviewed diff. And your test suite remains essential: static analysis proves the types line up, tests prove the behaviour is right, and neither substitutes for the other.
The takeaway
The progression is well-trodden by now: strict types in every file, PHPStan at whatever level passes, a baseline to freeze legacy debt, a ratchet upward, and generics annotations on the collections that flow through your core paths. Each step is unglamorous; the compound effect is a codebase where a whole category of runtime error, the null that was not handled, the array shape that changed, the method that does not exist, gets caught by a machine in seconds, every time, before a human ever reviews the diff. Cheap insurance rarely comes cheaper.
Related Service
๐ป Web Development
Custom websites and web applications built with PHP, Laravel, WordPress, and React by a Chandigarh & Mohali based web developer โ fast, secure, scalable, and trusted by clients across India and worldwide.
Explore Web Development →
Reviews & Comments
Reviews are moderated and appear after approval.
No reviews yet โ be the first to share your thoughts.
Leave a Review