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 1/7] 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'); From 19054ebc0b359da7b8c19439d7a0a92f1ae0b02c Mon Sep 17 00:00:00 2001 From: Yurii Kuvshynov <141632421+fogrye@users.noreply.github.com> Date: Thu, 14 Mar 2024 21:00:44 +0100 Subject: [PATCH 2/7] test: check that incorrect classes don't trigger autoloading errors Thanks @oprypkhantc --- .../BadNamespace/BadlyNamespacedClass.php | 8 +++ .../BadNamespace/ClassWithoutNamespace.php | 5 ++ tests/Utils/NsTest.php | 56 +++++++++++++++++++ 3 files changed, 69 insertions(+) create mode 100644 tests/Fixtures/BadNamespace/BadlyNamespacedClass.php create mode 100644 tests/Fixtures/BadNamespace/ClassWithoutNamespace.php create mode 100644 tests/Utils/NsTest.php diff --git a/tests/Fixtures/BadNamespace/BadlyNamespacedClass.php b/tests/Fixtures/BadNamespace/BadlyNamespacedClass.php new file mode 100644 index 0000000000..258dac6fd5 --- /dev/null +++ b/tests/Fixtures/BadNamespace/BadlyNamespacedClass.php @@ -0,0 +1,8 @@ +getClassList())); + } + + public static function loadsClassListProvider(): iterable + { + yield 'autoload' => [ + [ + TestFactory::class, + GetterSetterType::class, + FooType::class, + MagicGetterSetterType::class, + FooExtendType::class, + NoTypeAnnotation::class, + AbstractFooType::class, + ], + 'TheCodingMachine\GraphQLite\Fixtures\Types', + ]; + + // The class should be ignored. + yield 'incorrect namespace class without autoload' => [ + [], + 'TheCodingMachine\GraphQLite\Fixtures\BadNamespace', + ]; + } +} \ No newline at end of file From 56b16b9afc9da536df5868f4732d18b655ae0ac3 Mon Sep 17 00:00:00 2001 From: Yurii Kuvshynov <141632421+fogrye@users.noreply.github.com> Date: Thu, 14 Mar 2024 21:48:59 +0100 Subject: [PATCH 3/7] fix: stop caching reflections --- src/Utils/Namespaces/NS.php | 20 +++++++++++++++++--- tests/Utils/NsTest.php | 25 +++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/src/Utils/Namespaces/NS.php b/src/Utils/Namespaces/NS.php index 3aaeeb0066..acd5b98694 100644 --- a/src/Utils/Namespaces/NS.php +++ b/src/Utils/Namespaces/NS.php @@ -8,6 +8,7 @@ use Psr\SimpleCache\CacheInterface; use Psr\SimpleCache\InvalidArgumentException; use ReflectionClass; +use ReflectionException; /** * The NS class represents a PHP Namespace and provides utility methods to explore those classes. @@ -43,9 +44,22 @@ public function __construct( public function getClassList(): array { if ($this->classes === null) { - $cacheKey = 'GraphQLite_NS_' . $this->namespace; + $cacheKey = 'GraphQLite_NS_' . preg_replace('/[\/{}()\\\\@:]/', '', $this->namespace); try { - $this->classes = $this->cache->get($cacheKey); + $classes = $this->cache->get($cacheKey); + if ($classes !== null) { + foreach ($classes as $class) { + if (class_exists($class, false) || + interface_exists($class, false) || + trait_exists($class, false)) { + try { + $this->classes[$class] = new ReflectionClass($class); + } catch (ReflectionException) { + // @ignoreException + } + } + } + } } catch (InvalidArgumentException) { $this->classes = null; } @@ -62,7 +76,7 @@ public function getClassList(): array $this->classes[$className] = $reflector; } try { - $this->cache->set($cacheKey, $this->classes, $this->globTTL); + $this->cache->set($cacheKey, array_keys($this->classes), $this->globTTL); } catch (InvalidArgumentException) { // @ignoreException } diff --git a/tests/Utils/NsTest.php b/tests/Utils/NsTest.php index 464b036749..1904e66708 100644 --- a/tests/Utils/NsTest.php +++ b/tests/Utils/NsTest.php @@ -3,6 +3,7 @@ namespace TheCodingMachine\GraphQLite\Utils; use Kcs\ClassFinder\Finder\ComposerFinder; +use Kcs\ClassFinder\Finder\FinderInterface; use PHPUnit\Framework\TestCase; use Symfony\Component\Cache\Adapter\ArrayAdapter; use Symfony\Component\Cache\Psr16Cache; @@ -53,4 +54,28 @@ public static function loadsClassListProvider(): iterable 'TheCodingMachine\GraphQLite\Fixtures\BadNamespace', ]; } + + public function testCaching(){ + $cache = new Psr16Cache(new ArrayAdapter()); + $finder = new ComposerFinder(); + $namespace = 'TheCodingMachine\GraphQLite\Fixtures\Types'; + $ns = new NS( + namespace: $namespace, + cache: $cache, + finder: $finder, + globTTL: 10 + ); + self::assertNotNull($ns->getClassList()); + + // create with mock finder to test cache + $finder = $this->createMock(FinderInterface::class); + $finder->expects(self::never())->method('inNamespace')->willReturnSelf(); + $ns = new NS( + namespace: $namespace, + cache: $cache, + finder: $finder, + globTTL: 10 + ); + self::assertNotNull($ns->getClassList()); + } } \ No newline at end of file From f66e9e5b1320e62f91ba656c0c4ab9469f1ba1ef Mon Sep 17 00:00:00 2001 From: Yurii Kuvshynov <141632421+fogrye@users.noreply.github.com> Date: Thu, 14 Mar 2024 21:54:07 +0100 Subject: [PATCH 4/7] fix: static checks --- src/Utils/Namespaces/NS.php | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/Utils/Namespaces/NS.php b/src/Utils/Namespaces/NS.php index acd5b98694..db1724d9a7 100644 --- a/src/Utils/Namespaces/NS.php +++ b/src/Utils/Namespaces/NS.php @@ -10,6 +10,12 @@ use ReflectionClass; use ReflectionException; +use function array_keys; +use function class_exists; +use function interface_exists; +use function preg_replace; +use function trait_exists; + /** * The NS class represents a PHP Namespace and provides utility methods to explore those classes. * @@ -49,18 +55,18 @@ public function getClassList(): array $classes = $this->cache->get($cacheKey); if ($classes !== null) { foreach ($classes as $class) { - if (class_exists($class, false) || - interface_exists($class, false) || - trait_exists($class, false)) { - try { - $this->classes[$class] = new ReflectionClass($class); - } catch (ReflectionException) { - // @ignoreException - } + if ( + ! class_exists($class, false) && + ! interface_exists($class, false) && + ! trait_exists($class, false) + ) { + continue; } + + $this->classes[$class] = new ReflectionClass($class); } } - } catch (InvalidArgumentException) { + } catch (InvalidArgumentException | ReflectionException) { $this->classes = null; } From d027c1ebe67d3c742cb9e087c07c34dcdb0cc330 Mon Sep 17 00:00:00 2001 From: Yurii Kuvshynov <141632421+fogrye@users.noreply.github.com> Date: Sat, 16 Mar 2024 13:20:29 +0100 Subject: [PATCH 5/7] fix: stop using cached classes if restoring fails --- src/Utils/Namespaces/NS.php | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/Utils/Namespaces/NS.php b/src/Utils/Namespaces/NS.php index db1724d9a7..262a42494b 100644 --- a/src/Utils/Namespaces/NS.php +++ b/src/Utils/Namespaces/NS.php @@ -4,7 +4,9 @@ namespace TheCodingMachine\GraphQLite\Utils\Namespaces; +use Exception; use Kcs\ClassFinder\Finder\FinderInterface; +use Psr\SimpleCache\CacheException; use Psr\SimpleCache\CacheInterface; use Psr\SimpleCache\InvalidArgumentException; use ReflectionClass; @@ -38,7 +40,8 @@ public function __construct( private readonly CacheInterface $cache, private readonly FinderInterface $finder, private readonly int|null $globTTL, - ) { + ) + { } /** @@ -60,13 +63,15 @@ public function getClassList(): array ! interface_exists($class, false) && ! trait_exists($class, false) ) { - continue; + // assume the cache is invalid + throw new class extends Exception implements CacheException { + }; } - $this->classes[$class] = new ReflectionClass($class); + $this->classes[$class] = new ReflectionClass($class); } } - } catch (InvalidArgumentException | ReflectionException) { + } catch (CacheException | InvalidArgumentException | ReflectionException) { $this->classes = null; } From 06e73f683c0c7629bee549c5344acf6e2c03373b Mon Sep 17 00:00:00 2001 From: Yurii Kuvshynov <141632421+fogrye@users.noreply.github.com> Date: Sat, 16 Mar 2024 13:20:50 +0100 Subject: [PATCH 6/7] test: improve coverage --- tests/Fixtures/Types/EnumType.php | 8 +++ tests/Utils/NsTest.php | 101 ++++++++++++++++++++++++------ 2 files changed, 90 insertions(+), 19 deletions(-) create mode 100644 tests/Fixtures/Types/EnumType.php diff --git a/tests/Fixtures/Types/EnumType.php b/tests/Fixtures/Types/EnumType.php new file mode 100644 index 0000000000..65b04334d8 --- /dev/null +++ b/tests/Fixtures/Types/EnumType.php @@ -0,0 +1,8 @@ +cache = new Psr16Cache(new ArrayAdapter()); + $this->namespace = 'TheCodingMachine\GraphQLite\Fixtures\Types'; + $this->finder = new ComposerFinder(); + $this->globTTL = 10; + } + + /** @dataProvider loadsClassListProvider */ public function testLoadsClassList(array $expectedClasses, string $namespace): void { $ns = new NS( namespace: $namespace, - cache: new Psr16Cache(new ArrayAdapter()), - finder: new ComposerFinder(), - globTTL: null + cache: $this->cache, + finder: $this->finder, + globTTL: null, ); self::assertEqualsCanonicalizing($expectedClasses, array_keys($ns->getClassList())); @@ -44,6 +62,7 @@ public static function loadsClassListProvider(): iterable FooExtendType::class, NoTypeAnnotation::class, AbstractFooType::class, + EnumType::class ], 'TheCodingMachine\GraphQLite\Fixtures\Types', ]; @@ -55,15 +74,13 @@ public static function loadsClassListProvider(): iterable ]; } - public function testCaching(){ - $cache = new Psr16Cache(new ArrayAdapter()); - $finder = new ComposerFinder(); - $namespace = 'TheCodingMachine\GraphQLite\Fixtures\Types'; + public function testCaching(): void + { $ns = new NS( - namespace: $namespace, - cache: $cache, - finder: $finder, - globTTL: 10 + namespace: $this->namespace, + cache: $this->cache, + finder: $this->finder, + globTTL: $this->globTTL, ); self::assertNotNull($ns->getClassList()); @@ -71,11 +88,57 @@ public function testCaching(){ $finder = $this->createMock(FinderInterface::class); $finder->expects(self::never())->method('inNamespace')->willReturnSelf(); $ns = new NS( - namespace: $namespace, - cache: $cache, + namespace: $this->namespace, + cache: $this->cache, finder: $finder, - globTTL: 10 + globTTL: $this->globTTL, ); self::assertNotNull($ns->getClassList()); } -} \ No newline at end of file + + public function testCachingWithInvalidKey(): void + { + $exception = new class extends Exception implements InvalidArgumentException { + }; + $cache = $this->createMock(CacheInterface::class); + $cache->expects(self::once())->method('get')->willThrowException($exception); + $cache->expects(self::once())->method('set')->willThrowException($exception); + $ns = new NS( + namespace: $this->namespace, + cache: $cache, + finder: $this->finder, + globTTL: $this->globTTL, + ); + $ns->getClassList(); + } + + public function testCachingWithInvalidCache(): void + { + $cache = $this->createMock(CacheInterface::class); + $cache->expects(self::once())->method('get')->willReturn(['foo']); + $ns = new NS( + namespace: $this->namespace, + cache: $cache, + finder: $this->finder, + globTTL: $this->globTTL, + ); + $classList = $ns->getClassList(); + self::assertNotNull($classList); + self::assertNotEmpty($classList); + } + + public function testFinderWithUnexpectedOutput() { + + $finder = $this->createMock(FinderInterface::class); + $finder->expects(self::once())->method('inNamespace')->willReturnSelf(); + $finder->expects(self::once())->method('getIterator')->willReturn(new \ArrayIterator([ 'test' => new \ReflectionException()])); + $ns = new NS( + namespace: $this->namespace, + cache: $this->cache, + finder: $finder, + globTTL: $this->globTTL, + ); + $classList = $ns->getClassList(); + self::assertNotNull($classList); + self::assertEmpty($classList);} +} From 90d9b95bbfea612e04facfaa04e0f7cf911113d2 Mon Sep 17 00:00:00 2001 From: Yurii Kuvshynov <141632421+fogrye@users.noreply.github.com> Date: Tue, 19 Mar 2024 13:55:44 +0100 Subject: [PATCH 7/7] fix: prevent possible issue with long-running apps As each condition applied to finder stays there each place it is applied should be done on cloned instance to avoid accumulation. Main instance of finder is kept with all the conditions provided during configuring. NS is created only from NSFctory where it's cloned so no need to add same clone inside NS. --- src/GlobControllerQueryProvider.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/GlobControllerQueryProvider.php b/src/GlobControllerQueryProvider.php index eac742a5ba..6fc7e659d1 100644 --- a/src/GlobControllerQueryProvider.php +++ b/src/GlobControllerQueryProvider.php @@ -95,7 +95,7 @@ private function getInstancesList(): array private function buildInstancesList(): array { $instances = []; - foreach ($this->finder->inNamespace($this->namespace) as $className => $refClass) { + foreach ((clone $this->finder)->inNamespace($this->namespace) as $className => $refClass) { if (! class_exists($className) && ! interface_exists($className)) { continue; }