Skip to content

Autoloader behaviour on case-insensitive filesystems (Mac os) #10

@ajbonner

Description

@ajbonner

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:

  1. PHPStan analyzes an array like ['index', 'save'].
  2. PHPStan checks whether it could be a callable array like ['ClassName', 'methodName'].
  3. PHPStan asks BetterReflection whether class index exists.
  4. The phpstan-bootstrap.php autoloader maps class index to Index.php.
  5. On a case-insensitive macOS filesystem, Index.php matches the real root file index.php.
  6. The root front controller is included.
  7. Mage::run() executes inside PHPStan.
  8. Application exception handling runs, including MM_Ignition.
  9. 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:

['index', 'save']

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:

index

this creates:

Index.php

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:

./index.php

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!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions