Summary
With PHPStan 2.2.10 (released 2026-08-30) every generated Factory, Proxy, Extension and
ExtensionInterface class causes an internal error:
Internal error: Failed opening required '<?php
namespace Demo\Model;
/**
* Factory class for @see \Demo\Model\Thing101
*/
class Thing101Factory
{
/**
* Create class instance with specified parameters
*
* @param array $data
* @return Thing101
*/
public function create(array $data = array()) {}
}
' (include_path='.:/usr/local/lib/php') while analysing file /app/src/File101.php
require_once() receives the generated source code in place of the file path.
The same run passes with PHPStan 2.2.9.
Cause
bitExpert\PHPStan\Magento\Autoload\Cache\FileCacheStorage does not keep the
PHPStan\Cache\CacheStorage contract:
save($key, $variableKey, $data) writes $data (the source code) to a file.
load($key, $variableKey) returns the file path, not the data that was saved.
FactoryAutoloader::autoload() depends on that asymmetry
(src/bitExpert/PHPStan/Magento/Autoload/FactoryAutoloader.php:52-57):
$pathToLocalClass = $this->cache->load($class, '');
if ($pathToLocalClass === null) {
$this->cache->save($class, '', $this->getFileContents($class));
$pathToLocalClass = $this->cache->load($class, '');
}
require_once($pathToLocalClass);
PHPStan\Cache\Cache now has a shared-memory arena in front of the storage. Cache::save()
publishes $data to the arena, and Cache::load() returns the arena value before it asks the
storage. Thus the second load() gives back the source code, and require_once() gets it.
PHPStan 2.2.9 has the same arena code, but the arena stayed inactive: the turbo extension was
loaded in worker processes only. PHPStan 2.2.10 restarts the main process with the turbo
extension (TurboProcessRestarter) and forks the workers, thus the arena becomes active and the
mismatch shows.
Only PHP 8.3, 8.4 and 8.5 are affected, because PHPStan distributes turbo binaries for those
versions only. PHP 8.1 and 8.2 stay on the old path and pass.
The same pattern is in:
Autoload/FactoryAutoloader.php:52
Autoload/ProxyAutoloader.php:51
Autoload/ExtensionAutoloader.php:68
Autoload/ExtensionInterfaceAutoloader.php:67
How to reproduce
The reproduction needs Linux, PHP 8.3 or later, pcntl/posix and more than one analysis
process. The Docker command below covers all of it.
cat > composer.json <<'JSON'
{
"require-dev": {
"phpstan/phpstan": "2.2.10",
"bitexpert/phpstan-magento": "^0.43",
"phpstan/extension-installer": "^1.4"
},
"config": { "allow-plugins": { "phpstan/extension-installer": true } }
}
JSON
cat > phpstan.neon <<'NEON'
parameters:
level: 2
paths:
- src
parallel:
jobSize: 5
minimumNumberOfJobsPerProcess: 1
maximumNumberOfProcesses: 4
NEON
mkdir src
for i in $(seq 1 200); do
printf '<?php declare(strict_types=1);\nnamespace Demo;\nclass File%s { public function run(\\Demo\\Model\\Thing%sFactory $f): void { $f->create(); } }\n' "$i" "$i" > "src/File$i.php"
done
docker run --rm --platform linux/amd64 -v "$PWD":/app -w /app php:8.4-cli bash -c '
apt-get update -qq && apt-get install -y -qq unzip git
docker-php-ext-install -j4 pcntl posix
curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
composer update --no-interaction
vendor/bin/phpstan diagnose | grep -A3 "Turbo extension"
vendor/bin/phpstan analyse --no-progress
'
Result:
Turbo extension: enabled (version 2659ad4)
Turbo platform: linux-gnu-x86_64 (os: Linux, machine: x86_64, libc: gnu, php: 8.4, zts: no, debug: no)
Turbo worker binary: /app/vendor/phpstan/phpstan/turbo-ext/linux-gnu-x86_64/phpstan_turbo-8.4.so (loaded via process restart)
[ERROR] Found 50 errors
Test matrix
| Setup |
Result |
| phpstan 2.2.10 |
50 internal errors (PHPStan stops at its internal-error limit) |
| phpstan 2.2.9 |
No errors |
phpstan 2.2.10, PHPSTAN_ARENA=0 |
No errors |
phpstan 2.2.10, PHPSTAN_TURBO=0 |
No errors |
| phpstan 2.2.10, PHP 8.1 |
No errors (Turbo extension: not loaded) |
vendor/bin/phpstan diagnose shows the difference:
- 2.2.9:
Turbo extension: enabled in worker processes (the main process runs without it) / Mechanism: spawn (react/child-process)
- 2.2.10:
Turbo extension: enabled (loaded via process restart) / Mechanism: fork (pcntl_fork)
Environment
- bitexpert/phpstan-magento v0.43.0
- phpstan/phpstan 2.2.10 (2.2.9 is good)
- PHP 8.4 (also 8.3 and 8.5), Linux x86_64
- Found in CI on Magento 2.4.9 images with PHP 8.4
Summary
With PHPStan 2.2.10 (released 2026-08-30) every generated Factory, Proxy, Extension and
ExtensionInterface class causes an internal error:
require_once()receives the generated source code in place of the file path.The same run passes with PHPStan 2.2.9.
Cause
bitExpert\PHPStan\Magento\Autoload\Cache\FileCacheStoragedoes not keep thePHPStan\Cache\CacheStoragecontract:save($key, $variableKey, $data)writes$data(the source code) to a file.load($key, $variableKey)returns the file path, not the data that was saved.FactoryAutoloader::autoload()depends on that asymmetry(
src/bitExpert/PHPStan/Magento/Autoload/FactoryAutoloader.php:52-57):PHPStan\Cache\Cachenow has a shared-memory arena in front of the storage.Cache::save()publishes
$datato the arena, andCache::load()returns the arena value before it asks thestorage. Thus the second
load()gives back the source code, andrequire_once()gets it.PHPStan 2.2.9 has the same arena code, but the arena stayed inactive: the turbo extension was
loaded in worker processes only. PHPStan 2.2.10 restarts the main process with the turbo
extension (
TurboProcessRestarter) and forks the workers, thus the arena becomes active and themismatch shows.
Only PHP 8.3, 8.4 and 8.5 are affected, because PHPStan distributes turbo binaries for those
versions only. PHP 8.1 and 8.2 stay on the old path and pass.
The same pattern is in:
Autoload/FactoryAutoloader.php:52Autoload/ProxyAutoloader.php:51Autoload/ExtensionAutoloader.php:68Autoload/ExtensionInterfaceAutoloader.php:67How to reproduce
The reproduction needs Linux, PHP 8.3 or later,
pcntl/posixand more than one analysisprocess. The Docker command below covers all of it.
Result:
Test matrix
PHPSTAN_ARENA=0PHPSTAN_TURBO=0Turbo extension: not loaded)vendor/bin/phpstan diagnoseshows the difference:Turbo extension: enabled in worker processes (the main process runs without it)/Mechanism: spawn (react/child-process)Turbo extension: enabled (loaded via process restart)/Mechanism: fork (pcntl_fork)Environment