diff --git a/composer.json b/composer.json index 2930e01e..d85fd93e 100644 --- a/composer.json +++ b/composer.json @@ -12,6 +12,7 @@ "php": "^8.1", "ext-pdo": "*", "ext-mbstring": "*", + "ext-tokenizer": "*", "aura/sql": "^4.0 || ^5.0", "doctrine/annotations": "^1.12 || ^2.0", "guzzlehttp/guzzle": "^6.3 || ^7.2", @@ -24,7 +25,6 @@ "ray/aop": "^2.10.4", "ray/aura-sql-module": "^1.12.0", "ray/di": "^2.14", - "roave/better-reflection": "^4.12 || ^5.6 || ^6.25", "symfony/polyfill-php81": "^1.24" }, "require-dev": { diff --git a/src/ClassesInDirectories.php b/src/ClassesInDirectories.php index c814c686..6064d273 100644 --- a/src/ClassesInDirectories.php +++ b/src/ClassesInDirectories.php @@ -5,51 +5,122 @@ namespace Ray\MediaQuery; use Generator; -use Roave\BetterReflection\BetterReflection; -use Roave\BetterReflection\Reflector\DefaultReflector; -use Roave\BetterReflection\SourceLocator\Type\AggregateSourceLocator; -use Roave\BetterReflection\SourceLocator\Type\AutoloadSourceLocator; -use Roave\BetterReflection\SourceLocator\Type\DirectoriesSourceLocator; +use RecursiveDirectoryIterator; +use RecursiveIteratorIterator; +use SplFileInfo; use function assert; use function class_exists; +use function count; +use function file_get_contents; use function interface_exists; +use function is_array; +use function is_string; +use function token_get_all; + +use const T_CLASS; +use const T_INTERFACE; +use const T_NAME_QUALIFIED; +use const T_NAMESPACE; +use const T_STRING; final class ClassesInDirectories { /** - * get a list of all classes in the given directories. - * - * Based on: https://github.com/Roave/BetterReflection/blob/396a07c9d276cb9ffba581b24b2dadbb542d542e/demo/parsing-whole-directory/example2.php. - * - * @param list $directories + + * @param string ...$directories * * @return Generator - * - * This function code is taken from https://github.com/WyriHaximus/php-list-classes-in-directory/blob/master/src/functions.php - * and modified for roave/better-reflection 5.x - * - * @see https://github.com/WyriHaximus/php-list-classes-in-directory - * @psalm-suppress MixedReturnTypeCoercion - * @phpstan-ignore-next-line/ */ - public static function list(string ...$directories): iterable + public static function list(string ...$directories): Generator + { + foreach ($directories as $directory) { + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($directory), + ); + + /** @psalm-suppress MixedAssignment */ + foreach ($iterator as $file) { + if (! $file instanceof SplFileInfo || $file->getExtension() !== 'php') { + continue; + } + + $className = self::getClassFromFile($file->getRealPath()); + + if ($className === null || ! class_exists($className) && ! interface_exists($className)) { + continue; + } + + yield $className; + } + } + } + + private static function getClassFromFile(string $filePath): string|null + { + $content = file_get_contents($filePath); + if ($content === false) { + return null; + } + + $tokens = token_get_all($content); + /** @var array $tokens */ + + $namespace = self::extractNamespace($tokens); + $class = self::extractClassName($tokens); + + if ($class === null) { + return null; + } + + if ($namespace === null) { + return $class; + } + + return $namespace . '\\' . $class; + } + + /** @param array $tokens*/ + private static function extractNamespace(array $tokens): string|null { - /** @var list $directories */ - $sourceLocator = new AggregateSourceLocator([ - new DirectoriesSourceLocator( - $directories, - (new BetterReflection())->astLocator(), - ), - // ↓ required to autoload parent classes/interface from another directory than /src (e.g. /vendor) - new AutoloadSourceLocator((new BetterReflection())->astLocator()), - ]); - - foreach ((new DefaultReflector($sourceLocator))->reflectAllClasses() as $class) { - $className = $class->getName(); - assert(class_exists($className) || interface_exists($className)); - - yield $className; + /** @psalm-suppress MixedAssignment */ + foreach ($tokens as $index => $token) { + if (is_array($token) && $token[0] !== T_NAMESPACE) { + continue; + } + + for ($j = $index + 1, $count = count($tokens); $j < $count; $j++) { + if (isset($tokens[$j][0]) && $tokens[$j][0] === T_NAME_QUALIFIED) { // @phpstan-ignore-line + assert(isset($tokens[$j][1])); // @phpstan-ignore-line + $string = $tokens[$j][1]; + assert(is_string($string)); + + return $string; + } + } } + + return null; + } + + /** @param array $tokens */ + private static function extractClassName(array $tokens): string|null + { + /** @psalm-suppress MixedAssignment */ + foreach ($tokens as $index => $token) { + if (isset($token[0]) && $token[0] !== T_CLASS && $token[0] !== T_INTERFACE) { // @phpstan-ignore-line + continue; + } + + for ($j = $index + 1, $count = count($tokens); $j < $count; $j++) { + if (is_array($tokens[$j]) && $tokens[$j][0] === T_STRING) { + $string = $tokens[$j][1]; + assert(is_string($string)); + + return $string; + } + } + } + + return null; } }