Download Latest Version 2.3.2 source code.zip (29.4 MB) Google Add to Preferred Sources
Home / 2.3.0
Name Modified Size InfoDownloads / 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-2-3-performance

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 static variable 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_* and T::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.php constraint in Scope::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 never in MutatingScope::intersectButNotNever() instead of widening it to null (#6504), [#15285]
  • Resolve "supported only on PHP X" checks from Scope::getPhpVersion() instead of the injected PhpVersion (#6496), [#13133], thanks @staabm and @VincentLanglet!
  • Resolve property, trait constant, arrow function never and attribute PHP version checks from Scope::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 injecting PhpVersion into 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 a TypeCombinatorCache memo 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, openlog and ZipArchive::open flags (#6571), [#15306], thanks @VincentLanglet!
  • Generalize keys and values of oversized arrays in ConstantTypeHelper::getTypeFromValue() like OversizedArrayBuilder (#6572), [#15305], thanks @staabm!
  • Fix stale phpredis signatures in functionMap (#6587)

Internals πŸ”

Source: README.md, updated 2026-10-06