Skip to content

Commit

Permalink
feat: replace thecodingmachine/class-explorer with kcs/class-finder
Browse files Browse the repository at this point in the history
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
  • Loading branch information
fogrye committed Mar 14, 2024
1 parent 256da31 commit 5414813
Show file tree
Hide file tree
Showing 7 changed files with 71 additions and 74 deletions.
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
31 changes: 13 additions & 18 deletions src/GlobControllerQueryProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -33,27 +33,25 @@ final class GlobControllerQueryProvider implements QueryProviderInterface
{
/** @var array<int,string>|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),
Expand Down Expand Up @@ -96,15 +94,12 @@ private function getInstancesList(): array
/** @return array<int,string> */
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)) {
Expand Down
12 changes: 6 additions & 6 deletions src/SchemaFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -493,7 +493,7 @@ public function createSchema(): Schema
$this->container,
$annotationReader,
$namespacedCache,
$this->classNameMapper,
$this->finder,
$this->globTTL,
);
}
Expand Down
51 changes: 24 additions & 27 deletions src/Utils/Namespaces/NS.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -24,17 +21,16 @@ final class NS
* 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;

/** @param string $namespace The namespace that contains the GraphQL types (they must have a `@Type` annotation) */
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,
) {
}

Expand All @@ -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<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)) {
$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<object> $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;
}

Expand Down
13 changes: 7 additions & 6 deletions src/Utils/Namespaces/NamespaceFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -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);
}
}
23 changes: 13 additions & 10 deletions tests/GlobControllerQueryProviderTest.php
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
<?php

declare(strict_types=1);

namespace TheCodingMachine\GraphQLite;

use Kcs\ClassFinder\Finder\ComposerFinder;
use Psr\Container\ContainerInterface;
use ReflectionClass;
use Symfony\Component\Cache\Adapter\NullAdapter;
use Symfony\Component\Cache\Psr16Cache;
use TheCodingMachine\GraphQLite\Fixtures\TestController;
Expand All @@ -13,37 +17,36 @@ public function testGlob(): void
{
$controller = new TestController();

$container = new class([ TestController::class => $controller ]) implements ContainerInterface {
/**
* @var array
*/
$container = new class ([TestController::class => $controller]) implements ContainerInterface {
/** @var array */
private $controllers;

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();
Expand Down
11 changes: 6 additions & 5 deletions tests/SchemaFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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()),
Expand All @@ -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');

Expand Down Expand Up @@ -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()),
Expand All @@ -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');

Expand Down

0 comments on commit 5414813

Please sign in to comment.