skip to content

PHPStan

PHPStan finds type and logic errors in PHP without running it, dialled through numbered rule levels, with a baseline and PHPDoc types for legacy code. Asked as the standard way into old code.

on this pageshow

explore

questions

10

In PHPStan 2, what are rule levels 0 to 10, and what do the main levels add as you move up?

level: juniorimportance: must knowfreq 58%

answer

  1. cumulative strictness dial
  2. default 0; --level or level: in neon
  3. 5: argument types; 6: missing typehints
  4. 8: nullable; 9: explicit mixed
  5. 10 (new in 2.0): implicit mixed

basics

~20 s

PHPStan's rule levels are a cumulative strictness dial from 0 (unknown classes, functions, wrong argument counts) to 10 (errors even for implicit mixed); each level adds checks to all lower ones, and max is an alias for the highest.

solid answer

~40 s

A rule level selects which checks PHPStan runs, and levels are cumulative: level 5 includes everything from 0 to 4. The default on the command line is 0. Level 0 reports unknown classes and functions, unknown methods on `$this`, wrong argument counts and always-undefined variables; 2 checks methods on all expressions and validates PHPDocs; 3 checks return types and property assignments; 5 checks argument types; 6 reports missing typehints; 7 partially wrong union types; 8 method calls and property access on nullable types; 9 is strict about explicit `mixed`. PHPStan 2.0 added level 10, which also treats a missing type as implicit `mixed` and reports unsafe use of it. `--level max` always means the highest level, which in 2.x is 10.

code

bash · 5 lines
bash
# run level 6 over two directories
vendor/bin/phpstan analyse --level 6 src tests

# always the highest level of the installed PHPStan (10 in 2.x)
vendor/bin/phpstan analyse --level max src

go deeper

for a junior

Know that levels run from 0 to 10, are cumulative, and that the default is 0. Be able to name one thing level 0 catches, such as an unknown class.

for a middle

Place the key checks: argument types at 5, missing typehints at 6, nullable access at 8, explicit mixed at 9, implicit mixed at 10.

for a senior

Explain why level 6 and level 10 cause big jumps on legacy code, and how --level on the CLI and level: in the config interact.

for a principal

Judge whether to pin a numeric level or use max, weighing automatic strictness against upgrade cost across many repositories.

## What a rule level is **PHPStan** is a static analyser for PHP: it reads your code without running it and reports errors such as calling an undefined method or passing a string where an `int` is declared. A **rule level** is how you choose how strict it is. In PHPStan 2 there are **11 levels, 0 to 10**, selected with `--level` (`-l`) on the command line or `level:` in `phpstan.neon`. Levels are **cumulative**. Internally each level's config file includes the one below it (`config.level6.neon` includes `config.level5.neon`, and so on), so running level 6 gives you every check from levels 0 to 5 plus the new ones. ## What each level adds | Level | Adds | |---|---| | 0 | unknown classes and functions, unknown methods called on `$this`, wrong number of arguments, always-undefined variables | | 1 | possibly undefined variables, unknown magic methods and properties on classes with `__call` and `__get` | | 2 | unknown methods on all expressions, not just `$this`; PHPDoc validation | | 3 | return types, types assigned to properties | | 4 | basic dead code: always-false `instanceof` and type checks, dead `else` branches, unreachable code after `return` | | 5 | types of arguments passed to functions and methods | | 6 | missing typehints | | 7 | partially wrong union types, such as calling a method that exists on only some types in a union | | 8 | calling methods and accessing properties on nullable types | | 9 | strict about explicit `mixed`: you may only pass it to another `mixed` | | 10 | also strict about implicit `mixed`, meaning values with no type at all (new in PHPStan 2.0) | ## The levels interviewers ask about most - **Level 5** is where argument type errors appear. Below it, `strlen(42)`-style mismatches go unreported, which surprises people who assume PHPStan checks types from the start. - **Level 6** is the jump that hurts on legacy code: every parameter, return and property without a type becomes an error, so the count often goes up by thousands. - **Level 8** reports calling a method on something that might be `null`, such as the result of a repository `find()` that returns `?User`. This is where many real production bugs are caught. - **Level 9 versus 10**: level 9 (internally `checkExplicitMixed`) only restricts values declared as `mixed`. Level 10 (`checkImplicitMixed`) extends that to values whose type is simply unknown, for example the return of a third-party function with no return type, or a parameter left untyped and hidden from level 6 by a baseline. ## Defaults and the max alias 1. With no config file and no `--level`, PHPStan runs **level 0**. 2. With a config file, the level must come from `--level` or the `level` parameter. If neither is set, PHPStan stops with *No rules detected* and explains the choices. 3. A `--level` on the command line wins over `level:` in the config. 4. `--level max` (or `level: max`) always selects the highest level the installed PHPStan has. In 2.x that is 10. The docs warn about the last one: pinning `max` means a PHPStan upgrade that adds a level, as 2.0 did with level 10, can turn a green build red. ## How the levels are meant to be used - The levels exist for **incremental adoption**. You start where the error count is manageable, reach zero, merge, and raise the level one step at a time. - A level is a floor for the whole analysed code, not a score. Two codebases at level 6 have the same checks enabled, not the same quality. - For strictness beyond level 10, PHPStan points to the separate `phpstan-strict-rules` extension, Bleeding Edge, and extra config options such as `checkUninitializedProperties`. In an interview, knowing where argument types (5), missing types (6), nullability (8) and `mixed` (9 and 10) come in is usually what separates someone who has used the levels from someone who has only heard of them.

  • Why can a codebase clean at level 9 show new errors at level 10?
    Level 9 only restricts values explicitly typed `mixed`. Level 10 treats values with no type at all, such as returns from untyped third-party functions or parameters whose missing types were baselined, as implicit `mixed` and reports unsafe operations on them.
  • What happens if phpstan.neon exists but sets no level and you pass no --level?
    PHPStan stops with a 'No rules detected' message. The default level 0 applies only when no config file is used; with a config you must set `level` in it or pass `--level`, or declare a custom ruleset with `customRulesetUsed: true`.
  • Is --level max a good setting for CI?
    It keeps you at the strictest level automatically, but the docs warn it can make PHPStan upgrades expensive: when a new major release adds a level or new checks at the top, CI turns red until the new errors are fixed. Pinning a number makes upgrades a separate, planned step.

