Skip to content

Instantly share code, notes, and snippets.

@soyuka
Created March 18, 2025 15:29
Show Gist options
  • Save soyuka/6419e2aecc8ab2e25020d9f018eccff3 to your computer and use it in GitHub Desktop.
Save soyuka/6419e2aecc8ab2e25020d9f018eccff3 to your computer and use it in GitHub Desktop.
ReflectionClassRecursiveIterator with PSR-4 namespace
<?php
/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace ApiPlatform\Metadata\Util;
use Composer\Autoload\ClassLoader;
/**
* Gets reflection classes for php files in the given directories.
*
* @author Antoine Bluchet <[email protected]>
*
* @internal
*/
final class ReflectionClassRecursiveIterator
{
/**
* @var array<string, array<class-string, \ReflectionClass>>
*/
private static array $localCache;
private function __construct()
{
}
/**
* @param string[] $directories
*
* @return array<class-string, \ReflectionClass>
*/
public static function getReflectionClassesFromDirectories(array $directories): array
{
$id = hash('xxh3', implode('', $directories));
if (isset(self::$localCache[$id])) {
return self::$localCache[$id];
}
$includedFiles = [];
foreach ($directories as $path) {
foreach (static::getPhpFilesFromPath($path) as $file) {
try {
require_once $file;
} catch (\Throwable) {
// invalid PHP file (example: missing parent class)
continue;
}
$includedFiles[] = $file;
}
}
$sortedClasses = get_declared_classes();
sort($sortedClasses);
$sortedInterfaces = get_declared_interfaces();
sort($sortedInterfaces);
$declared = [...$sortedClasses, ...$sortedInterfaces];
$ret = [];
foreach ($declared as $className) {
$reflectionClass = new \ReflectionClass($className);
$sourceFile = $reflectionClass->getFileName();
if (isset($includedFiles[$sourceFile])) {
$ret[$className] = $reflectionClass;
}
}
return self::$localCache[$id] = $ret;
}
/**
* @param string[] $directories
*
* @return array<class-string, \ReflectionClass>
*/
public static function getPSRReflectionClassesFromDirectories(array $directories): array
{
$id = 'psr_' . hash('xxh3', implode('', $directories));
if (isset(self::$localCache[$id])) {
return self::$localCache[$id];
}
$includedFiles = [];
foreach ($directories as $rootDir) {
$ns = static::getPsr4Namespace($rootDir);
if (!$ns) {
continue;
}
foreach (static::getPhpFilesFromPath($rootDir) as $file) {
$className = static::getPsr4ClassName($rootDir, $ns, $file);
if (class_exists($className)) {
$includedFiles[] = new \ReflectionClass($className);
}
}
}
return $includedFiles;
}
/**
* @return Generator<int, string>
*/
private static function getPhpFilesFromPath(string $path): \Generator
{
$iterator = new \RegexIterator(
new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($path, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS),
\RecursiveIteratorIterator::LEAVES_ONLY
),
'/^.+\.php$/i',
\RecursiveRegexIterator::GET_MATCH
);
foreach ($iterator as $file) {
$sourceFile = $file[0];
if (!preg_match('(^phar:)i', (string) $sourceFile)) {
$sourceFile = realpath($sourceFile);
}
yield $sourceFile;
}
}
private static function findComposerJson(string $path): ?string
{
$currentDir = $path;
while (true) {
$composerJsonPath = $currentDir . '/composer.json';
if (file_exists($composerJsonPath)) {
return $composerJsonPath;
}
$parentDir = dirname($currentDir);
if ($parentDir === $currentDir || $parentDir === '.') {
break;
}
$currentDir = $parentDir;
}
return null;
}
private static function getPsr4Namespace(string $dirname): ?string
{
$composerJsonPath = static::findComposerJson($dirname);
$rootDir = dirname($composerJsonPath);
if (!$composerJsonPath) {
return null;
}
$json = file_get_contents($composerJsonPath);
if (!$json) {
return null;
}
$composer = json_decode($json, true);
if (!$composer) {
return null;
}
$relative = ltrim(str_replace($rootDir, '', $dirname), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
foreach ($composer['autoload']['psr-4'] ?? [] as $ns => $dir) {
if ($dir === $relative) {
return $ns;
}
}
return null;
}
private static function getPsr4ClassName(string $rootDir, string $namespace, string $filename): ?string
{
return $namespace . str_replace(DIRECTORY_SEPARATOR, '\\', substr(substr($filename, strlen($rootDir) + 1), 0, -4));
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment