Summary
PHPStan 2.2.6 introduced PHPStan Turbo — an optional native C++ extension (PHP 8.3+) that makes analysis 10–30 % faster with bit-for-bit identical output. We are pinned at 2.2.3 in phive.xml.
Recommendation: bump the phar to 2.2.7. Do NOT ship the turbo binaries yet.
Research was done read-only (no repo changes) and the key claims were verified by execution in a php:8.4-cli container. Full writeup lives outside the repo at untracked/PHPSTAN-TURBO-RESEARCH.md; this issue carries the substance.
Where we are
|
|
| Pinned now |
phpstan ^2.2.3, installed="2.2.3" |
| Latest |
2.2.7 (2026-07-29) |
| Delivery model |
PHIVE phar in vendor-phar/, plus "replace": {"phpstan/phpstan": "*"} — the Composer package is never installed |
Relevant releases: 2.2.4 · 2.2.5 · 2.2.6 · 2.2.7
Finding 1 — the "you get it automatically" headline does not apply to us
The announcement says Composer users get Turbo automatically and phar users should pie install phpstan/turbo. Both halves are misleading for our architecture.
Reading TurboExtensionSelector, the binaries are located next to the phar:
$pharPath = Phar::running(false);
$file = sprintf('%s/turbo-ext/%s/phpstan_turbo-%d.%d%s.%s', dirname($pharPath), $platform, ...);
Composer users only get it automatically because the package is the git repo where phpstan.phar and turbo-ext/ are siblings. So we do not need PIE — we could ship the binaries in vendor-phar/ and every consumer would get it with zero action.
Verified. With the binary staged next to the phar, phpstan diagnose reports:
Turbo extension: enabled in worker processes (the main process runs without it)
Turbo platform: linux-gnu-x86_64 (os: Linux, machine: x86_64, libc: gnu, php: 8.4, zts: no, debug: no)
Turbo worker binary: /w/turbo-ext/linux-gnu-x86_64/phpstan_turbo-8.4.so
Control (directory moved away): Turbo extension: not loaded.
Note PHPStan states the limitation itself — worker processes only. The main process stays un-accelerated, so a path-scoped run that never forks workers gains nothing.
Finding 2 — output identity holds ✅
Same config, result cache cleared, --error-format=raw, with vs without turbo: 148 errors both ways, diff empty. Upstream's bit-for-bit promise is sound.
Finding 3 — turbo costs ~36 % more memory ⚠️ (not in any changelog)
This surfaced only by running it:
|
Used memory (PHPStan -v, total across workers) |
| turbo ON |
643.06 MB |
| turbo OFF |
474.05 MB |
| delta |
+169 MB (+35.7 %) |
Crash-threshold sweep (per-process limit, 51-file analysis):
| Limit |
turbo ON |
turbo OFF |
| 128M |
CRASHES (3/3 reproductions) |
ok |
| 160M+ |
ok |
ok |
Consequences:
- Turbo largely gives back 2.2.4's memory win (that release cut usage 2.8 GB → 2.1 GB; turbo adds ~36 % back). A fair trade — speed for memory — but a trade, and the two facts live in different places upstream.
- Harmless at our 4 G default (
phpqaMemoryLimit, applied via phpNoXdebug in includes/functions.inc.bash and propagated to workers). But any consumer on a lowered limit (our own docs give 2G as an example) or a memory-capped CI container gets a crash, not a slowdown.
Finding 4 — the version-pin drift is real and fast-moving
The extension version is the short SHA of the last commit touching turbo-ext/src/, and the phar hard-pins the one it expects. A mismatch silently deactivates the extension (prints a note, runs without it).
|
Expected extension version |
| phpstan 2.2.6 |
2d4b432 |
| phpstan 2.2.7 |
86312b7 |
| turbo-ext default branch (today) |
1ecf6e0 |
It changed between two consecutive patch releases three days apart. This means:
- The PIE route is unmanageable for an estate — every developer and CI image would have to re-run
pie install in lockstep with every phar bump, or silently lose the extension.
- If we ever vendor the binaries, they must be re-copied from the same tag as the phar on every bump, with a parity check asserting
turbo-ext/.version matches — otherwise we carry ~5 MB of dead weight and never notice.
Blast radius — this repo
Low-moderate, and of the ordinary kind. Mechanically a phive bump plus a rebuilt phar blob (~27 MB, business as usual for this repo).
- All three stricter new checks in 2.2.6 are Bleeding Edge only (unnecessary
?? null, impossible in_array() needles, non-empty-string DOMDocument args). We include bleedingEdge.neon nowhere — configDefaults/generic/phpstan.neon is just level: max. So they cannot fire for us.
- Changes that can add errors at
level: max: stronger explode() narrowing (2.2.5/2.2.6), list-shapes for functionMap returns, invalid preg_replace_callback() flags, curl_setopt callback signatures. Expect tautologies (alreadyNarrowedType / alwaysTrue / alwaysFalse) especially in tests — exactly what the educational note already in includes/generic/phpstan.inc.bash was written for.
- Custom rules: near-zero API risk. All 28 rules in
src/PHPStan/Rules/ use only Rules\Rule, RuleErrorBuilder, Analyser\Scope, plus a little ReflectionProvider / InClassNode / FileNode / Type. Long-stable public API.
- 2.2.7 is bugfixes + perf + internals only — no new rules, no new Bleeding Edge checks. It adds upgrade value at near-zero extra risk over 2.2.6, and notably contains a turbo bugfix ("Do not key the container cache on the turbo extension state").
- Upside: 2.2.4 cut memory 2.8 GB → 2.1 GB. Good reason to take this upgrade regardless of turbo.
PhpStanGuardPlugin validates the phar version against the extension-installer constraint; moving forward can only make that check more likely to pass.
- Result cache is invalidated by any version change → one full re-analysis everywhere, once.
Blast radius — downstream consumers
This is where the risk concentrates, because consumers inherit the version without choosing it.
- Their next
composer update lts/php-qa-ci silently moves them 2.2.3 → 2.2.7, at level: max, on codebases we have never seen.
- Under GitHub Actions,
GITHUB_ACTIONS=true ⇒ qaReadOnly ⇒ aggregate mode, so new errors are a hard CI failure, not a nag — a consumer's green pipeline can go red from our dependency bump with no change of theirs.
- One-off full re-analysis on their first post-upgrade run.
- Ship this as a clearly flagged release, noting that new PHPStan true-positives are expected.
If we later ship turbo binaries, the per-install cost is:
| Set |
Raw |
~Packed (measured 35.3 % gzip) |
| Everything upstream ships |
20.8 MB |
~7.3 MB |
| PHP 8.4 only, non-ZTS, all 6 platforms |
4.98 MB |
~1.7 MB |
| linux-gnu-x86_64, PHP 8.4 only |
851 KB |
~300 KB |
linux-musl-* (Alpine CI) and macos (dev laptops) are separate binaries; omitting them means those hosts silently get nothing.
Recommendation
- Bump the phar 2.2.3 → 2.2.7 as its own commit, so any new PHPStan errors are unambiguously attributable to the version change. Update phive.xml and re-run
scripts/tool-install.bash update.
- Run
vendor/bin/qa -t stan afterwards — 2.2.6/2.2.7 heavily refactored extension registration (ExtensionsCollection, #[AutowiredExtensions], nette/di 3.1.10). Confirm the 28 custom rules still register. This is the one check I would not skip.
- Defer the turbo binaries. We have no speed measurement on our own code, they cost +36 % memory, and upstream is visibly still stabilising them (a turbo bug fixed within 3 days).
- If/when we revisit: adopt the sibling-directory route (never PIE), fetch
turbo-ext/ from the same tag as the phar inside tool-install.bash, and add a version-parity assertion.
Still unmeasured
- Speed on our own codebase. The 10–30 % claim is unverified here; the container test was too small (51 files, startup-dominated) to measure. Needs an interleaved A/B on user CPU.
phpNoXdebug compatibility (php -n -c <generated-ini>) — code-reading says fine, not yet observed.
- New error count at
level: max with our full rule set — the container run used a bare level-5 config.
Related
- #3 (closed) — PHPStan: foolproof phar/extension management with composer update. Same problem family; worth reading before implementing.
- #10 — PHPStan path-coverage check.
Summary
PHPStan 2.2.6 introduced PHPStan Turbo — an optional native C++ extension (PHP 8.3+) that makes analysis 10–30 % faster with bit-for-bit identical output. We are pinned at 2.2.3 in phive.xml.
Recommendation: bump the phar to 2.2.7. Do NOT ship the turbo binaries yet.
Research was done read-only (no repo changes) and the key claims were verified by execution in a
php:8.4-clicontainer. Full writeup lives outside the repo atuntracked/PHPSTAN-TURBO-RESEARCH.md; this issue carries the substance.Where we are
phpstan ^2.2.3,installed="2.2.3"vendor-phar/, plus"replace": {"phpstan/phpstan": "*"}— the Composer package is never installedRelevant releases: 2.2.4 · 2.2.5 · 2.2.6 · 2.2.7
Finding 1 — the "you get it automatically" headline does not apply to us
The announcement says Composer users get Turbo automatically and phar users should
pie install phpstan/turbo. Both halves are misleading for our architecture.Reading
TurboExtensionSelector, the binaries are located next to the phar:Composer users only get it automatically because the package is the git repo where
phpstan.pharandturbo-ext/are siblings. So we do not need PIE — we could ship the binaries invendor-phar/and every consumer would get it with zero action.Verified. With the binary staged next to the phar,
phpstan diagnosereports:Control (directory moved away):
Turbo extension: not loaded.Note PHPStan states the limitation itself — worker processes only. The main process stays un-accelerated, so a path-scoped run that never forks workers gains nothing.
Finding 2 — output identity holds ✅
Same config, result cache cleared,
--error-format=raw, with vs without turbo: 148 errors both ways,diffempty. Upstream's bit-for-bit promise is sound.Finding 3 — turbo costs ~36 % more memory⚠️ (not in any changelog)
This surfaced only by running it:
-v, total across workers)Crash-threshold sweep (per-process limit, 51-file analysis):
Consequences:
phpqaMemoryLimit, applied viaphpNoXdebugin includes/functions.inc.bash and propagated to workers). But any consumer on a lowered limit (our own docs give2Gas an example) or a memory-capped CI container gets a crash, not a slowdown.Finding 4 — the version-pin drift is real and fast-moving
The extension version is the short SHA of the last commit touching
turbo-ext/src/, and the phar hard-pins the one it expects. A mismatch silently deactivates the extension (prints a note, runs without it).2d4b43286312b71ecf6e0It changed between two consecutive patch releases three days apart. This means:
pie installin lockstep with every phar bump, or silently lose the extension.turbo-ext/.versionmatches — otherwise we carry ~5 MB of dead weight and never notice.Blast radius — this repo
Low-moderate, and of the ordinary kind. Mechanically a phive bump plus a rebuilt phar blob (~27 MB, business as usual for this repo).
?? null, impossiblein_array()needles,non-empty-stringDOMDocument args). We includebleedingEdge.neonnowhere — configDefaults/generic/phpstan.neon is justlevel: max. So they cannot fire for us.level: max: strongerexplode()narrowing (2.2.5/2.2.6), list-shapes for functionMap returns, invalidpreg_replace_callback()flags,curl_setoptcallback signatures. Expect tautologies (alreadyNarrowedType/alwaysTrue/alwaysFalse) especially in tests — exactly what the educational note already in includes/generic/phpstan.inc.bash was written for.src/PHPStan/Rules/use onlyRules\Rule,RuleErrorBuilder,Analyser\Scope, plus a littleReflectionProvider/InClassNode/FileNode/Type. Long-stable public API.PhpStanGuardPluginvalidates the phar version against the extension-installer constraint; moving forward can only make that check more likely to pass.Blast radius — downstream consumers
This is where the risk concentrates, because consumers inherit the version without choosing it.
composer update lts/php-qa-cisilently moves them 2.2.3 → 2.2.7, atlevel: max, on codebases we have never seen.GITHUB_ACTIONS=true⇒qaReadOnly⇒ aggregate mode, so new errors are a hard CI failure, not a nag — a consumer's green pipeline can go red from our dependency bump with no change of theirs.If we later ship turbo binaries, the per-install cost is:
linux-musl-*(Alpine CI) andmacos(dev laptops) are separate binaries; omitting them means those hosts silently get nothing.Recommendation
scripts/tool-install.bash update.vendor/bin/qa -t stanafterwards — 2.2.6/2.2.7 heavily refactored extension registration (ExtensionsCollection,#[AutowiredExtensions], nette/di 3.1.10). Confirm the 28 custom rules still register. This is the one check I would not skip.turbo-ext/from the same tag as the phar insidetool-install.bash, and add a version-parity assertion.Still unmeasured
phpNoXdebugcompatibility (php -n -c <generated-ini>) — code-reading says fine, not yet observed.level: maxwith our full rule set — the container run used a bare level-5 config.Related