Skip to content

Resolve bootstrap autoloaders' classes from the files they ask for, running them for real only when that cannot redeclare a symbol - #6287

Merged
ondrejmirtes merged 1 commit into
2.2.xfrom
fix-15102-alias-of-unloaded-target
Aug 27, 2026
Merged

Resolve bootstrap autoloaders' classes from the files they ask for, running them for real only when that cannot redeclare a symbol#6287
ondrejmirtes merged 1 commit into
2.2.xfrom
fix-15102-alias-of-unloaded-target

Conversation

@ondrejmirtes

Copy link
Copy Markdown
Member

The function_exists($className) gate added for phpstan/phpstan#14988 confused two meanings of "functions": AutoloadFunctionsSourceLocator is about autoload functions — the spl_autoload_register() callbacks that bootstrap files register — not about class names coinciding with defined functions. Executed for a class, such an autoloader either asks for a file, which the file-read trap detects, or defines the class without one, through class_alias() or eval().

The gate's probe also poisoned its own evidence: a trapped include succeeds with empty content, defining nothing but still registering in get_included_files(), so comparing the trapped files against get_included_files() taken after the probe declined the very lookup it was probing. Laravel's facade aliases whose target class is not loaded yet hit exactly that: resolving Redirect makes class_alias() ask for the file of Illuminate\Support\Facades\Redirect, the probe trapped that read, found the file "already included" and declined — and larastan's monicahq/monica e2e failed with seven unknown class Redirect errors on 2.2.9 and current 2.2.x. Any earlier probe trapping the same file poisoned later lookups the same way, since the pseudo-include sticks for the rest of the process; include-list bookkeeping cannot be made sound.

Now every class lookup through this locator works the same way:

  1. Probe the autoload functions under the file-read trap. One that defines the class without reading a file has already done its work.
  2. Locate the class statically in the files they asked for (AutoloadSourceLocator::locateIdentifierInFiles()) — a plain include-based autoloader resolves with no execution at all.
  3. Run the autoloaders for real only as a fallback — the alias-of-unloaded-target case, where the trapped file is the target's and never declares the alias name — and only when wouldIncludingFilesRedeclareSymbols() says executing the located files cannot fatally redeclare a currently defined class or function. That is the actual #14988 hazard, judged by the files' statically parsed symbols, which pseudo-includes cannot poison.

The new e2e/bug-15102c covers the shape Illuminate\Foundation\AliasLoader creates in a real Laravel app: a prepended autoloader aliasing a name that collides with a global helper function (Redirect vs redirect()) to a class only Composer can autoload. It fails on current 2.2.x and passes with this change. bug-15102, bug-15102b, bug-14988, bug-12972b and bug-12972c still pass (verified with cleared result caches), and the full monicahq/monica + larastan analysis is clean against this branch.

See phpstan/phpstan#15102

🤖 Generated with Claude Code

https://claude.ai/code/session_01JkCjnojdWZvswKCPmzt4zY

…unning them for real only when that cannot redeclare a symbol

The function_exists($className) gate added for phpstan/phpstan#14988
confused two meanings of "functions": AutoloadFunctionsSourceLocator is
about autoload functions - the spl_autoload_register() callbacks that
bootstrap files register - not about class names coinciding with defined
functions. Executed for a class, such an autoloader either asks for a
file, which the file-read trap detects, or defines the class without one,
through class_alias() or eval().

The gate's probe also poisoned its own evidence: a trapped include
succeeds with empty content, defining nothing but still registering in
get_included_files(), so comparing the trapped files against
get_included_files() taken after the probe declined the very lookup it
was probing. Laravel's facade aliases whose target class is not loaded
yet hit exactly that: resolving Redirect makes class_alias() ask for the
file of Illuminate\Support\Facades\Redirect, the probe trapped that read,
found the file "already included" and declined, and larastan's
monicahq/monica e2e failed with seven "unknown class Redirect" errors.
Any earlier probe trapping the same file poisoned later lookups the same
way, since the pseudo-include sticks for the rest of the process.

Now every lookup probes under the trap first, the class is located
statically in the files the autoloaders asked for, and the autoloaders
run for real only when executing those files could not fatally redeclare
anything - judged by the files' statically parsed symbols, which
pseudo-includes cannot poison, instead of by get_included_files().

The bug-15102c e2e covers the shape Illuminate\Foundation\AliasLoader
creates in a real Laravel app: a prepended autoloader aliasing a name
that collides with a global helper function to a class only Composer can
autoload.

See phpstan/phpstan#15102

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JkCjnojdWZvswKCPmzt4zY
@ondrejmirtes
ondrejmirtes merged commit 598faaa into 2.2.x Aug 27, 2026
429 checks passed
@ondrejmirtes
ondrejmirtes deleted the fix-15102-alias-of-unloaded-target branch August 27, 2026 11:42
@ondrejmirtes

ondrejmirtes commented Aug 27, 2026

Copy link
Copy Markdown
Member Author

FYI reverting this: #6292.

#15102 keeps working.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant