Skip to content

Commit 9fc7f9e

Browse files
authored
feat: replace thecodingmachine/class-explorer with kcs/class-finder (#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.
1 parent c6f8e08 commit 9fc7f9e

File tree

11 files changed

+258
-71
lines changed

11 files changed

+258
-71
lines changed

composer.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@
2525
"symfony/cache": "^4.3 || ^5 || ^6 || ^7",
2626
"symfony/expression-language": "^4 || ^5 || ^6 || ^7",
2727
"thecodingmachine/cache-utils": "^1",
28-
"thecodingmachine/class-explorer": "^1.1.0",
29-
"webonyx/graphql-php": "^v15.0"
28+
"webonyx/graphql-php": "^v15.0",
29+
"kcs/class-finder": "^0.4.0"
3030
},
3131
"require-dev": {
3232
"beberlei/porpaginas": "^1.2 || ^2.0",

src/GlobControllerQueryProvider.php

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@
66

77
use GraphQL\Type\Definition\FieldDefinition;
88
use InvalidArgumentException;
9-
use Mouf\Composer\ClassNameMapper;
9+
use Kcs\ClassFinder\Finder\ComposerFinder;
10+
use Kcs\ClassFinder\Finder\FinderInterface;
1011
use Psr\Container\ContainerInterface;
1112
use Psr\SimpleCache\CacheInterface;
1213
use ReflectionClass;
1314
use ReflectionMethod;
1415
use Symfony\Component\Cache\Adapter\Psr16Adapter;
1516
use Symfony\Contracts\Cache\CacheInterface as CacheContractInterface;
16-
use TheCodingMachine\ClassExplorer\Glob\GlobClassExplorer;
1717
use TheCodingMachine\GraphQLite\Annotations\Mutation;
1818
use TheCodingMachine\GraphQLite\Annotations\Query;
1919
use TheCodingMachine\GraphQLite\Annotations\Subscription;
@@ -33,27 +33,25 @@ final class GlobControllerQueryProvider implements QueryProviderInterface
3333
{
3434
/** @var array<int,string>|null */
3535
private array|null $instancesList = null;
36-
private ClassNameMapper $classNameMapper;
36+
private FinderInterface $finder;
3737
private AggregateControllerQueryProvider|null $aggregateControllerQueryProvider = null;
3838
private CacheContractInterface $cacheContract;
3939

4040
/**
4141
* @param string $namespace The namespace that contains the GraphQL types (they must have a `@Type` annotation)
4242
* @param ContainerInterface $container The container we will fetch controllers from.
43-
* @param bool $recursive Whether subnamespaces of $namespace must be analyzed.
4443
*/
4544
public function __construct(
46-
private string $namespace,
47-
private FieldsBuilder $fieldsBuilder,
48-
private ContainerInterface $container,
49-
private AnnotationReader $annotationReader,
50-
private CacheInterface $cache,
51-
ClassNameMapper|null $classNameMapper = null,
52-
private int|null $cacheTtl = null,
53-
private bool $recursive = true,
45+
private readonly string $namespace,
46+
private readonly FieldsBuilder $fieldsBuilder,
47+
private readonly ContainerInterface $container,
48+
private readonly AnnotationReader $annotationReader,
49+
private readonly CacheInterface $cache,
50+
FinderInterface|null $finder = null,
51+
int|null $cacheTtl = null,
5452
)
5553
{
56-
$this->classNameMapper = $classNameMapper ?? ClassNameMapper::createFromComposerFile(null, null, true);
54+
$this->finder = $finder ?? new ComposerFinder();
5755
$this->cacheContract = new Psr16Adapter(
5856
$this->cache,
5957
str_replace(['\\', '{', '}', '(', ')', '/', '@', ':'], '_', $namespace),
@@ -96,15 +94,12 @@ private function getInstancesList(): array
9694
/** @return array<int,string> */
9795
private function buildInstancesList(): array
9896
{
99-
$explorer = new GlobClassExplorer($this->namespace, $this->cache, $this->cacheTtl, $this->classNameMapper, $this->recursive);
100-
$classes = $explorer->getClasses();
10197
$instances = [];
102-
foreach ($classes as $className) {
98+
foreach ((clone $this->finder)->inNamespace($this->namespace) as $className => $refClass) {
10399
if (! class_exists($className) && ! interface_exists($className)) {
104100
continue;
105101
}
106-
$refClass = new ReflectionClass($className);
107-
if (! $refClass->isInstantiable()) {
102+
if (! $refClass instanceof ReflectionClass || ! $refClass->isInstantiable()) {
108103
continue;
109104
}
110105
if (! $this->hasOperations($refClass)) {

src/SchemaFactory.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
use Doctrine\Common\Annotations\PsrCachedReader;
99
use Doctrine\Common\Annotations\Reader;
1010
use GraphQL\Type\SchemaConfig;
11-
use Mouf\Composer\ClassNameMapper;
11+
use Kcs\ClassFinder\Finder\FinderInterface;
1212
use MyCLabs\Enum\Enum;
1313
use PackageVersions\Versions;
1414
use Psr\Cache\CacheItemPoolInterface;
@@ -109,7 +109,7 @@ class SchemaFactory
109109

110110
private NamingStrategyInterface|null $namingStrategy = null;
111111

112-
private ClassNameMapper|null $classNameMapper = null;
112+
private FinderInterface|null $finder = null;
113113

114114
private SchemaConfig|null $schemaConfig = null;
115115

@@ -262,9 +262,9 @@ public function setSchemaConfig(SchemaConfig $schemaConfig): self
262262
return $this;
263263
}
264264

265-
public function setClassNameMapper(ClassNameMapper $classNameMapper): self
265+
public function setFinder(FinderInterface $finder): self
266266
{
267-
$this->classNameMapper = $classNameMapper;
267+
$this->finder = $finder;
268268

269269
return $this;
270270
}
@@ -344,7 +344,7 @@ public function createSchema(): Schema
344344
$namingStrategy = $this->namingStrategy ?: new NamingStrategy();
345345
$typeRegistry = new TypeRegistry();
346346

347-
$namespaceFactory = new NamespaceFactory($namespacedCache, $this->classNameMapper, $this->globTTL);
347+
$namespaceFactory = new NamespaceFactory($namespacedCache, $this->finder, $this->globTTL);
348348
$nsList = array_map(
349349
static fn (string $namespace) => $namespaceFactory->createNamespace($namespace),
350350
$this->typeNamespaces,
@@ -493,7 +493,7 @@ public function createSchema(): Schema
493493
$this->container,
494494
$annotationReader,
495495
$namespacedCache,
496-
$this->classNameMapper,
496+
$this->finder,
497497
$this->globTTL,
498498
);
499499
}

src/Utils/Namespaces/NS.php

Lines changed: 46 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,19 @@
44

55
namespace TheCodingMachine\GraphQLite\Utils\Namespaces;
66

7-
use Mouf\Composer\ClassNameMapper;
7+
use Exception;
8+
use Kcs\ClassFinder\Finder\FinderInterface;
9+
use Psr\SimpleCache\CacheException;
810
use Psr\SimpleCache\CacheInterface;
11+
use Psr\SimpleCache\InvalidArgumentException;
912
use ReflectionClass;
10-
use TheCodingMachine\ClassExplorer\Glob\GlobClassExplorer;
13+
use ReflectionException;
1114

15+
use function array_keys;
1216
use function class_exists;
1317
use function interface_exists;
18+
use function preg_replace;
19+
use function trait_exists;
1420

1521
/**
1622
* The NS class represents a PHP Namespace and provides utility methods to explore those classes.
@@ -24,18 +30,18 @@ final class NS
2430
* Only instantiable classes are returned.
2531
* Key: fully qualified class name
2632
*
27-
* @var array<string,ReflectionClass<object>>
33+
* @var array<class-string,ReflectionClass<object>>
2834
*/
2935
private array|null $classes = null;
3036

3137
/** @param string $namespace The namespace that contains the GraphQL types (they must have a `@Type` annotation) */
3238
public function __construct(
3339
private readonly string $namespace,
3440
private readonly CacheInterface $cache,
35-
private readonly ClassNameMapper $classNameMapper,
41+
private readonly FinderInterface $finder,
3642
private readonly int|null $globTTL,
37-
private readonly bool $recursive,
38-
) {
43+
)
44+
{
3945
}
4046

4147
/**
@@ -47,31 +53,47 @@ public function __construct(
4753
public function getClassList(): array
4854
{
4955
if ($this->classes === null) {
50-
$this->classes = [];
51-
$explorer = new GlobClassExplorer($this->namespace, $this->cache, $this->globTTL, $this->classNameMapper, $this->recursive);
52-
/** @var array<class-string, string> $classes Override class-explorer lib */
53-
$classes = $explorer->getClassMap();
54-
foreach ($classes as $className => $phpFile) {
55-
if (! class_exists($className, false) && ! interface_exists($className, false)) {
56-
// Let's try to load the file if it was not imported yet.
57-
// We are importing the file manually to avoid triggering the autoloader.
58-
// The autoloader might trigger errors if the file does not respect PSR-4 or if the
59-
// Symfony DebugAutoLoader is installed. (see https://github.com/thecodingmachine/graphqlite/issues/216)
60-
require_once $phpFile;
61-
// Does it exists now?
62-
// @phpstan-ignore-next-line
63-
if (! class_exists($className, false) && ! interface_exists($className, false)) {
64-
continue;
56+
$cacheKey = 'GraphQLite_NS_' . preg_replace('/[\/{}()\\\\@:]/', '', $this->namespace);
57+
try {
58+
$classes = $this->cache->get($cacheKey);
59+
if ($classes !== null) {
60+
foreach ($classes as $class) {
61+
if (
62+
! class_exists($class, false) &&
63+
! interface_exists($class, false) &&
64+
! trait_exists($class, false)
65+
) {
66+
// assume the cache is invalid
67+
throw new class extends Exception implements CacheException {
68+
};
69+
}
70+
71+
$this->classes[$class] = new ReflectionClass($class);
6572
}
6673
}
74+
} catch (CacheException | InvalidArgumentException | ReflectionException) {
75+
$this->classes = null;
76+
}
6777

68-
$refClass = new ReflectionClass($className);
78+
if ($this->classes === null) {
79+
$this->classes = [];
80+
/** @var class-string $className */
81+
/** @var ReflectionClass<object> $reflector */
82+
foreach ($this->finder->inNamespace($this->namespace) as $className => $reflector) {
83+
if (! ($reflector instanceof ReflectionClass)) {
84+
continue;
85+
}
6986

70-
$this->classes[$className] = $refClass;
87+
$this->classes[$className] = $reflector;
88+
}
89+
try {
90+
$this->cache->set($cacheKey, array_keys($this->classes), $this->globTTL);
91+
} catch (InvalidArgumentException) {
92+
// @ignoreException
93+
}
7194
}
7295
}
7396

74-
// @phpstan-ignore-next-line - Not sure why we cannot annotate the $classes above
7597
return $this->classes;
7698
}
7799

src/Utils/Namespaces/NamespaceFactory.php

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44

55
namespace TheCodingMachine\GraphQLite\Utils\Namespaces;
66

7-
use Mouf\Composer\ClassNameMapper;
7+
use Kcs\ClassFinder\Finder\ComposerFinder;
8+
use Kcs\ClassFinder\Finder\FinderInterface;
89
use Psr\SimpleCache\CacheInterface;
910

1011
/**
@@ -14,16 +15,16 @@
1415
*/
1516
final class NamespaceFactory
1617
{
17-
private ClassNameMapper $classNameMapper;
18+
private FinderInterface $finder;
1819

19-
public function __construct(private readonly CacheInterface $cache, ClassNameMapper|null $classNameMapper = null, private int|null $globTTL = 2)
20+
public function __construct(private readonly CacheInterface $cache, FinderInterface|null $finder = null, private int|null $globTTL = 2)
2021
{
21-
$this->classNameMapper = $classNameMapper ?? ClassNameMapper::createFromComposerFile(null, null, true);
22+
$this->finder = $finder ?? new ComposerFinder();
2223
}
2324

2425
/** @param string $namespace A PHP namespace */
25-
public function createNamespace(string $namespace, bool $recursive = true): NS
26+
public function createNamespace(string $namespace): NS
2627
{
27-
return new NS($namespace, $this->cache, $this->classNameMapper, $this->globTTL, $recursive);
28+
return new NS($namespace, $this->cache, clone $this->finder, $this->globTTL);
2829
}
2930
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace TheCodingMachine\GraphQLite\Fixtures\BadNamespace\None;
4+
5+
class BadlyNamespacedClass
6+
{
7+
8+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?php
2+
class ClassWithoutNamespace
3+
{
4+
5+
}

tests/Fixtures/Types/EnumType.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace TheCodingMachine\GraphQLite\Fixtures\Types;
4+
5+
enum EnumType
6+
{
7+
8+
}

tests/GlobControllerQueryProviderTest.php

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
<?php
22

3+
declare(strict_types=1);
4+
35
namespace TheCodingMachine\GraphQLite;
46

7+
use Kcs\ClassFinder\Finder\ComposerFinder;
58
use Psr\Container\ContainerInterface;
9+
use ReflectionClass;
610
use Symfony\Component\Cache\Adapter\NullAdapter;
711
use Symfony\Component\Cache\Psr16Cache;
812
use TheCodingMachine\GraphQLite\Fixtures\TestController;
@@ -13,37 +17,36 @@ public function testGlob(): void
1317
{
1418
$controller = new TestController();
1519

16-
$container = new class([ TestController::class => $controller ]) implements ContainerInterface {
17-
/**
18-
* @var array
19-
*/
20+
$container = new class ([TestController::class => $controller]) implements ContainerInterface {
21+
/** @var array */
2022
private $controllers;
2123

2224
public function __construct(array $controllers)
2325
{
2426
$this->controllers = $controllers;
2527
}
2628

27-
public function get($id):mixed
29+
public function get($id): mixed
2830
{
2931
return $this->controllers[$id];
3032
}
3133

32-
public function has($id):bool
34+
public function has($id): bool
3335
{
3436
return isset($this->controllers[$id]);
3537
}
3638
};
3739

40+
$finder = new ComposerFinder();
41+
$finder->filter(static fn (ReflectionClass $class) => $class->getNamespaceName() === 'TheCodingMachine\\GraphQLite\\Fixtures'); // Fix for recursive:false
3842
$globControllerQueryProvider = new GlobControllerQueryProvider(
3943
'TheCodingMachine\\GraphQLite\\Fixtures',
4044
$this->getFieldsBuilder(),
4145
$container,
4246
$this->getAnnotationReader(),
43-
new Psr16Cache(new NullAdapter),
44-
null,
45-
false,
46-
false,
47+
new Psr16Cache(new NullAdapter()),
48+
$finder,
49+
0,
4750
);
4851

4952
$queries = $globControllerQueryProvider->getQueries();

0 commit comments

Comments
 (0)