On a default macOS checkout, PHPStan can accidentally include OpenMage's root index.php while trying to determine whether the string index is a class name.
The visible failure is misleading. It often ends as:
Fatal error: Uncaught Error: Class "Hoa\Event\Bucket" not found
in phar:///.../vendor/phpstan/phpstan/phpstan.phar/vendor/hoa/stream/Stream.php:237
while running parallel worker
That is not the root cause. It is a shutdown/autoload failure after PHPStan has already entered a bad execution path.
The root cause is:
- PHPStan analyzes an array like
['index', 'save'].
- PHPStan checks whether it could be a callable array like
['ClassName', 'methodName'].
- PHPStan asks BetterReflection whether class
index exists.
- The
phpstan-bootstrap.php autoloader maps class index to Index.php.
- On a case-insensitive macOS filesystem,
Index.php matches the real root file index.php.
- The root front controller is included.
Mage::run() executes inside PHPStan.
- Application exception handling runs, including
MM_Ignition.
- PHPStan later reports misleading PHAR/autoload errors.
Linux works because Index.php does not match index.php on a case-sensitive filesystem.
Docker on macOS can still reproduce the issue if the application is mounted from the host, because the lookup is backed by the macOS bind mount.
app/code/core/Mage/Adminhtml/controllers/Sales/Order/CreateController.php
The expression is in `_isAllowed()`:
```php
$action = strtolower($this->getRequest()->getActionName());
if (in_array($action, ['index', 'save'], true) && $this->_getSession()->getReordered()) {
$action = 'reorder';
}
The array ['index', 'save'] is the important part.
PHPStan considers whether a two-element string array could be a callable:
could theoretically mean:
['SomeClass', 'someMethod']
So PHPStan asks whether index is a class. That invokes registered autoloaders.
Autoloader Path
macopedia/phpstan-magento1/phpstan-bootstrap.php registers this fallback autoloader:
spl_autoload_register(static function ($className) {
$classFile = str_replace(' ', DIRECTORY_SEPARATOR, ucwords(str_replace('_', ' ', $className)));
$classFile .= '.php';
foreach (explode(':', get_include_path()) as $path) {
if (\file_exists($path . DIRECTORY_SEPARATOR . $classFile)) {
return include $classFile;
}
}
}, true, true);
For class name:
this creates:
The same bootstrap also does:
$paths = [];
$paths[] = BP . DS . 'app' . DS . 'code' . DS . 'local';
$paths[] = BP . DS . 'app' . DS . 'code' . DS . 'community';
$paths[] = BP . DS . 'app' . DS . 'code' . DS . 'core';
$paths[] = BP . DS . 'lib';
$appPath = implode(PS, $paths);
set_include_path($appPath . PS . get_include_path());
Because it appends the original include path, the current directory remains available, either as . or through an empty include path segment.
On macOS:
file_exists('./Index.php')
matches:
So the autoloader includes the root front controller.
Potential workarounds
XDEBUG_MODE=off php -d include_path=/__phpstan_no_cwd__ vendor/bin/phpstan analyse --no-progress --no-ansi --memory-limit=2G
This basically strips the openmage root dir from the include path which makes things work for the openmage core distribution but may cause issues for merchants who have code in the root.
A sledgehammer solution which would have some performance implications is to do a case sensitive sanity check
function fileExistsWithExactCase(string $file): bool
{
if (!file_exists($file)) {
return false;
}
$directory = dirname($file);
$basename = basename($file);
$entries = scandir($directory);
return is_array($entries) && in_array($basename, $entries, true);
}
Use it here:
foreach (explode(PATH_SEPARATOR, get_include_path()) as $path) {
$file = $path . DIRECTORY_SEPARATOR . $classFile;
if (fileExistsWithExactCase($file)) {
return include $file;
}
}
Given a default full pass of phpstan on the core openmage distro doesn't work, gating this behind a case sensitive fs check would at least make it work for mac users without degrading perf for everyone else on sensible filesystems. I've not tested windows, I assume nothing works there (wsl likely as well?).
Personally I'm just using -d include_path=/phpstan_no_cwd to avoid the issue but would be good to determine a robust fix so other mac based openmage developers don't have to puzzle about it as I did!
On a default macOS checkout, PHPStan can accidentally include OpenMage's root
index.phpwhile trying to determine whether the stringindexis a class name.The visible failure is misleading. It often ends as:
That is not the root cause. It is a shutdown/autoload failure after PHPStan has already entered a bad execution path.
The root cause is:
['index', 'save'].['ClassName', 'methodName'].indexexists.phpstan-bootstrap.phpautoloader maps classindextoIndex.php.Index.phpmatches the real root fileindex.php.Mage::run()executes inside PHPStan.MM_Ignition.Linux works because
Index.phpdoes not matchindex.phpon a case-sensitive filesystem.Docker on macOS can still reproduce the issue if the application is mounted from the host, because the lookup is backed by the macOS bind mount.
app/code/core/Mage/Adminhtml/controllers/Sales/Order/CreateController.php
The array
['index', 'save']is the important part.PHPStan considers whether a two-element string array could be a callable:
could theoretically mean:
So PHPStan asks whether
indexis a class. That invokes registered autoloaders.Autoloader Path
macopedia/phpstan-magento1/phpstan-bootstrap.phpregisters this fallback autoloader:For class name:
this creates:
The same bootstrap also does:
Because it appends the original include path, the current directory remains available, either as
.or through an empty include path segment.On macOS:
matches:
So the autoloader includes the root front controller.
Potential workarounds
This basically strips the openmage root dir from the include path which makes things work for the openmage core distribution but may cause issues for merchants who have code in the root.
A sledgehammer solution which would have some performance implications is to do a case sensitive sanity check
Use it here:
Given a default full pass of phpstan on the core openmage distro doesn't work, gating this behind a case sensitive fs check would at least make it work for mac users without degrading perf for everyone else on sensible filesystems. I've not tested windows, I assume nothing works there (wsl likely as well?).
Personally I'm just using -d include_path=/phpstan_no_cwd to avoid the issue but would be good to determine a robust fix so other mac based openmage developers don't have to puzzle about it as I did!