From 5414813d0c07384ba7015467b68770f094c8d724 Mon Sep 17 00:00:00 2001 From: Yurii Kuvshynov <141632421+fogrye@users.noreply.github.com> Date: Thu, 14 Mar 2024 17:33:15 +0100 Subject: [PATCH] feat: replace thecodingmachine/class-explorer with kcs/class-finder Main issue is to let type mappers find types in vendor packages which class-explorer and maintainer is not updating the project. Symfony and Laravel bundles have to be updated too. Fixes: #657 --- composer.json | 4 +- src/GlobControllerQueryProvider.php | 31 ++++++-------- src/SchemaFactory.php | 12 +++--- src/Utils/Namespaces/NS.php | 51 +++++++++++------------ src/Utils/Namespaces/NamespaceFactory.php | 13 +++--- tests/GlobControllerQueryProviderTest.php | 23 +++++----- tests/SchemaFactoryTest.php | 11 ++--- 7 files changed, 71 insertions(+), 74 deletions(-) diff --git a/composer.json b/composer.json index 24dab77d1d..d3e79e0426 100644 --- a/composer.json +++ b/composer.json @@ -25,8 +25,8 @@ "symfony/cache": "^4.3 || ^5 || ^6 || ^7", "symfony/expression-language": "^4 || ^5 || ^6 || ^7", "thecodingmachine/cache-utils": "^1", - "thecodingmachine/class-explorer": "^1.1.0", - "webonyx/graphql-php": "^v15.0" + "webonyx/graphql-php": "^v15.0", + "kcs/class-finder": "^0.4.0" }, "require-dev": { "beberlei/porpaginas": "^1.2 || ^2.0", diff --git a/src/GlobControllerQueryProvider.php b/src/GlobControllerQueryProvider.php index 85998c780b..eac742a5ba 100644 --- a/src/GlobControllerQueryProvider.php +++ b/src/GlobControllerQueryProvider.php @@ -6,14 +6,14 @@ use GraphQL\Type\Definition\FieldDefinition; use InvalidArgumentException; -use Mouf\Composer\ClassNameMapper; +use Kcs\ClassFinder\Finder\ComposerFinder; +use Kcs\ClassFinder\Finder\FinderInterface; use Psr\Container\ContainerInterface; use Psr\SimpleCache\CacheInterface; use ReflectionClass; use ReflectionMethod; use Symfony\Component\Cache\Adapter\Psr16Adapter; use Symfony\Contracts\Cache\CacheInterface as CacheContractInterface; -use TheCodingMachine\ClassExplorer\Glob\GlobClassExplorer; use TheCodingMachine\GraphQLite\Annotations\Mutation; use TheCodingMachine\GraphQLite\Annotations\Query; use TheCodingMachine\GraphQLite\Annotations\Subscription; @@ -33,27 +33,25 @@ final class GlobControllerQueryProvider implements QueryProviderInterface { /** @var array|null */ private array|null $instancesList = null; - private ClassNameMapper $classNameMapper; + private FinderInterface $finder; private AggregateControllerQueryProvider|null $aggregateControllerQueryProvider = null; private CacheContractInterface $cacheContract; /** * @param string $namespace The namespace that contains the GraphQL types (they must have a `@Type` annotation) * @param ContainerInterface $container The container we will fetch controllers from. - * @param bool $recursive Whether subnamespaces of $namespace must be analyzed. */ public function __construct( - private string $namespace, - private FieldsBuilder $fieldsBuilder, - private ContainerInterface $container, - private AnnotationReader $annotationReader, - private CacheInterface $cache, - ClassNameMapper|null $classNameMapper = null, - private int|null $cacheTtl = null, - private bool $recursive = true, + private readonly string $namespace, + private readonly FieldsBuilder $fieldsBuilder, + private readonly ContainerInterface $container, + private readonly AnnotationReader $annotationReader, + private readonly CacheInterface $cache, + FinderInterface|null $finder = null, + int|null $cacheTtl = null, ) { - $this->classNameMapper = $classNameMapper ?? ClassNameMapper::createFromComposerFile(null, null, true); + $this->finder = $finder ?? new ComposerFinder(); $this->cacheContract = new Psr16Adapter( $this->cache, str_replace(['\\', '{', '}', '(', ')', '/', '@', ':'], '_', $namespace), @@ -96,15 +94,12 @@ private function getInstancesList(): array /** @return array */ private function buildInstancesList(): array { - $explorer = new GlobClassExplorer($this->namespace, $this->cache, $this->cacheTtl, $this->classNameMapper, $this->recursive); - $classes = $explorer->getClasses(); $instances = []; - foreach ($classes as $className) { + foreach ($this->finder->inNamespace($this->namespace) as $className => $refClass) { if (! class_exists($className) && ! interface_exists($className)) { continue; } - $refClass = new ReflectionClass($className); - if (! $refClass->isInstantiable()) { + if (! $refClass instanceof ReflectionClass || ! $refClass->isInstantiable()) { continue; } if (! $this->hasOperations($refClass)) { diff --git a/src/SchemaFactory.php b/src/SchemaFactory.php index 8f23bf5df6..e49f51e8a1 100644 --- a/src/SchemaFactory.php +++ b/src/SchemaFactory.php @@ -8,7 +8,7 @@ use Doctrine\Common\Annotations\PsrCachedReader; use Doctrine\Common\Annotations\Reader; use GraphQL\Type\SchemaConfig; -use Mouf\Composer\ClassNameMapper; +use Kcs\ClassFinder\Finder\FinderInterface; use MyCLabs\Enum\Enum; use PackageVersions\Versions; use Psr\Cache\CacheItemPoolInterface; @@ -109,7 +109,7 @@ class SchemaFactory private NamingStrategyInterface|null $namingStrategy = null; - private ClassNameMapper|null $classNameMapper = null; + private FinderInterface|null $finder = null; private SchemaConfig|null $schemaConfig = null; @@ -262,9 +262,9 @@ public function setSchemaConfig(SchemaConfig $schemaConfig): self return $this; } - public function setClassNameMapper(ClassNameMapper $classNameMapper): self + public function setFinder(FinderInterface $finder): self { - $this->classNameMapper = $classNameMapper; + $this->finder = $finder; return $this; } @@ -344,7 +344,7 @@ public function createSchema(): Schema $namingStrategy = $this->namingStrategy ?: new NamingStrategy(); $typeRegistry = new TypeRegistry(); - $namespaceFactory = new NamespaceFactory($namespacedCache, $this->classNameMapper, $this->globTTL); + $namespaceFactory = new NamespaceFactory($namespacedCache, $this->finder, $this->globTTL); $nsList = array_map( static fn (string $namespace) => $namespaceFactory->createNamespace($namespace), $this->typeNamespaces, @@ -493,7 +493,7 @@ public function createSchema(): Schema $this->container, $annotationReader, $namespacedCache, - $this->classNameMapper, + $this->finder, $this->globTTL, ); } diff --git a/src/Utils/Namespaces/NS.php b/src/Utils/Namespaces/NS.php index 4e6fe98d99..3aaeeb0066 100644 --- a/src/Utils/Namespaces/NS.php +++ b/src/Utils/Namespaces/NS.php @@ -4,13 +4,10 @@ namespace TheCodingMachine\GraphQLite\Utils\Namespaces; -use Mouf\Composer\ClassNameMapper; +use Kcs\ClassFinder\Finder\FinderInterface; use Psr\SimpleCache\CacheInterface; +use Psr\SimpleCache\InvalidArgumentException; use ReflectionClass; -use TheCodingMachine\ClassExplorer\Glob\GlobClassExplorer; - -use function class_exists; -use function interface_exists; /** * The NS class represents a PHP Namespace and provides utility methods to explore those classes. @@ -24,7 +21,7 @@ final class NS * Only instantiable classes are returned. * Key: fully qualified class name * - * @var array> + * @var array> */ private array|null $classes = null; @@ -32,9 +29,8 @@ final class NS public function __construct( private readonly string $namespace, private readonly CacheInterface $cache, - private readonly ClassNameMapper $classNameMapper, + private readonly FinderInterface $finder, private readonly int|null $globTTL, - private readonly bool $recursive, ) { } @@ -47,31 +43,32 @@ public function __construct( public function getClassList(): array { if ($this->classes === null) { - $this->classes = []; - $explorer = new GlobClassExplorer($this->namespace, $this->cache, $this->globTTL, $this->classNameMapper, $this->recursive); - /** @var array $classes Override class-explorer lib */ - $classes = $explorer->getClassMap(); - foreach ($classes as $className => $phpFile) { - if (! class_exists($className, false) && ! interface_exists($className, false)) { - // Let's try to load the file if it was not imported yet. - // We are importing the file manually to avoid triggering the autoloader. - // The autoloader might trigger errors if the file does not respect PSR-4 or if the - // Symfony DebugAutoLoader is installed. (see https://github.com/thecodingmachine/graphqlite/issues/216) - require_once $phpFile; - // Does it exists now? - // @phpstan-ignore-next-line - if (! class_exists($className, false) && ! interface_exists($className, false)) { + $cacheKey = 'GraphQLite_NS_' . $this->namespace; + try { + $this->classes = $this->cache->get($cacheKey); + } catch (InvalidArgumentException) { + $this->classes = null; + } + + if ($this->classes === null) { + $this->classes = []; + /** @var class-string $className */ + /** @var ReflectionClass $reflector */ + foreach ($this->finder->inNamespace($this->namespace) as $className => $reflector) { + if (! ($reflector instanceof ReflectionClass)) { continue; } - } - $refClass = new ReflectionClass($className); - - $this->classes[$className] = $refClass; + $this->classes[$className] = $reflector; + } + try { + $this->cache->set($cacheKey, $this->classes, $this->globTTL); + } catch (InvalidArgumentException) { + // @ignoreException + } } } - // @phpstan-ignore-next-line - Not sure why we cannot annotate the $classes above return $this->classes; } diff --git a/src/Utils/Namespaces/NamespaceFactory.php b/src/Utils/Namespaces/NamespaceFactory.php index 9d1c6d32cf..e4d462cc2a 100644 --- a/src/Utils/Namespaces/NamespaceFactory.php +++ b/src/Utils/Namespaces/NamespaceFactory.php @@ -4,7 +4,8 @@ namespace TheCodingMachine\GraphQLite\Utils\Namespaces; -use Mouf\Composer\ClassNameMapper; +use Kcs\ClassFinder\Finder\ComposerFinder; +use Kcs\ClassFinder\Finder\FinderInterface; use Psr\SimpleCache\CacheInterface; /** @@ -14,16 +15,16 @@ */ final class NamespaceFactory { - private ClassNameMapper $classNameMapper; + private FinderInterface $finder; - public function __construct(private readonly CacheInterface $cache, ClassNameMapper|null $classNameMapper = null, private int|null $globTTL = 2) + public function __construct(private readonly CacheInterface $cache, FinderInterface|null $finder = null, private int|null $globTTL = 2) { - $this->classNameMapper = $classNameMapper ?? ClassNameMapper::createFromComposerFile(null, null, true); + $this->finder = $finder ?? new ComposerFinder(); } /** @param string $namespace A PHP namespace */ - public function createNamespace(string $namespace, bool $recursive = true): NS + public function createNamespace(string $namespace): NS { - return new NS($namespace, $this->cache, $this->classNameMapper, $this->globTTL, $recursive); + return new NS($namespace, $this->cache, clone $this->finder, $this->globTTL); } } diff --git a/tests/GlobControllerQueryProviderTest.php b/tests/GlobControllerQueryProviderTest.php index 8f9674fc37..dedc7d36ab 100644 --- a/tests/GlobControllerQueryProviderTest.php +++ b/tests/GlobControllerQueryProviderTest.php @@ -1,8 +1,12 @@ $controller ]) implements ContainerInterface { - /** - * @var array - */ + $container = new class ([TestController::class => $controller]) implements ContainerInterface { + /** @var array */ private $controllers; public function __construct(array $controllers) @@ -24,26 +26,27 @@ public function __construct(array $controllers) $this->controllers = $controllers; } - public function get($id):mixed + public function get($id): mixed { return $this->controllers[$id]; } - public function has($id):bool + public function has($id): bool { return isset($this->controllers[$id]); } }; + $finder = new ComposerFinder(); + $finder->filter(static fn (ReflectionClass $class) => $class->getNamespaceName() === 'TheCodingMachine\\GraphQLite\\Fixtures'); // Fix for recursive:false $globControllerQueryProvider = new GlobControllerQueryProvider( 'TheCodingMachine\\GraphQLite\\Fixtures', $this->getFieldsBuilder(), $container, $this->getAnnotationReader(), - new Psr16Cache(new NullAdapter), - null, - false, - false, + new Psr16Cache(new NullAdapter()), + $finder, + 0, ); $queries = $globControllerQueryProvider->getQueries(); diff --git a/tests/SchemaFactoryTest.php b/tests/SchemaFactoryTest.php index 9ac3edceb9..d57dc78a11 100644 --- a/tests/SchemaFactoryTest.php +++ b/tests/SchemaFactoryTest.php @@ -9,7 +9,8 @@ use GraphQL\Executor\ExecutionResult; use GraphQL\GraphQL; use GraphQL\Type\SchemaConfig; -use Mouf\Composer\ClassNameMapper; +use Kcs\ClassFinder\Finder\ComposerFinder; +use Kcs\ClassFinder\Finder\RecursiveFinder; use PHPUnit\Framework\TestCase; use Symfony\Component\Cache\Adapter\ArrayAdapter; use Symfony\Component\Cache\Adapter\Psr16Adapter; @@ -89,7 +90,7 @@ public function testSetters(): void $this->doTestSchema($schema); } - public function testClassNameMapperInjectionWithValidMapper(): void + public function testFinderInjectionWithValidMapper(): void { $factory = new SchemaFactory( new Psr16Cache(new ArrayAdapter()), @@ -99,7 +100,7 @@ public function testClassNameMapperInjectionWithValidMapper(): void ); $factory->setAuthenticationService(new VoidAuthenticationService()) ->setAuthorizationService(new VoidAuthorizationService()) - ->setClassNameMapper(ClassNameMapper::createFromComposerFile(null, null, true)) + ->setFinder(new ComposerFinder()) ->addControllerNamespace('TheCodingMachine\\GraphQLite\\Fixtures\\Integration\\Controllers') ->addTypeNamespace('TheCodingMachine\\GraphQLite\\Fixtures\\Integration'); @@ -136,7 +137,7 @@ public function testCreateSchemaOnlyWithFactories(): void $this->doTestSchema($schema); } - public function testClassNameMapperInjectionWithInvalidMapper(): void + public function testFinderInjectionWithInvalidMapper(): void { $factory = new SchemaFactory( new Psr16Cache(new ArrayAdapter()), @@ -146,7 +147,7 @@ public function testClassNameMapperInjectionWithInvalidMapper(): void ); $factory->setAuthenticationService(new VoidAuthenticationService()) ->setAuthorizationService(new VoidAuthorizationService()) - ->setClassNameMapper(new ClassNameMapper()) + ->setFinder(new RecursiveFinder(__DIR__ . '/Annotations')) ->addControllerNamespace('TheCodingMachine\\GraphQLite\\Fixtures\\Integration\\Controllers') ->addTypeNamespace('TheCodingMachine\\GraphQLite\\Fixtures\\Integration');