| name | phpstan-analysis |
| description | Invoke BEFORE running PHPStan or fixing PHPStan errors. Covers error resolution strategy (refactoring > phpDoc > ignoring), common Nette error patterns, baseline management, and type tests. Use this whenever the user mentions PHPStan, static analysis, type errors, wants to suppress warnings, or manage the baseline - even for a single error. |
PHPStan Analysis
Running PHPStan
vendor/bin/phpstan analyse
vendor/bin/phpstan analyse src/foo/ src/bar.php
vendor/bin/phpstan analyse --generate-baseline
Never use --error-format=json - its output format can change between PHPStan versions and is not designed for stable machine consumption. For machine-readable output, use --error-format=raw.
Target Levels
Target level for Nette libraries is 8. Levels higher than 8 are not worth pursuing - the additional strictness (e.g., non-empty-string, positive-int) catches very few real bugs relative to the annotation burden.
- Level 7: Union types checked
- Level 8: Null checks, strict types (our target)
nette/phpstan-rules
Installed by all Nette libraries. Transparently narrows types and silences false positives — many PHPStan errors disappear without manual fixes. Don't add asserts, casts, or @var for errors that fall into these categories:
- Nette helpers:
Strings::match(), Arrays::invoke(), Helpers::falseToNull(), Expect::array(), Html magic methods (setXxx/getXxx/addXxx), Container::getComponent() and $this['name'], Form $form['name']
- Native PHP functions:
|false / |null removed where unrealistic (getcwd, json_encode, preg_*, intl/GD/DOM/etc.)
- For
preg_* with the u modifier, false can genuinely occur on invalid UTF-8 input, yet it is stripped by design — a regex call site should not double as input validation. So a dead === false check after preg_* (identical.alwaysFalse) is not a bug in the extension: refactor to an explicit up-front preg_match('##u', $s) validation, the one form where false is preserved.
- After
Tester\Assert: notNull(), type(), true(), etc. narrow the type
- Silenced false positives: arrow fns passed to
test() / Assert::exception(), runtime variadic-closure type validation, Form event-handler callbacks with narrow data parameter
Two features require config in phpstan.neon (NOT app's common.neon):
Database row mapping — narrows Explorer::table(), ActiveRow::related(), ::ref() to concrete row classes. Keys may contain a single * wildcard; a bare * is the catch-all and substitutes PascalCase of the table name into * in the value. Exact keys win over wildcards; wildcards are tried in declaration order.
parameters:
nette:
database:
mapping:
tables:
booking: App\Entity\BookingRow # exact match
event_*: App\Entity\Event*Row # event_video → EventVideoRow
*: App\Entity\*Row # catch-all fallback
Asset type narrowing — narrows Registry::getMapper() / getAsset() / tryGetAsset() (and FilesystemMapper::getAsset() / ViteMapper::getAsset()) based on mapper ID and file extension. Values file and vite are shortcuts for the built-in FilesystemMapper / ViteMapper; any other value is treated as an FQCN of a custom mapper class.
parameters:
nette:
assets:
mapping:
default: file # FilesystemMapper
images: file
vite: vite # ViteMapper
custom: App\MyMapper # custom mapper FQCN
Full reference: https://doc.nette.org/en/best-practices/phpstan-rules
Error Resolution Strategy
Resolution Priority
Resolve every error in this order of preference. Only fall back to the next step when the current one genuinely doesn't apply — this ladder is the backbone of the whole skill:
- Refactoring - if an error reveals a design weakness, fix the design first
- phpDoc - if the code is correct but its types are imprecise
assert() - sparingly, only when the type cannot be expressed otherwise
- Ignore in
phpstan.neon - for systematic or intentional patterns, always with a comment explaining why
- Baseline - last resort, keep minimal
Two hard rules override the ladder at every step:
- Never silence errors - a fix must not hide a potential problem (see "Never Silence Errors" below).
- Never use
@phpstan-ignore annotations - keep checker-specific directives out of source code; ignore in phpstan.neon instead.
Create a Plan First
Before making any changes, create a plan:
- Group errors by type (
property.nonObject, method.notFound, new.static, etc.)
- For each type, choose a resolution following the priority order above
- Justify each decision with clear reasoning
- Present the plan before implementing
Refactoring as First Choice
Always ask: does this error reveal a real design issue? Examples:
- Overly broad return types - method returns
mixed or object but always returns a specific type; narrow the return type
- Interface too loose - code calls a method on implementation but not on interface; extend the interface
- Mixed responsibilities - class handles too many types; split it
- Unnecessary dynamic access -
__get/__set where typed properties would work
The goal is not to "make PHPStan happy" but to use its feedback as a catalyst for better code.
Code Fixes Guidelines
Never Silence Errors
The code worked before. A fix that hides an error degrades code quality.
function foo(): string {
return json_encode($this->value);
}
function foo(): string {
return (string) json_encode($this->value);
}
Better solutions, in order: use Json::encode(), or add an explicit check that throws. Only when neither applies, fall back to the baseline (last resort, per the resolution priority).
Throw Expression Pattern
$f = fopen($file, 'r');
$f = fopen($file, 'r') ?: throw new IOException("Cannot open file $file");
Beware of /** @var */ in Method Bodies
/** @var Type */ in method body is taken authoritatively by PHPStan - it completely disables type checking for that variable. Use only when no better solution exists.
Don't "refine" bare callable into callable(...mixed): mixed
When PHPStan reports a missing callable signature (missingType.callable), it's tempting to write callable(...mixed): mixed. Don't. That type is narrower than bare callable, not wider, because of parameter contravariance:
callable(...mixed): mixed claims the callee may be invoked with any arguments, so only callbacks whose parameters are all mixed-compatible (or which have no required typed params) satisfy it.
- A normal callback like
function (UiForm $form, mixed $value): void {} is then rejected at call sites: expects callable(mixed...): mixed, Closure(UiForm, mixed): void given.
Bare callable is PHPStan's top type for callables — it accepts anything invokable regardless of signature, and $cb(...$args) inside the function still type-checks. So for "invoke arbitrary user callbacks" APIs keep callable (e.g. @param iterable<callable> $callbacks) and, if missingType.callable fires, ignore it for that file in phpstan.neon with a comment. An explicit signature buys nothing and introduces false positives. (Confirmed in nette/utils Arrays::invoke().)
The same applies to bare \Closure — when the value is guaranteed to be a closure (stored property, result of Closure::fromCallable(), etc.) but its signature is unknown or intentionally polymorphic, use plain \Closure without parameters. missingType.callable then fires on it too and is ignored on the same grounds.
Readability tip — wrap typed callables in parentheses. When a callable/Closure type has a signature and appears in a union or alongside other type fragments, wrap it in (...) so the reader can see where the signature ends:
The parentheses are purely cosmetic for PHPStan (it parses both forms identically), but they save the reader from re-reading the line.
Beware of ?: operator with falsy values
The ?: operator treats 0, 0.0, '0', '', [], null, and false as falsy. This is dangerous when the value can legitimately be '0' or 0:
$result = $value ?: $default;
$result = $value ?? $default;
phpDoc and Type Annotations
For phpDoc conventions (when to skip docs, array types, writing style), see the php-doc skill.
Key rules specific to PHPStan compatibility:
- phpDoc type must always match the native type:
| Native Type | Wrong phpDoc | Correct phpDoc |
|---|
array|string | mixed[] | mixed[]|string |
array|null | int[] | int[]|null |
object|array | stdClass | stdClass|array |
- Don't use overly granular types (
positive-int, non-empty-string, non-empty-array, non-falsy-string) - they rarely catch real bugs but add significant annotation maintenance burden
- Use
class-string<T>, array<string, Foo>, list<int>, array{name: string, age: int} - these are useful for PHPStan and worth maintaining
Array notation preference:
foo[] - always prefer for simple types (shortest notation)
array<foo|bar> - for union types (more readable than (foo|bar)[])
array<string, foo> or list<foo> - when keys are not generic
Common Nette Error Patterns
property.nonObject - property access on array|object
In DI Extensions, $this->config returns array|object but is stdClass. This is one of the legitimate cases for /** @var */ in method body - the type cannot be expressed otherwise:
public function loadConfiguration(): void
{
$config = $this->config;
}
method.notFound / staticMethod.notFound / arguments.count
Calling method on interface that exists only on implementation. Fix type if possible, or use assert():
$component = $container->getComponent($name);
assert($component instanceof Component);
$component->saveState($params);
Note: when accessing components via $this['name'] or $this->getComponent('name') with a constant string and a matching createComponent<Name>() factory on the same class, phpstan-rules narrows the type automatically — no assert needed. The assert pattern above applies when the name is dynamic or the factory lives elsewhere.
property.uninitializedReadonly / property.readOnlyAssignNotInConstructor
Readonly properties initialized via inject methods (Nette DI pattern). Ignore - this is an intentional framework pattern.
new.static - unsafe usage of new static()
If intentional design pattern (derive/factory methods), ignore in phpstan.neon. Or change to new self if subclassing isn't expected.
closure.unusedUse
Variable in use ($var) used in require'd file. Ignore - false positive.
function.alreadyNarrowedType
PHPStan knows the type is already narrowed. Remove unnecessary condition, or ignore if it serves as runtime validation.
catch.neverThrown
Verify if the catch is actually needed. If so, ignore.
Ignoring Errors
Ignoring sits at the bottom of the resolution ladder (see "Resolution Priority") — exhaust refactoring, phpDoc, and assert() first. When ignoring is genuinely the right call, prefer phpstan.neon for systematic or intentional patterns (always with a comment) over phpstan-baseline.neon (last resort, minimize).
Forbidden and Discouraged Suppressions
These rules apply to both phpstan.neon ignoreErrors and phpstan-baseline.neon.
MUST NOT be suppressed — always fix:
phpDoc.parseError — broken phpDoc syntax. Suppressing it leaves the phpDoc permanently unparseable; fix the syntax.
argument.templateType — a generic template parameter cannot be inferred from arguments. The template is either misdesigned or redundant; redesign the generic or drop the template parameter.
- Anything in
tests/types/* — these files are the library's type contract (TypeAssert / assertType). Suppressing an error here silently invalidates the contract and defeats the purpose of type tests.
SHOULD NOT be suppressed — fix unless truly unavoidable:
missingType.* (e.g. missingType.iterableValue, missingType.parameter, missingType.return) — the type is almost always expressible (array<…>, concrete class, list<>, array{…}). Suppress only when the type genuinely cannot be expressed. Exception: missingType.callable for "invoke arbitrary user callbacks" APIs (see "Don't refine bare callable" above) — that one is legitimate.
parameter.phpDocType — phpDoc type doesn't match native type. This is almost always a real documentation bug; fix the phpDoc rather than hide it.
Systematic Patterns in phpstan.neon
Target ignores narrowly — never write a blank check. An entry like
# WRONG — blanket suppression hides every future method.notFound in this file
- identifier: method.notFound
path: src/Forms/Controls/SubmitButton.php
silences not only the intended pattern but every future legitimate method.notFound in that file — including real typos and broken refactorings.
The model pattern: one bullet = one specific phenomenon
Each entry should describe a single concrete phenomenon — a specific message in a specific scope — not "ignore identifier X in file Y". Constraining tools, in order of strength:
message: — regex matching the exact error message. The strongest safeguard: protects against future drift of different errors with the same identifier in the same file. Always include it when the message is reasonably stable.
count: — pin the number of occurrences. If a new instance appears (or one disappears), PHPStan reports a mismatch and the developer is forced to look.
identifier: — narrows to one error kind.
- Path scoping — three forms, choose the most specific:
path: src/Foo/Bar.php — single file (preferred when the phenomenon lives in one file)
path: src/Foo/Bar/* — wildcard mask for a directory tree (when the same phenomenon recurs across siblings)
paths: [...] — explicit list of files (when the phenomenon lives in a handful of unrelated files)
Important: count: requires path: (singular), not paths: (plural). With a wildcard mask in path: you still get count: — that's the compact form for "this phenomenon appears N times across this tree". With paths: you sacrifice count: for enumeration; compensate with a tight message: regex.
Canonical examples
parameters:
ignoreErrors:
# One phenomenon across a directory tree (wildcard path + count)
- # Latte nodes use new static() by design for extensibility
identifier: new.static
path: src/Bridges/FormsLatte/Nodes/*
count: 6
# One phenomenon across an explicit list of files (paths array + tight message regex)
- # parent::getControl()/getLabel() returns the wider public contract (Html|string|null),
# but BaseControl implementation deterministically returns Html, so chaining is safe.
identifier: method.nonObject
message: '#^Cannot call method \w+\(\) on Nette\\Utils\\Html\|string(\|null)?\.$#'
paths:
- src/Forms/Controls/Checkbox.php
- src/Forms/Controls/CheckboxList.php
- src/Forms/Controls/RadioList.php
# One phenomenon in one file (single path + message + count — strongest form)
- # SubmitButton::getScopeForValidation() walks getParent() which is typed as
# Container|Control; lookupPath() exists on Container at runtime.
identifier: method.notFound
message: '#^Call to an undefined method Nette\\Forms\\Container\|Nette\\Forms\\Control::lookupPath\(\)\.$#'
path: src/Forms/Controls/SubmitButton.php
count: 1
Canonical reference: nette/forms phpstan.neon follows this pattern throughout — use it as the template when refactoring other configs.
Always include a comment explaining why the error is ignored.
Baseline
vendor/bin/phpstan analyse --generate-baseline
Use only for false positives that are not systematic, or individual cases where fix requires BC break.
phpstan.neon Structure
parameters:
level: 8
paths:
- src
excludePaths:
- src/compatibility.php
# other files for historical compatibility
ignoreErrors:
# systematic patterns with comments
includes:
- phpstan-baseline.neon
Exclude files for backward compatibility with historical versions (compatibility.php, Latte 2 support, etc.).
Type Tests
Files in tests/types/*.php verify that types in the library are defined correctly.
Purpose:
- Guarantee that the library won't cause type problems for users
- Protect against unintended type changes during refactoring
- Especially important for complex generics
These tests must always pass and must never be ignored.
Using TypeAssert (from nette/phpstan-rules):
use Nette\PHPStan\Tester\TypeAssert;
TypeAssert::assertTypes(__DIR__ . '/data/types.php');
TypeAssert::assertNoErrors(__DIR__ . '/data/clean.php');
Data file with assertType:
use function PHPStan\Testing\assertType;
assertType('non-empty-string', getcwd());
assertType('string', Normalizer::normalize('foo'));
Workflow
- Run PHPStan and get list of errors
- Understand the project - relationships between classes are essential
- Exclude files for historical compatibility
- Create a plan grouping errors by type with justification for each strategy
- Refactor code where error reveals a design improvement
- Fix phpDoc where code is correct but types are imprecise
- Add assert() where necessary to communicate type to PHPStan
- Ignore in phpstan.neon systematic patterns with a comment
- Generate baseline for the rest (minimize)
- Verify that tests pass