|
31 | 31 | use function count; |
32 | 32 | use function defined; |
33 | 33 | use function function_exists; |
| 34 | +use function get_included_files; |
| 35 | +use function ini_get; |
34 | 36 | use function interface_exists; |
35 | 37 | use function is_array; |
36 | 38 | use function is_file; |
@@ -69,6 +71,9 @@ final class AutoloadSourceLocator implements SourceLocator |
69 | 71 | /** @var array<string, int> */ |
70 | 72 | private array $startLineByClass = []; |
71 | 73 |
|
| 74 | + /** How many get_included_files() entries invalidateIncludedFiles() has handled. */ |
| 75 | + private static int $invalidatedIncludedFilesCount = 0; |
| 76 | + |
72 | 77 | public function __construct(private FileNodesFetcher $fileNodesFetcher, private bool $executeAutoloadersInFileReadTrap) |
73 | 78 | { |
74 | 79 | } |
@@ -355,6 +360,8 @@ static function () use ($className, &$foundByComposer): ?array { |
355 | 360 | return [[$composerFile], $className, null]; |
356 | 361 | } |
357 | 362 |
|
| 363 | + self::invalidateIncludedFiles(); |
| 364 | + |
358 | 365 | try { |
359 | 366 | $preExistingAutoloader($className); |
360 | 367 | } catch (ParseError) { |
@@ -432,6 +439,33 @@ private static function findFileWithComposer(callable $autoloader, string $class |
432 | 439 | return $resolvedFile; |
433 | 440 | } |
434 | 441 |
|
| 442 | + /** |
| 443 | + * An autoloader can include a file that this process already included for real, for example a file |
| 444 | + * a bootstrap autoloader included without declaring the class. With OPcache active, the trapped |
| 445 | + * include of that file runs the script OPcache compiled from the real file, not the empty script |
| 446 | + * FileReadTrapStreamWrapper serves, and dies with "Cannot redeclare". Invalidating the entries of |
| 447 | + * the included files makes that include go through the trap. The opcache_invalidate() loop after |
| 448 | + * the trap cannot do this, because the include has already run by then. That loop removes the |
| 449 | + * empty script the trap left in OPcache. |
| 450 | + * |
| 451 | + * Only an autoloader that runs inside the trap needs this. findFileWithComposer() includes nothing. |
| 452 | + * Forked workers switch OPcache off (see ForkedProcess), so they skip this. |
| 453 | + */ |
| 454 | + private static function invalidateIncludedFiles(): void |
| 455 | + { |
| 456 | + if (!function_exists('opcache_invalidate') || !(bool) ini_get('opcache.enable') || !(bool) ini_get('opcache.enable_cli')) { |
| 457 | + return; |
| 458 | + } |
| 459 | + |
| 460 | + $includedFiles = get_included_files(); |
| 461 | + $includedFilesCount = count($includedFiles); |
| 462 | + for ($i = self::$invalidatedIncludedFilesCount; $i < $includedFilesCount; $i++) { |
| 463 | + opcache_invalidate($includedFiles[$i], true); |
| 464 | + } |
| 465 | + |
| 466 | + self::$invalidatedIncludedFilesCount = $includedFilesCount; |
| 467 | + } |
| 468 | + |
435 | 469 | private function silenceErrors(): void |
436 | 470 | { |
437 | 471 | set_error_handler(static fn (): bool => true); |
|
0 commit comments