Static analysis catches the type errors PHP would only find at runtime, which is essential in large codebases.
Attributes (PHP 8.0) attach structured metadata to classes, methods, properties, parameters and constants: #[Route('/users', methods: ['GET'])]. An attribute is a class marked #[Attribute]; nothing happens until code reads it through Reflection with getAttributes() and calls newInstance(). Frameworks use them for routing, validation, ORM mapping and dependency injection.
The engine itself understands a few: #[\Override] (8.3), #[\SensitiveParameter] (8.2, hides a value in stack traces), #[\Deprecated] (8.4, emits deprecation notices for your own functions) and #[\AllowDynamicProperties].
PHPStan (and Psalm) analyse code without running it. Install with composer require --dev phpstan/phpstan and run vendor/bin/phpstan analyse src --level 6. Levels go from 0 (basic checks) to 10 (strictest, also treating mixed strictly); raise the level gradually and use a baseline file to adopt it in legacy code.
PHP's native types can't express everything, so analysers read PHPDoc types:
@param list<User> $users,@return array<string, int>,non-empty-string,positive-int.- Array shapes:
array{id: int, name: string}. - Generics:
@template Ton a class or function, then@param class-string<T> $classand@return T.
These annotations are ignored at runtime but make the analyser, and your IDE, understand collections and factories precisely.
Example
<?php
#[Attribute(Attribute::TARGET_METHOD)]
final class Route {
public function __construct(public string $path, public array $methods = ['GET']) {}
}
final class UserController {
#[Route('/users', methods: ['GET', 'HEAD'])]
public function index(): string { return 'list'; }
}
foreach ((new ReflectionClass(UserController::class))->getMethods() as $m) {
foreach ($m->getAttributes(Route::class) as $attr) {
$route = $attr->newInstance();
echo implode('|', $route->methods), " {$route->path} -> {$m->name}\n";
}
}
// GET|HEAD /users -> index
/**
* @template T of object
* @param class-string<T> $class
* @return T
*/
function make(string $class): object { return new $class(); }
$c = make(UserController::class); // PHPStan knows $c is UserControllerRemember
- Attributes are metadata classes read via Reflection
- PHPStan levels 0-10 let you tighten checks gradually
- @template and class-string<T> express generics PHP can't
Watch out for
- Expecting an attribute to do something by itself; it only matters when code reads it.