Skip to content

Commit

Permalink
Allow loading types with autoloading
Browse files Browse the repository at this point in the history
  • Loading branch information
oprypkhantc committed Mar 13, 2024
1 parent 256da31 commit bf3f749
Show file tree
Hide file tree
Showing 5 changed files with 112 additions and 24 deletions.
14 changes: 10 additions & 4 deletions src/SchemaFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ class SchemaFactory
/** @var array<int,string> */
private array $controllerNamespaces = [];

/** @var array<int,string> */
/** @var array<int, array{ string, bool }> */
private array $typeNamespaces = [];

/** @var QueryProviderInterface[] */
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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,
);

Expand Down
52 changes: 34 additions & 18 deletions src/Utils/Namespaces/NS.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,8 @@ final class NS
/**
* The array of globbed classes.
* Only instantiable classes are returned.
* Key: fully qualified class name
*
* @var array<string,ReflectionClass<object>>
* @var array<class-string, ReflectionClass<object>>
*/
private array|null $classes = null;

Expand All @@ -35,14 +34,15 @@ public function __construct(
private readonly ClassNameMapper $classNameMapper,
private readonly int|null $globTTL,
private readonly bool $recursive,
private readonly bool $autoload = true,
) {
}

/**
* Returns the array of globbed classes.
* Only instantiable classes are returned.
*
* @return array<class-string,ReflectionClass<object>> Key: fully qualified class name
* @return array<class-string, ReflectionClass<object>> Key: fully qualified class name
*/
public function getClassList(): array
{
Expand All @@ -52,31 +52,47 @@ public function getClassList(): array
/** @var array<class-string, string> $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;
}

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);
}
}
11 changes: 9 additions & 2 deletions src/Utils/Namespaces/NamespaceFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
);
}
}
59 changes: 59 additions & 0 deletions tests/Utils/Namespaces/NSTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

namespace TheCodingMachine\GraphQLite\Utils\Namespaces;

use Mouf\Composer\ClassNameMapper;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
use Symfony\Component\Cache\Psr16Cache;
use TheCodingMachine\GraphQLite\Fixtures\Types\AbstractFooType;
use TheCodingMachine\GraphQLite\Fixtures\Types\FooExtendType;
use TheCodingMachine\GraphQLite\Fixtures\Types\FooType;
use TheCodingMachine\GraphQLite\Fixtures\Types\GetterSetterType;
use TheCodingMachine\GraphQLite\Fixtures\Types\MagicGetterSetterType;
use TheCodingMachine\GraphQLite\Fixtures\Types\NoTypeAnnotation;
use TheCodingMachine\GraphQLite\Fixtures\Types\TestFactory;

class NSTest extends TestCase
{
/**
* @dataProvider loadsClassListProvider
*/
public function testLoadsClassList(array $expectedClasses, string $namespace, bool $autoload): void
{
$ns = new NS(
namespace: $namespace,
cache: new Psr16Cache(new ArrayAdapter()),
classNameMapper: ClassNameMapper::createFromComposerFile(null, null, true),
globTTL: null,
recursive: true,
autoload: $autoload,
);

self::assertSame($expectedClasses, array_keys($ns->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,
];
}
}

0 comments on commit bf3f749

Please sign in to comment.