| Name | Modified | Size | Downloads / Week |
|---|---|---|---|
| Parent folder | |||
| phpstan.phar | 2026-10-06 | 31.2 MB | |
| phpstan.phar.asc | 2026-10-06 | 833 Bytes | |
| 2.3.0 source code.tar.gz | 2026-10-06 | 108.6 MB | |
| 2.3.0 source code.zip | 2026-10-06 | 108.6 MB | |
| README.md | 2026-10-06 | 13.7 kB | |
| Totals: 5 Items | 248.4 MB | 0 | |
This release fixes a whopping 75 issues! π
Read about PHPStan 2.3.0 on the blog Β» I promise it's worth checking it out yourself, and not just letting your agent summarize it for you π It contains a lot of interactive widgets with practical examples of the new features in action.
Major leap in performance. Back in December 2025 with PHPStan 2.1.33, as an example, analysing WordPress core took 60 seconds. With today's PHPStan 2.3.0, it takes under 8 seconds. Similar performance speedups can be observed across many codebases.
PHPStan Turbo speeds up PHPStan by 60% over the no-Turbo baseline.
Bleeding edge πͺ
- Generics: no more scalar generalization, multi-pass inference for
new Foo()from usages, aka "Bidirectional type narrowing" (#6332), [#6732], [#12704], [#10419], [#14647], [#13431], [#12601], [#12490], [#12420], [#11835], [#11435], [#10290], [#10289], [#5741], [#8031], [#10783], [#15147], [#13325] - Detecting unused variables (#6330), [#12789], [#12012], [#11483], [#10202]
- Unused variables: Report variable writes whose value never reaches a use, per array offset (#6334)
- Infer signatures of closures from where their values are sent (#6604, #6622), [#11317], [#3770]
- Apply the effects of a closure's by-ref uses where it is invoked (#6610), [#7751], [#14248]
- Infer the type of a
staticvariable from what the function body does with it (#6617, #6619), [#7066]
If you want to see the shape of things to come and adopt bleeding edge features early, you can include this config file in your project's phpstan.neon:
includes:
- vendor/phpstan/phpstan/conf/bleedingEdge.neon
Of course, there are no backwards compatibility guarantees when you include this file. The behaviour and reported errors can change in minor versions with this file included. Learn more
Improvements π§
- Single-pass inside-out internals rewrite (#5857), [#13944], [#12207], [#7155], [#14396], [#11953], [#12780], [#14999], [#13334], [#15004]
- Implement
@pure-unless-parameter-passed(#6018, #6666), [#11884], thanks @zonuexe! - Check preg_quote delimiters in array patterns (#6474), thanks @arpitjain099!
- Resolve
static::FOO_*andT::FOO_*PHPDoc types once the class is known (#6638), [#4548], [#11510]
Bugfixes π
- Keep property fetches on the receiver when a static method or static closure invalidates it (#6391), [#13735], thanks @staabm and @SanderMuller
- Derive conditional guard types from the branch set difference (#6380), [#13833], [#9685], [#7706], [#14421]
- Build a loop's back edge from its continue statements when the body end is unreachable (#6420), [#14418], [#13959], [#11919], [#1946]
- Widen only the variables a loop can write when its scope converges (#6421), [#12666]
- Infer closure parameter types in array arguments (#6422), [#6430], [#11215]
- Resolve generic parameter types from an array literal argument's skeleton before typing the closures nested in it (#6482), [#15269]
- Preserve conditional expression holders across more scope merges (#6385), [#9628], [#6830]
- Narrow optional array offsets through
isset()conditions (#6382), [#9426], [#6379], [#12786] - Do not read PHP_SBINDIR and PHP_BUILD_PROVIDER off the analysing machine (#6454), thanks @SanderMuller!
- Honour the composer.json
require.phpconstraint inScope::getPhpVersion()(#6476), [#15270], thanks @staabm and @VincentLanglet! - Skip
@phpstan-assert, conditional return types and call effects when the nullsafe chain may short-circuit (#6486), [#15016], [#12925], thanks @VincentLanglet and @staabm! - NativeTypedClassConstantRule is scope php-version aware (#6497), [#13133], thanks @staabm!
- Keep
neverinMutatingScope::intersectButNotNever()instead of widening it tonull(#6504), [#15285] - Resolve "supported only on PHP X" checks from
Scope::getPhpVersion()instead of the injectedPhpVersion(#6496), [#13133], thanks @staabm and @VincentLanglet! - Resolve property, trait constant, arrow function
neverand attribute PHP version checks fromScope::getPhpVersion()(#6510), [#13133], thanks @staabm and @VincentLanglet! - Resolve the printf format parser's PHP version check from
Scope::getPhpVersion()(#6536), thanks @staabm! - Resolve PHP version checks in src/Type/Php extensions from
Scope::getPhpVersion()(#6551), thanks @staabm! - Resolve PHP version checks in remaining scope-aware extensions from
Scope::getPhpVersion()and forbid injectingPhpVersioninto them (#6563), [#15287], thanks @staabm and @SanderMuller! - DeprecatedCastRule: Utilize
Scope::getPhpVersion()(#6600), thanks @staabm! - Do not cast floats past the int range, or NAN, where PHP does not (#6541)
- Ask
isCallable()of callable-like arrays and strings only where the answer is consumed (#6559), [#15292] - Narrow the left side of
??to null in a non-true context only when its other values cannot satisfy it (#6581) - Memoize hasInstanceProperty() into its own cache (#6597)
- Keep an empty array next to an oversized array in a union (#6609)
- Do not report a private method overriding a private trait method as unused (#6191), [#12201], thanks @Jean-Beru!
- Keep the narrowed bound of a template type that
TemplateTypeHelper::resolveTemplateTypes()maps to itself (#6613), [#15321], thanks @staabm! - Do not remember a void equality assertion statement as true when its arguments contain an impure call (#6625), [#15328]
- Compare the element type of a variadic by-ref parameter against its out type (#6227), [#15066], thanks @SanderMuller!
- Keep unresolved template arguments of different sites apart, solve covariant ones like Hack (#6650), [#15331]
Performance ποΈ
- Turbo now shadows majority of the analyser engine (#6499, #6503, #6506, #6508, #6509, #6511, #6521, #6523, #6530, #6532, #6533, #6535)
- Resolve constant offsets on constant arrays through the key index (#6442)
- Speed up enum-case removal, finite type counting, large unions and loop widening of shapes (#6443)
- Memoize
PhpMethodReflection::getName()and return the right operand on aTypeCombinatorCachememo hit (#6489) - Harden Turbo traversal, arena, and iterable parity (#6538)
- Build smaller turbo binaries (#6552)
- Pass immutable array arguments non-refcounted through
zv::Args(#6561), thanks @zonuexe! - Cheaper file cache, type memo bookkeeping and expression-key lookups (#6568)
- Share one NamespaceUses between the name scopes of a file (#6583), [#15304]
- Share one NamespaceUses between the exported PHPDocs of a file (#6585)
- Share one contents string per unchanged file in FileNodesFetcher (#6589), [#15314], thanks @rubas!
- ParentDirectoryRelativePathHelper: Move repetitive work into constructor (#6611), thanks @staabm!
- Make closure signature and static variable inference cheaper (#6623)
- Faster runs with a warm result cache (#6627)
- Do not export functions declared inside functions (#6637)
- Lazier verbosity in
ConstantArrayType->checkOurKeys()(#6649), thanks @staabm! - ConstantArrayType: reduce duplicate work in
checkOurKeys()(#6661), thanks @staabm! - Return dependencies in the results of the handlers (get rid of old DependencyResolver) (#6662)
- Add
TypeTraverser::mapMemoized()(#6652), [#15348], thanks @staabm and @SanderMuller!
Function signature fixes π€
- Declare mutually exclusive constant groups for
json_encode,json_decode,filter_var,filter_input,openlogandZipArchive::openflags (#6571), [#15306], thanks @VincentLanglet! - Generalize keys and values of oversized arrays in
ConstantTypeHelper::getTypeFromValue()likeOversizedArrayBuilder(#6572), [#15305], thanks @staabm! - Fix stale phpredis signatures in functionMap (#6587)
Internals π
- Result cache: value and file dependencies declared on the scope (#6638, #6644, #6659), [#15144], https://github.com/phpstan/phpstan-symfony/issues/455
- Deprecate ResultCacheMetaExtension (https://github.com/phpstan/phpstan-src/commit/f9140a2a30be0074466b8711090037c7493f259b)
- OpenSslCipherMethodsProvider: track the supported ciphers as values (https://github.com/phpstan/phpstan-src/commit/339e91e25d0dde9ad137a091de05fddc29102161)
- test: update literal types after template union fix (#6404), thanks @calebdw!
- Extract parts of NodeScopeResolver into smaller classes (#6433)
- Declare the turbo shadowing classes under their real names at activation (#6490)
- Lint, sanitize and harden the native build (#6518)
- Let a rule listen for several node types #6540
- Reuse turbo extension binaries compiled from identical build inputs (#6588)
- Find the base phar checksum artifact by name (#6633), thanks @SanderMuller!
- Use stricter assertions in tests (#6640), thanks @staabm!