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
…664)

* 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

* test: check that incorrect classes don't trigger autoloading errors

Thanks @oprypkhantc

* fix: stop caching reflections

* fix: static checks

* fix: stop using cached classes if restoring fails

* test: improve coverage

* 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.
  • Loading branch information
fogrye authored Mar 20, 2024
1 parent c6f8e08 commit 9fc7f9e
Show file tree
Hide file tree
Showing 11 changed files with 258 additions and 71 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 ((clone $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
70 changes: 46 additions & 24 deletions src/Utils/Namespaces/NS.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,19 @@

namespace TheCodingMachine\GraphQLite\Utils\Namespaces;

use Mouf\Composer\ClassNameMapper;
use Exception;
use Kcs\ClassFinder\Finder\FinderInterface;
use Psr\SimpleCache\CacheException;
use Psr\SimpleCache\CacheInterface;
use Psr\SimpleCache\InvalidArgumentException;
use ReflectionClass;
use TheCodingMachine\ClassExplorer\Glob\GlobClassExplorer;
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.
Expand All @@ -24,18 +30,18 @@ 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 +53,47 @@ 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)) {
continue;
$cacheKey = 'GraphQLite_NS_' . preg_replace('/[\/{}()\\\\@:]/', '', $this->namespace);
try {
$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)
) {
// assume the cache is invalid
throw new class extends Exception implements CacheException {
};
}

$this->classes[$class] = new ReflectionClass($class);
}
}
} catch (CacheException | InvalidArgumentException | ReflectionException) {
$this->classes = null;
}

$refClass = new ReflectionClass($className);
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;
}

$this->classes[$className] = $refClass;
$this->classes[$className] = $reflector;
}
try {
$this->cache->set($cacheKey, array_keys($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);
}
}
8 changes: 8 additions & 0 deletions tests/Fixtures/BadNamespace/BadlyNamespacedClass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace TheCodingMachine\GraphQLite\Fixtures\BadNamespace\None;

class BadlyNamespacedClass
{

}
5 changes: 5 additions & 0 deletions tests/Fixtures/BadNamespace/ClassWithoutNamespace.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?php
class ClassWithoutNamespace
{

}
8 changes: 8 additions & 0 deletions tests/Fixtures/Types/EnumType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace TheCodingMachine\GraphQLite\Fixtures\Types;

enum EnumType
{

}
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
Loading

0 comments on commit 9fc7f9e

Please sign in to comment.