diff --git a/src/SchemaFactory.php b/src/SchemaFactory.php index 8f23bf5df6..396cc12122 100644 --- a/src/SchemaFactory.php +++ b/src/SchemaFactory.php @@ -78,7 +78,7 @@ class SchemaFactory /** @var array */ private array $controllerNamespaces = []; - /** @var array */ + /** @var array */ private array $typeNamespaces = []; /** @var QueryProviderInterface[] */ @@ -142,10 +142,12 @@ public function addControllerNamespace(string $namespace): self /** * Registers a namespace that can contain GraphQL types. + * + * @param bool $autoload Use autoloading when scanning classes. Set to `false` to use `require_once` instead. */ - public function addTypeNamespace(string $namespace): self + public function addTypeNamespace(string $namespace, bool $autoload = true): self { - $this->typeNamespaces[] = $namespace; + $this->typeNamespaces[] = [$namespace, $autoload]; return $this; } @@ -346,7 +348,11 @@ public function createSchema(): Schema $namespaceFactory = new NamespaceFactory($namespacedCache, $this->classNameMapper, $this->globTTL); $nsList = array_map( - static fn (string $namespace) => $namespaceFactory->createNamespace($namespace), + static function (array $namespacePair) use ($namespaceFactory) { + [$namespace, $autoload] = $namespacePair; + + return $namespaceFactory->createNamespace($namespace, autoload: $autoload); + }, $this->typeNamespaces, ); diff --git a/src/Utils/Namespaces/NS.php b/src/Utils/Namespaces/NS.php index 4e6fe98d99..5c5f1e94f1 100644 --- a/src/Utils/Namespaces/NS.php +++ b/src/Utils/Namespaces/NS.php @@ -22,9 +22,8 @@ final class NS /** * The array of globbed classes. * Only instantiable classes are returned. - * Key: fully qualified class name * - * @var array> + * @var array> */ private array|null $classes = null; @@ -35,6 +34,7 @@ public function __construct( private readonly ClassNameMapper $classNameMapper, private readonly int|null $globTTL, private readonly bool $recursive, + private readonly bool $autoload = true, ) { } @@ -42,7 +42,7 @@ public function __construct( * Returns the array of globbed classes. * Only instantiable classes are returned. * - * @return array> Key: fully qualified class name + * @return array> Key: fully qualified class name */ public function getClassList(): array { @@ -52,26 +52,14 @@ public function getClassList(): array /** @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)) { - continue; - } + if (!$this->loadClass($className, $phpFile)) { + continue; } - $refClass = new ReflectionClass($className); - - $this->classes[$className] = $refClass; + $this->classes[$className] = new ReflectionClass($className); } } - // @phpstan-ignore-next-line - Not sure why we cannot annotate the $classes above return $this->classes; } @@ -79,4 +67,32 @@ public function getNamespace(): string { return $this->namespace; } + + /** + * Attempt to load a class depending on the @see $autoload setting. + * + * @param class-string $className + */ + private function loadClass(string $className, string $phpFile): bool + { + if (class_exists($className, $this->autoload) || interface_exists($className, $this->autoload)) { + return true; + } + + // If autoloading was requested and there's no class by this name, then it's most likely that the + // guessed class name from GlobClassExplorer is simply not a class, so we'll skip it. + // See: https://github.com/thecodingmachine/graphqlite/issues/659 + if ($this->autoload) { + return false; + } + + // Otherwise attempt to load the file without autoloading. + // 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; + + // The class might still not be loaded if guessed class name doesn't match the PHP file, + // so we should check if it got loaded after requiring the PHP file. + return class_exists($className, false) || interface_exists($className, false); + } } diff --git a/src/Utils/Namespaces/NamespaceFactory.php b/src/Utils/Namespaces/NamespaceFactory.php index 9d1c6d32cf..2559fddf57 100644 --- a/src/Utils/Namespaces/NamespaceFactory.php +++ b/src/Utils/Namespaces/NamespaceFactory.php @@ -22,8 +22,15 @@ public function __construct(private readonly CacheInterface $cache, ClassNameMap } /** @param string $namespace A PHP namespace */ - public function createNamespace(string $namespace, bool $recursive = true): NS + public function createNamespace(string $namespace, bool $recursive = true, bool $autoload = true): NS { - return new NS($namespace, $this->cache, $this->classNameMapper, $this->globTTL, $recursive); + return new NS( + namespace: $namespace, + cache: $this->cache, + classNameMapper: $this->classNameMapper, + globTTL: $this->globTTL, + recursive: $recursive, + autoload: $autoload, + ); } } diff --git a/tests/Fixtures/Integration/Models/BadNamespaceClass.php b/tests/Fixtures/BadNamespace/BadNamespaceClass.php similarity index 100% rename from tests/Fixtures/Integration/Models/BadNamespaceClass.php rename to tests/Fixtures/BadNamespace/BadNamespaceClass.php diff --git a/tests/Utils/Namespaces/NSTest.php b/tests/Utils/Namespaces/NSTest.php new file mode 100644 index 0000000000..f51e49adae --- /dev/null +++ b/tests/Utils/Namespaces/NSTest.php @@ -0,0 +1,59 @@ +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', + true, + ]; + + // The class should be ignored. + yield 'incorrect namespace class without autoload' => [ + [], + 'TheCodingMachine\GraphQLite\Fixtures\BadNamespace', + false, + ]; + } +} \ No newline at end of file