From nette-dev
Guides PHPStan error resolution prioritizing refactoring over phpDoc and ignoring, with Nette patterns, baseline management, and type tests. Use before running PHPStan or fixing errors.
npx claudepluginhub nette/claude-code --plugin nette-devThis skill uses the workspace's default tool permissions.
```bash
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Checks Next.js compilation errors using a running Turbopack dev server after code edits. Fixes actionable issues before reporting complete. Replaces `next build`.
# Run with project configuration
vendor/bin/phpstan analyse
# Run on specific paths
vendor/bin/phpstan analyse src/foo/ src/bar.php
# Generate baseline for legacy projects
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 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.
All Nette libraries use nette/phpstan-rules. It provides:
Strings::match(), matchAll(), split(), Arrays::invoke(), etc.|false and |null from PHP functions where error values are unrealistic (getcwd, json_encode, preg_split, etc.)Tester\Assert calls (notNull(), type(), true())Before making any changes, create a plan:
property.nonObject, method.notFound, new.static, etc.)@phpstan-ignore annotations - keep checker-specific directives out of source codeassert() sparingly, only when type information cannot be expressed otherwisephpstan.neon with a comment explaining whyAlways ask: does this error reveal a real design issue? Examples:
mixed or object but always returns a specific type; narrow the return type__get/__set where typed properties would workThe goal is not to "make PHPStan happy" but to use its feedback as a catalyst for better code.
The code worked before. A fix that hides an error degrades code quality.
// Before - json_encode returns false on error and we find out (type error)
function foo(): string {
return json_encode($this->value);
}
// WRONG - error is hidden
function foo(): string {
return (string) json_encode($this->value);
}
Better solutions: use Json::encode(), add explicit check with exception, or ignore in baseline.
// Before - fopen can return false
$f = fopen($file, 'r');
// Correct fix
$f = fopen($file, 'r') ?: throw new IOException("Cannot open file $file");
/** @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.
For phpDoc conventions (when to skip docs, array types, writing style), see the php-doc skill.
Key rules specific to PHPStan compatibility:
| Native Type | Wrong phpDoc | Correct phpDoc |
|---|---|---|
array|string | mixed[] | mixed[]|string |
array|null | int[] | int[]|null |
object|array | stdClass | stdClass|array |
positive-int, non-empty-string, non-empty-array, non-falsy-string) - they rarely catch real bugs but add significant annotation maintenance burdenclass-string<T>, array<string, Foo>, list<int>, array{name: string, age: int} - these are useful for PHPStan and worth maintainingArray 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 genericproperty.nonObject - property access on array|objectIn 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
{
/** @var \stdClass $config */
$config = $this->config;
}
method.notFound / staticMethod.notFound / arguments.countCalling 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);
property.uninitializedReadonly / property.readOnlyAssignNotInConstructorReadonly 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.unusedUseVariable in use ($var) used in require'd file. Ignore - false positive.
function.alreadyNarrowedTypePHPStan knows the type is already narrowed. Remove unnecessary condition, or ignore if it serves as runtime validation.
catch.neverThrownVerify if the catch is actually needed. If so, ignore.
parameters:
ignoreErrors:
- # Intentional design pattern for derive() and factory methods
identifier: new.static
paths:
- src/Caching/Cache.php
- src/Bridges/CacheLatte/Nodes/CacheNode.php
- # Runtime validation for untyped array input
identifier: function.alreadyNarrowedType
path: src/Caching/Cache.php
Always include a comment explaining why the error is ignored.
vendor/bin/phpstan analyse --generate-baseline
Use only for false positives that are not systematic, or individual cases where fix requires BC break.
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.).
Files in tests/types/*.php verify that types in the library are defined correctly.
Purpose:
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'));