Download Latest Version 2.2.7 source code.zip (11.9 MB)
Email in envelope

Get an email when there's a new version of PHPStan

Home / 2.2.6
Name Modified Size InfoDownloads / Week
Parent folder
phpstan.phar 2026-07-26 27.8 MB
phpstan.phar.asc 2026-07-26 833 Bytes
2.2.6 source code.tar.gz 2026-07-26 11.9 MB
2.2.6 source code.zip 2026-07-26 11.9 MB
README.md 2026-07-26 13.1 kB
Totals: 5 Items   51.6 MB 2

This release fixes 33 issues! 🎉

Major new features 🚀

  • PHPStan Turbo (https://github.com/phpstan/turbo-ext/)
  • Optional native PHP extension (PHP 8.3+) written in C++ that makes running PHPStan 10-30 % faster (depending on project specifics)
  • Users installing PHPStan through Composer with phpstan/phpstan get it automatically. Composer package ships prebuilt binaries for the most common platforms — Linux (glibc and musl, x86_64 and arm64), macOS, and Windows (x86_64)
  • Users running standalone phpstan.phar can install the extension with pie install phpstan/turbo
  • The fact that it's enabled can be verified with vendor/bin/phpstan diagnose or running analyse -vvv
  • There's room to improve performance with this technique in the future, but we're shipping it now because it brings real benefits already.

Bleeding edge 🔪

  • Report finite-typed values in a constant array haystack that can never be the in_array()/array_search()/array_keys() needle (#6054), [#14960]
  • Report ?? null / ??= null on an always-set left side as an unnecessary null coalesce (#5865), [#4337], [#12179], [#9966], thanks @VincentLanglet and @staabm!
  • Type DOMDocument load/save/validate string arguments as non-empty-string on PHP 8 (#6075), [#14979], thanks @VincentLanglet and @staabm!

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 🔧

  • Implement @pure-unless-callable-is-impure (#3482), [#11101], [#11100], thanks @zonuexe!
  • Report invalid $flags constants for preg_replace_callback() and preg_replace_callback_array() (#6019), [#11437], [#14910], thanks @staabm and @VincentLanglet!
  • Validate callback signatures for curl_setopt callback options like CURLOPT_PREREQFUNCTION (#6050), [#14956], thanks @staabm and @VincentLanglet!

Bugfixes 🐛

  • Do not narrow the left side of ?? to non-null when the coalesce context still permits a falsey value (#6000), [#13488], thanks @VincentLanglet and @staabm!
  • Add UnaryOperatorTypeSpecifyingExtension for BcMath\Number so unary +/- infer BcMath\Number (#5994), [#13965], thanks @VincentLanglet and @staabm!
  • Narrow explode() result for limit 0, 1, and -1 (#5993), thanks @paulbalandan!
  • Accept decimal-int-string arguments for %d printf placeholders in strict mode (#5997), [#13609], thanks @VincentLanglet and @staabm!
  • Resolve the base of $prop[] = ... to its readable type instead of its writable type (#5996), [#14054], thanks @VincentLanglet and @staabm!
  • Invalidate tracked superglobal narrowings after impure function, method, static and constructor calls (#6007), [#12799], thanks @VincentLanglet and @staabm!
  • Invalidate volatile expressions on eval/include (#6009), thanks @VincentLanglet!
  • Do not treat offset writes on ArrayAccess properties as reassignments of readonly properties (#5995), [#13856], thanks @VincentLanglet and @staabm!
  • Forget negative function_exists()/class_exists() narrowings when global state may change (#5998), [#13608], [#12094], thanks @VincentLanglet and @staabm!
  • Reanalyse trait-consuming files after a body-only trait change even when the trait shares its file with another symbol (#6033), [#14943]
  • Negating the smallest integer results in a float (#6028), [#14946], [#14947], thanks @zonuexe!
  • Fix 'path ... does not exist' false-positive for expr containing DIRECTORY_SEPARATOR (#6035), [#14944], thanks @staabm!
  • Track file dependencies for global constant fetches (#6023), thanks @SanderMuller!
  • Track define()'d constants as exported nodes (#6036), thanks @SanderMuller!
  • Fix filter_var throw-on-failure detection (#6042), thanks @simPod!
  • Make Type objects behave as the immutable values they are (#6037)
  • Account for subclasses in get_parent_class() of a $this/static value (#6048), [#14951], thanks @zonuexe!
  • Treat all in-scope variables as used when a closure or constructor body contains include/require/eval (#6056), [#14962], thanks @staabm and @VincentLanglet!
  • Treat all in-scope variables as used when an unused-parameter body accesses a variable variable ($$name) (#6058), [#13960], thanks @staabm and @VincentLanglet!
  • Resolve static/self/parent relative to @param-closure-this bound class (#6061), [#11010], thanks @calebdw!
  • Do not report abstract interface methods provided by a non-abstract built-in parent class in MissingMethodImplementationRule (#6059), [#14964], thanks @VincentLanglet!
  • Preserve maybe in BitwiseFlagHelper for bitwise-or with dynamic operands (#6080), thanks @vrana!
  • Treat trait-declared properties as initialized when promoted by an inherited constructor (#6085), [#14983], thanks @VincentLanglet and @staabm!
  • Keep the result cache when composer.lock changes but no package version did (#6086), [#14984], thanks @SanderMuller!
  • Fix "ob_get_*() === false will always evaluate to false" false positive (#6097), [#14985], thanks @staabm!
  • Invalidate the whole result cache when a changed package registers a container class (#6088), [#14986], thanks @SanderMuller!

Performance 🏎️

  • Cache ObjectType instead of re-creating it over and over (#6006), thanks @staabm!
  • Single-flight cold directory scans in OptimizedDirectorySourceLocatorFactory (#5846), thanks @SanderMuller!
  • Disable zend.assertions as suggested by PHP-Parser (#6021), thanks @staabm!
  • Faster DuplicateClassDeclarationRule (#6038), thanks @staabm!
  • Faster DuplicateFunctionDeclarationRule (#6039), thanks @staabm!
  • Memoize ExprHandler dispatch in MutatingScope::resolveType() (#6040), thanks @staabm!
  • Replace NodeFinder with direct recursion in shouldInvalidateExpression() (#6041), thanks @staabm!
  • Return early from ClassReflection::getMethod() cache (#6043), thanks @staabm!
  • Index PSR autoloader locators by symbol instead of sweeping them per lookup (#6037)
  • Short-circuit no-op remove() comparisons by identity before equals() (#6037)
  • Cache reflections built from the PhpStorm stubs (https://github.com/phpstan/phpstan-src/commit/ae5fd3163f507b5091ff19c0e96d7ab3c8d3e4c0)
  • Do not park fibers on expressions that can never receive a before-scope (#6064)
  • Extend the compositional-key shortcut to more invalidation scans (#6064)
  • Prevent unnecessary method calls on Type (#6076), thanks @staabm!
  • Prevent reflection when Property Hooks not supported (#6082), thanks @staabm!
  • Iterate through nullsafe expression chains (#6084), thanks @realFlowControl!
  • Iterate in collectInterfaces() (#6092), thanks @staabm!
  • Iterate in getBooleanExpressionDepth() (#6091), thanks @staabm!
  • Prevent duplicate filterByTruthyValue() work in NodeScopeResolver (#6103), thanks @staabm!
  • Run indentation detection, node cloning and node replacing in a single AST traversal and cache per-file fixing data (#6101), [#14996], thanks @staabm and @VincentLanglet!
  • Remove unnecessary work in BooleanOrHandler (#6106), thanks @staabm!
  • Iterative NullsafeOperatorHelper (#6105), thanks @staabm!
  • Simplify AccessoryArrayListType (#6107), thanks @staabm!

Function signature fixes 🤖

  • Use list-shapes for functionMap return types that are guaranteed lists (#6024), thanks @zonuexe!
  • Add array-shape return type for localeconv() and shape-based localtime() return type extension (#6031), [#14949], thanks @staabm and @VincentLanglet!

Internals 🔍

  • Refactor: extract ConfiguredPhpVersionRangeHelper (#6014), thanks @staabm!
  • Utilize ConfiguredPhpVersionRangeHelper (#6017), thanks @staabm!
  • Add named argument tests for @pure-unless-callable-is-impure (#6022), thanks @zonuexe!
  • Rename Test-classes colliding with builtin classes (#6032), thanks @staabm!
  • Upgrade PHPBench to 1.7.0 and use aggregate-preview report (#6067), thanks @dantleech!
  • Introduce #[ExtensionInterface] attribute, derive the interface-tag mapping from it (#6096)
  • Introduce ExtensionsCollection, #[AutowiredExtensions] and Container::getExtensionsCollection(), replace extension providers (#6098)
  • Add autoTag parameter to #[AutowiredService], migrate services that must not be auto-tagged (#6096)
  • Introduce #[InstanceofDeprecated] attribute, derive ApiInstanceofTypeRule map from it (#6096)
  • Update composer-attribute-collector: collect attributes without autoloading under PHP < 8 (#6096)
  • Remove obsolete PDO.patch for phpstorm-stubs (#6073), thanks @vrana!
  • Compile #[AutowiredParameter] references into deferred getParameter() lookups (#6102)
  • Validate service tags in afterCompile() so tags added by any extension are covered (#6102)
  • Discover compiler extensions from the #[ContainerExtension] attribute (#6102)
  • Upgrade nette/di to 3.1.10 and drop the deleted-tag pin (#6102)
Source: README.md, updated 2026-07-26