saying these in an interview costs you the question

  • Level 0 already checks argument types against declarations
  • Each level replaces the previous one's checks
  • Level 9 is the highest level in PHPStan 2
  • The default level is max when no level is given
  • Level 10 is about coding style, not types
open as a page

In PHPStan, how do you generate a baseline, what does each entry record, and how does the baseline shrink as errors get fixed?

level: middleimportance: must knowfreq 50%

basics

~20 s

vendor/bin/phpstan analyse --generate-baseline writes phpstan-baseline.neon, an ignoreErrors list with message, identifier, count and path per file, which you include in phpstan.neon; fixed errors leave entries unmatched, PHPStan reports them, and you regenerate to shrink it.

open as a page

In a phpstan.neon file, what do level, paths, excludePaths and includes do, and how do command-line arguments interact with them?

level: middleimportance: must knowfreq 45%

basics

~20 s

Under parameters, level sets the rule level, paths lists what to analyse and excludePaths removes fnmatch patterns; includes pulls in other config files. A --level or paths given on the command line replace the config values rather than merging with them.

open as a page

In PHPStan, what do the PHPDoc types list<T> and array{...} shapes express that PHP's native array type cannot?

level: juniorimportance: should knowfreq 42%

basics

~20 s

Native array only says the value is an array. list<T> means keys 0, 1, 2 with no gaps and values of type T; array{id: int, name?: string} describes each key and its type, optional keys included.

open as a page

In PHPStan, how should a plain string from config become a class-string: narrowed with class_exists() or forced with an inline @var?

level: middleimportance: should knowfreq 30%

basics

~20 s

Narrow it: after if (class_exists($name)) PHPStan treats $name as class-string, and the check also protects runtime. An inline @var is trusted without proof, so the docs call it a last resort and prefer fixing the type at its source.

open as a page

In PHPStan 2, how do you ignore one error with @phpstan-ignore and an identifier, and how does that differ from ignoreErrors in phpstan.neon?

level: middleimportance: should knowfreq 40%

basics

~20 s

Put // @phpstan-ignore argument.type (reason) on or above the line, naming the error identifier; it silences only that error there. ignoreErrors in phpstan.neon matches by message regex or rawMessage, identifier, path and count across files.

open as a page

How does PHPStan's result cache decide what to re-analyse, and how do you keep it effective in CI using tmpDir?

level: middleimportance: should knowfreq 28%

basics

~20 s

PHPStan stores the last result and a file dependency tree in %tmpDir%/resultCache.php and re-analyses only changed files and files that reference their symbols; setting tmpDir inside the workspace lets CI save and restore that cache between runs.

open as a page

How would you use PHPStan's @template to type a collection class so that its elements keep their type through add(), get() and foreach?

level: seniorimportance: should knowfreq 35%

basics

~20 s

Declare @template T above the class, use T in @param and @return tags, declare @implements IteratorAggregate<int, T> for foreach, and type usages as Collection<User>; PHPStan then infers User from get() and foreach and rejects adding other types.

open as a page

What do PHPStan's Bleeding Edge and the phpstan-strict-rules extension each add beyond rule levels, and how do you enable them?

level: seniorimportance: should knowfreq 30%

basics

~10 s

Bleeding Edge, enabled by including phar://phpstan.phar/conf/bleedingEdge.neon, turns on the next major version's rules and behaviour early; phpstan-strict-rules is an opinionated extension of extra checks, enabled through phpstan/extension-installer or by including its rules.neon.

open as a page

You are introducing PHPStan into a 300,000-line legacy PHP codebase; how do you choose the starting level and configure the first runs?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Start at level 0 over your own code only, fix configuration noise such as unknown classes with scanDirectories, bootstrapFiles and framework extensions, get to zero, gate CI there, then raise one level at a time, expecting level 6 to be the big jump.

open as a page