nette/claude-code/phpstan-analysis

Trust tier 2
  • community
LLM review pending
Source check pending

Checking that the SKILL.md file is still in the repository.

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.

Install

npx vskill@latest install nette/claude-code/phpstan-analysis

SKILL.md

PHPStan Analysis

Running PHPStan

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.

Take the verdict from the exit code, never from grepping the output. A filter defines what you see, so anything it misses reads as success - and some findings (an unmatched ignoreErrors pattern, for one) are not tied to any file and carry no file prefix at all.

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. It works in both directions, and mixing them up is a real hazard:

  • It narrows types and silences false positives — many PHPStan errors disappear without

manual fixes. Don't fight those (list below).

  • It also REPORTS errors of its own and turns on stricter reporting. Two rules of its own:

nette.abortException (an AbortException caught and not rethrown — the fix is to rethrow it in its own catch, which is not guessable) and nette.strings.regexpPattern (an invalid regular expression). On top of that extension.neon forces nine flags on, including checkMissingCallableSignature, reportMaybesInMethodSignatures and reportAlwaysTrueInLastCondition — so some errors exist only because this extension is installed. Those are genuine findings; never silence them.

It also registers universalObjectCratesClasses for DefaultTemplate, Database\Row, SessionSection, SimpleIdentity and Html, so the most common property.notFound in a Nette app is already solved — don't add @property annotations for those.

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.

neon
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.

neon
parameters:
nette:
assets:
mapping:
default: file # FilesystemMapper
images: file
vite: vite # ViteMapper
custom: App\MyMapper # custom mapper FQCN

Full reference: Nette PHPStan Rules, bundled with the plugin.


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:

  1. Refactoring - if an error reveals a design weakness, fix the design first
  2. phpDoc - if the code is correct but its types are imprecise
  3. `assert()` - sparingly, only when the type cannot be expressed otherwise
  4. Ignore in `phpstan.neon` - for systematic or intentional patterns, always with a comment explaining why
  5. 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.
Agree on the Resolutions First

Before changing code, present the errors grouped by identifier, with the rung of the ladder chosen for each group and the reason.

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.

php
// 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, 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
php
// Before - fopen can return false
$f = fopen($file, 'r');
// Correct fix
$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. (nette/utils Arrays::invoke() is an example.)

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.


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 TypeWrong phpDocCorrect 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:

php
public function loadConfiguration(): void
{
/** @var \stdClass $config */
$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():

php
$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:

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 100 of 100Pattern checks on the skill's files.
  • Model reviewNot runA model review is scheduled for the next rescan.

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

Repository stars
37
Repository forks
4
7-day trend score
18

Stars and forks are for the whole GitHub repository, not this skill alone. Refreshed 21 Mar 2026, 7 months ago.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/nette/claude-code/phpstan-analysis/badge)](https://verified-skill.com/skills/nette/claude-code/phpstan-analysis)

Something wrong with this skill or its page? Tell us.