Skip to content

Commit 2d20290

Browse files
SanderMullerclaude
andcommitted
Consult a Composer replacement before the static source locators
collectNewAutoloadFunctions() splits the autoloaders a bootstrap file registers at Composer's position in the spl_autoload queue. When no Composer ClassLoader instance is left in that queue there is no boundary to split on, and every collected loader went to the appended bucket - consulted only after the static source locators. A bootstrap that unregisters [$composerLoader, 'loadClass'] and registers a wrapper delegating to it, as typo3/class-alias-loader does, produces exactly that queue. Its loader carries Composer's runtime priority, so it belongs before the static locators, which is where it sat until 2.2.9. Falling back to prepended restores that order and leaves the split untouched whenever Composer's own loader is still registered. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent e6357e5 commit 2d20290

2 files changed

Lines changed: 44 additions & 4 deletions

File tree

‎src/autoloadFunctions.php‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,14 @@ function autoloadFunctionsPrependedToComposer(): array // phpcs:ignore Squiz.Fun
4040
* same order relative to Composer as PHP does at runtime, instead of always
4141
* invoking them before (or after) the static Composer source locators.
4242
*
43+
* When no Composer ClassLoader instance is left in the queue there is no
44+
* boundary to split on: a bootstrap file has taken Composer's place, so its
45+
* loader carries Composer's runtime priority and belongs before the static
46+
* source locators. Bucketing those as appended would demote a loader that
47+
* actually resolves the class first, which is what
48+
* https://github.com/phpstan/phpstan/issues/15102 reported for
49+
* typo3/class-alias-loader.
50+
*
4351
* @param list<mixed>|false $autoloadFunctionsBefore
4452
* @param list<mixed>|false $autoloadFunctionsAfter
4553
* @return array{prepended: list<mixed>, appended: list<mixed>}
@@ -85,7 +93,7 @@ function collectNewAutoloadFunctions($autoloadFunctionsBefore, $autoloadFunction
8593
}
8694
}
8795

88-
if ($composerIndex !== null && $index < $composerIndex) {
96+
if ($composerIndex === null || $index < $composerIndex) {
8997
$prepended[] = $after;
9098
} else {
9199
$appended[] = $after;

‎tests/PHPStan/CollectNewAutoloadFunctionsTest.php‎

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,14 @@ public function testAutoloadersAreSplitByComposerPosition(): void
4040
$this->assertSame([$appended], $result['appended']);
4141
}
4242

43-
public function testWithoutComposerEverythingIsAppended(): void
43+
/**
44+
* With no Composer ClassLoader instance in the queue there is no boundary to split on -
45+
* a bootstrap loader has taken Composer's place and resolves the class first at runtime,
46+
* so it belongs before the static source locators.
47+
*
48+
* @see https://github.com/phpstan/phpstan/issues/15102
49+
*/
50+
public function testWithoutComposerEverythingIsPrepended(): void
4451
{
4552
$first = static function (string $class): void {
4653
};
@@ -49,8 +56,33 @@ public function testWithoutComposerEverythingIsAppended(): void
4956

5057
$result = collectNewAutoloadFunctions([], [$first, $second]);
5158

52-
$this->assertSame([], $result['prepended']);
53-
$this->assertSame([$first, $second], $result['appended']);
59+
$this->assertSame([$first, $second], $result['prepended']);
60+
$this->assertSame([], $result['appended']);
61+
}
62+
63+
/**
64+
* The shape typo3/class-alias-loader creates: the bootstrap unregisters
65+
* [$composerLoader, 'loadClass'] and registers a wrapper that delegates to it. The
66+
* wrapper is not a ClassLoader instance, so the queue holds no Composer entry to
67+
* split on and everything collected has to be consulted first.
68+
*/
69+
public function testComposerReplacementAndItsBootstrapAreBothPrepended(): void
70+
{
71+
$replacement = new class {
72+
73+
public function loadClass(string $class): void
74+
{
75+
}
76+
77+
};
78+
$wrapper = [$replacement, 'loadClass'];
79+
$bootstrap = static function (string $class): void {
80+
};
81+
82+
$result = collectNewAutoloadFunctions([], [$wrapper, $bootstrap]);
83+
84+
$this->assertSame([$wrapper, $bootstrap], $result['prepended']);
85+
$this->assertSame([], $result['appended']);
5486
}
5587

5688
public function testComposerAndPharAutoloaderAndPreexistingAreExcluded(): void

0 commit comments

Comments
 (0)