From 592568c1060f3a3f600c118b69c1e7f5b5f96ed1 Mon Sep 17 00:00:00 2001 From: Oleksandr Prypkhan Date: Tue, 24 Sep 2024 06:22:36 +0300 Subject: [PATCH] Performance optimizations and caching (#698) * Combine addTypeNamespace() and addControllerNamespace() into addNamespace() * Refactor CachedDocBlockFactory to use existing caching infrastructure * Refactor field name * Refactor code to use ClassFinder instead of separating namespaces * Make caches only recompute on file changes * Refactor EnumTypeMapper to use cached doc block factory * Fix FileModificationClassFinderBoundCache * Use Kcs ClassFinder cache * One more tiny optimization for unchanged cache * Change name of ClassFinderBoundCache * Code style * PHPStan * Some fixes and renames * Fix broken class bound cache after merge * Fix some tests and PHPStan * Fix all failing tests * Fix one failing test on CI * Fix one failing test on CI, again * Fix --prefer-lowest tests * Some simplifications and tests * Simplify cached doc blocks * Simplify cached doc blocks * More tests for doc block factories * Tests for the Discovery namespace * Fix the docs build on CI and broken links * Add a changelog entry * Deprecate setGlobTTL() instead of removing it * Code style --- .github/workflows/doc_generation.yml | 2 +- composer.json | 4 +- examples/no-framework/index.php | 3 +- src/AggregateControllerQueryProvider.php | 35 ++-- src/Cache/ClassBoundCache.php | 24 +++ src/Cache/ClassBoundCacheContract.php | 43 ---- src/Cache/ClassBoundCacheContractFactory.php | 15 -- ...lassBoundCacheContractFactoryInterface.php | 12 -- .../ClassBoundCacheContractInterface.php | 13 -- src/Cache/FilesSnapshot.php | 86 ++++++++ src/Cache/SnapshotClassBoundCache.php | 41 ++++ .../Cache/ClassFinderComputedCache.php | 41 ++++ .../Cache/HardClassFinderComputedCache.php | 69 ++++++ .../SnapshotClassFinderComputedCache.php | 129 ++++++++++++ src/Discovery/ClassFinder.php | 14 ++ src/Discovery/KcsClassFinder.php | 32 +++ src/Discovery/StaticClassFinder.php | 44 ++++ src/FactoryContext.php | 26 +-- src/FieldsBuilder.php | 22 +- src/GlobControllerQueryProvider.php | 94 +++------ ...peMapper.php => ClassFinderTypeMapper.php} | 196 ++++++------------ src/Mappers/GlobAnnotationsCache.php | 6 + src/Mappers/GlobExtendAnnotationsCache.php | 2 + src/Mappers/GlobExtendTypeMapperCache.php | 6 +- src/Mappers/GlobTypeMapper.php | 82 -------- src/Mappers/GlobTypeMapperCache.php | 14 +- src/Mappers/Parameters/TypeHandler.php | 6 +- src/Mappers/Root/EnumTypeMapper.php | 72 +++---- src/Mappers/Root/MyCLabsEnumTypeMapper.php | 47 ++--- .../Root/RootTypeMapperFactoryContext.php | 28 ++- src/Mappers/StaticClassListTypeMapper.php | 93 --------- .../StaticClassListTypeMapperFactory.php | 14 +- src/Reflection/CachedDocBlockFactory.php | 133 ------------ .../DocBlock/CachedDocBlockFactory.php | 52 +++++ src/Reflection/DocBlock/DocBlockFactory.php | 25 +++ .../DocBlock/PhpDocumentorDocBlockFactory.php | 51 +++++ src/SchemaFactory.php | 193 +++++++++++------ src/Utils/Namespaces/NS.php | 104 ---------- src/Utils/Namespaces/NamespaceFactory.php | 29 --- tests/AbstractQueryProvider.php | 66 ++++-- tests/Cache/FilesSnapshotTest.php | 77 +++++++ tests/Cache/SnapshotClassBoundCacheTest.php | 58 ++++++ .../HardClassFinderComputedCacheTest.php | 58 ++++++ .../SnapshotClassFinderComputedCacheTest.php | 89 ++++++++ tests/Discovery/KcsClassFinderTest.php | 53 +++++ tests/Discovery/StaticClassFinderTest.php | 42 ++++ tests/FactoryContextTest.php | 19 +- tests/FieldsBuilderTest.php | 8 +- tests/GlobControllerQueryProviderTest.php | 9 +- .../Psr15GraphQLMiddlewareBuilderTest.php | 4 +- tests/Integration/AnnotatedInterfaceTest.php | 3 +- tests/Integration/EndToEndTest.php | 67 +----- tests/Integration/IntegrationTestCase.php | 100 +++++---- ...Test.php => ClassFinderTypeMapperTest.php} | 84 ++++---- tests/Mappers/Parameters/TypeMapperTest.php | 40 ++-- tests/Mappers/RecursiveTypeMapperTest.php | 6 +- .../Root/MyCLabsEnumTypeMapperTest.php | 2 +- .../Mappers/StaticClassListTypeMapperTest.php | 31 --- tests/Mappers/StaticTypeMapperTest.php | 2 +- .../Reflection/CachedDocBlockFactoryTest.php | 48 ----- .../DocBlock/CachedDocBlockFactoryTest.php | 62 ++++++ .../PhpDocumentorDocBlockFactoryTest.php | 47 +++++ tests/RootTypeMapperFactoryContextTest.php | 20 +- tests/SchemaFactoryTest.php | 32 +-- tests/Utils/NsTest.php | 145 ------------- website/docs/CHANGELOG.md | 18 ++ website/docs/input-types.mdx | 2 +- website/docs/other-frameworks.mdx | 12 +- website/docs/validation.mdx | 3 +- .../version-6.0/input-types.mdx | 2 +- .../version-6.1/input-types.mdx | 2 +- .../version-7.0.0/input-types.mdx | 2 +- 72 files changed, 1700 insertions(+), 1415 deletions(-) create mode 100644 src/Cache/ClassBoundCache.php delete mode 100644 src/Cache/ClassBoundCacheContract.php delete mode 100644 src/Cache/ClassBoundCacheContractFactory.php delete mode 100644 src/Cache/ClassBoundCacheContractFactoryInterface.php delete mode 100644 src/Cache/ClassBoundCacheContractInterface.php create mode 100644 src/Cache/FilesSnapshot.php create mode 100644 src/Cache/SnapshotClassBoundCache.php create mode 100644 src/Discovery/Cache/ClassFinderComputedCache.php create mode 100644 src/Discovery/Cache/HardClassFinderComputedCache.php create mode 100644 src/Discovery/Cache/SnapshotClassFinderComputedCache.php create mode 100644 src/Discovery/ClassFinder.php create mode 100644 src/Discovery/KcsClassFinder.php create mode 100644 src/Discovery/StaticClassFinder.php rename src/Mappers/{AbstractTypeMapper.php => ClassFinderTypeMapper.php} (68%) delete mode 100644 src/Mappers/GlobTypeMapper.php delete mode 100644 src/Mappers/StaticClassListTypeMapper.php delete mode 100644 src/Reflection/CachedDocBlockFactory.php create mode 100644 src/Reflection/DocBlock/CachedDocBlockFactory.php create mode 100644 src/Reflection/DocBlock/DocBlockFactory.php create mode 100644 src/Reflection/DocBlock/PhpDocumentorDocBlockFactory.php delete mode 100644 src/Utils/Namespaces/NS.php delete mode 100644 src/Utils/Namespaces/NamespaceFactory.php create mode 100644 tests/Cache/FilesSnapshotTest.php create mode 100644 tests/Cache/SnapshotClassBoundCacheTest.php create mode 100644 tests/Discovery/Cache/HardClassFinderComputedCacheTest.php create mode 100644 tests/Discovery/Cache/SnapshotClassFinderComputedCacheTest.php create mode 100644 tests/Discovery/KcsClassFinderTest.php create mode 100644 tests/Discovery/StaticClassFinderTest.php rename tests/Mappers/{GlobTypeMapperTest.php => ClassFinderTypeMapperTest.php} (65%) delete mode 100644 tests/Mappers/StaticClassListTypeMapperTest.php delete mode 100644 tests/Reflection/CachedDocBlockFactoryTest.php create mode 100644 tests/Reflection/DocBlock/CachedDocBlockFactoryTest.php create mode 100644 tests/Reflection/DocBlock/PhpDocumentorDocBlockFactoryTest.php delete mode 100644 tests/Utils/NsTest.php diff --git a/.github/workflows/doc_generation.yml b/.github/workflows/doc_generation.yml index e2851c911b..4c73a105b5 100644 --- a/.github/workflows/doc_generation.yml +++ b/.github/workflows/doc_generation.yml @@ -24,7 +24,7 @@ jobs: - name: "Setup NodeJS" uses: actions/setup-node@v4 with: - node-version: '16.x' + node-version: '20.x' - name: "Yarn install" run: yarn install diff --git a/composer.json b/composer.json index 5b47aa1049..6063ab0c19 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ "symfony/cache": "^4.3 || ^5 || ^6 || ^7", "symfony/expression-language": "^4 || ^5 || ^6 || ^7", "webonyx/graphql-php": "^v15.0", - "kcs/class-finder": "^0.5.0" + "kcs/class-finder": "^0.5.1" }, "require-dev": { "beberlei/porpaginas": "^1.2 || ^2.0", @@ -34,7 +34,7 @@ "myclabs/php-enum": "^1.6.6", "php-coveralls/php-coveralls": "^2.1", "phpstan/extension-installer": "^1.1", - "phpstan/phpstan": "^1.9", + "phpstan/phpstan": "^1.11", "phpunit/phpunit": "^10.1 || ^11.0", "symfony/var-dumper": "^5.4 || ^6.0 || ^7", "thecodingmachine/phpstan-strict-rules": "^1.0" diff --git a/examples/no-framework/index.php b/examples/no-framework/index.php index b8f43ee18b..3db0b0fe20 100644 --- a/examples/no-framework/index.php +++ b/examples/no-framework/index.php @@ -24,8 +24,7 @@ ]); $factory = new SchemaFactory($cache, $container); -$factory->addControllerNamespace('App\\Controllers') - ->addTypeNamespace('App'); +$factory->addNamespace('App'); $schema = $factory->createSchema(); diff --git a/src/AggregateControllerQueryProvider.php b/src/AggregateControllerQueryProvider.php index 432e8623a7..41a3d07027 100644 --- a/src/AggregateControllerQueryProvider.php +++ b/src/AggregateControllerQueryProvider.php @@ -8,16 +8,12 @@ use Psr\Container\ContainerInterface; use TheCodingMachine\GraphQLite\Mappers\DuplicateMappingException; -use function array_filter; -use function array_intersect_key; -use function array_keys; use function array_map; use function array_merge; use function array_sum; use function array_values; use function assert; use function count; -use function reset; use function sort; /** @@ -94,18 +90,29 @@ private function flattenList(array $list): array } // We have an issue, let's detect the duplicate - $duplicates = array_intersect_key(...array_values($list)); - // Let's display an error from the first one. - $firstDuplicate = reset($duplicates); - assert($firstDuplicate instanceof FieldDefinition); + $queriesByName = []; + $duplicateClasses = null; + $duplicateQueryName = null; - $duplicateName = $firstDuplicate->name; + foreach ($list as $class => $queries) { + foreach ($queries as $query => $field) { + $duplicatedClass = $queriesByName[$query] ?? null; - $classes = array_keys(array_filter($list, static function (array $fields) use ($duplicateName) { - return isset($fields[$duplicateName]); - })); - sort($classes); + if (! $duplicatedClass) { + $queriesByName[$query] = $class; - throw DuplicateMappingException::createForQueryInTwoControllers($classes[0], $classes[1], $duplicateName); + continue; + } + + $duplicateClasses = [$duplicatedClass, $class]; + $duplicateQueryName = $query; + } + } + + assert($duplicateClasses !== null && $duplicateQueryName !== null); + + sort($duplicateClasses); + + throw DuplicateMappingException::createForQueryInTwoControllers($duplicateClasses[0], $duplicateClasses[1], $duplicateQueryName); } } diff --git a/src/Cache/ClassBoundCache.php b/src/Cache/ClassBoundCache.php new file mode 100644 index 0000000000..6e9d20f66b --- /dev/null +++ b/src/Cache/ClassBoundCache.php @@ -0,0 +1,24 @@ +cachePrefix = str_replace(['\\', '{', '}', '(', ')', '/', '@', ':'], '_', $cachePrefix); - } - - /** - * @param string $key An optional key to differentiate between cache items attached to the same class. - * - * @throws InvalidArgumentException - */ - public function get(ReflectionClass $reflectionClass, callable $resolver, string $key = '', int|null $ttl = null): mixed - { - $cacheKey = $reflectionClass->getName() . '__' . $key; - $cacheKey = $this->cachePrefix . str_replace(['\\', '{', '}', '(', ')', '/', '@', ':'], '_', $cacheKey); - - $item = $this->classBoundCache->get($cacheKey); - if ($item !== null) { - return $item; - } - - $item = $resolver(); - - $this->classBoundCache->set($cacheKey, $item, $ttl); - - return $item; - } -} diff --git a/src/Cache/ClassBoundCacheContractFactory.php b/src/Cache/ClassBoundCacheContractFactory.php deleted file mode 100644 index 0c2dfc7750..0000000000 --- a/src/Cache/ClassBoundCacheContractFactory.php +++ /dev/null @@ -1,15 +0,0 @@ - $dependencies */ + private function __construct( + private readonly array $dependencies, + ) + { + } + + /** @param list $files */ + public static function for(array $files): self + { + $dependencies = []; + + foreach (array_unique($files) as $file) { + $dependencies[$file] = filemtime($file); + } + + return new self($dependencies); + } + + public static function forClass(ReflectionClass $class, bool $withInheritance = false): self + { + return self::for( + self::dependencies($class, $withInheritance), + ); + } + + public static function alwaysUnchanged(): self + { + return new self([]); + } + + /** @return list */ + private static function dependencies(ReflectionClass $class, bool $withInheritance = false): array + { + $filename = $class->getFileName(); + + // Internal classes are treated as always the same, e.g. you'll have to drop the cache between PHP versions. + if ($filename === false) { + return []; + } + + $files = [$filename]; + + if (! $withInheritance) { + return $files; + } + + if ($class->getParentClass() !== false) { + $files = [...$files, ...self::dependencies($class->getParentClass(), $withInheritance)]; + } + + foreach ($class->getTraits() as $trait) { + $files = [...$files, ...self::dependencies($trait, $withInheritance)]; + } + + foreach ($class->getInterfaces() as $interface) { + $files = [...$files, ...self::dependencies($interface, $withInheritance)]; + } + + return $files; + } + + public function changed(): bool + { + foreach ($this->dependencies as $filename => $modificationTime) { + if ($modificationTime !== filemtime($filename)) { + return true; + } + } + + return false; + } +} diff --git a/src/Cache/SnapshotClassBoundCache.php b/src/Cache/SnapshotClassBoundCache.php new file mode 100644 index 0000000000..62f2a329d4 --- /dev/null +++ b/src/Cache/SnapshotClassBoundCache.php @@ -0,0 +1,41 @@ +getName() . '__' . $key; + $cacheKey = str_replace(['\\', '{', '}', '(', ')', '/', '@', ':'], '_', $cacheKey); + + $item = $this->cache->get($cacheKey); + + if ($item !== null && ! $item['snapshot']->changed()) { + return $item['data']; + } + + $item = [ + 'data' => $resolver(), + 'snapshot' => ($this->filesSnapshotFactory)($reflectionClass, $withInheritance), + ]; + + $this->cache->set($cacheKey, $item); + + return $item['data']; + } +} diff --git a/src/Discovery/Cache/ClassFinderComputedCache.php b/src/Discovery/Cache/ClassFinderComputedCache.php new file mode 100644 index 0000000000..66f7fd05c2 --- /dev/null +++ b/src/Discovery/Cache/ClassFinderComputedCache.php @@ -0,0 +1,41 @@ +. Once all classes are iterated, + * $reduce will then be called with that map, and it's final result is returned. + * + * Now the point of this is now whenever file A changes, we can automatically remove entries generated for it + * and simply call $map only for classes from file A, leaving all other entries untouched and not having to + * waste resources on the rest of them. We then only need to call the cheap $reduce and have the final result :) + * + * @param callable(ReflectionClass): TEntry $map + * @param callable(array): TReturn $reduce + * + * @return TReturn + * + * @template TEntry of mixed + * @template TReturn of mixed + */ + public function compute( + ClassFinder $classFinder, + string $key, + callable $map, + callable $reduce, + ): mixed; +} diff --git a/src/Discovery/Cache/HardClassFinderComputedCache.php b/src/Discovery/Cache/HardClassFinderComputedCache.php new file mode 100644 index 0000000000..472aa5a58a --- /dev/null +++ b/src/Discovery/Cache/HardClassFinderComputedCache.php @@ -0,0 +1,69 @@ +): TEntry $map + * @param callable(array): TReturn $reduce + * + * @return TReturn + * + * @template TEntry of mixed + * @template TReturn of mixed + */ + public function compute( + ClassFinder $classFinder, + string $key, + callable $map, + callable $reduce, + ): mixed + { + $result = $this->cache->get($key); + + if ($result !== null) { + return $result; + } + + $result = $reduce($this->entries($classFinder, $map)); + + $this->cache->set($key, $result); + + return $result; + } + + /** + * @param callable(ReflectionClass): TEntry $map + * + * @return array + * + * @template TEntry of mixed + */ + private function entries( + ClassFinder $classFinder, + callable $map, + ): mixed + { + $entries = []; + + foreach ($classFinder as $classReflection) { + $entries[$classReflection->getFileName()] = $map($classReflection); + } + + /** @phpstan-ignore return.type */ + return $entries; + } +} diff --git a/src/Discovery/Cache/SnapshotClassFinderComputedCache.php b/src/Discovery/Cache/SnapshotClassFinderComputedCache.php new file mode 100644 index 0000000000..3e4b9d2306 --- /dev/null +++ b/src/Discovery/Cache/SnapshotClassFinderComputedCache.php @@ -0,0 +1,129 @@ +): TEntry $map + * @param callable(array): TReturn $reduce + * + * @return TReturn + * + * @template TEntry of mixed + * @template TReturn of mixed + */ + public function compute( + ClassFinder $classFinder, + string $key, + callable $map, + callable $reduce, + ): mixed + { + $entries = $this->entries($classFinder, $key . '.entries', $map); + + return $reduce($entries); + } + + /** + * @param callable(ReflectionClass): TEntry $map + * + * @return array + * + * @template TEntry of mixed + */ + private function entries( + ClassFinder $classFinder, + string $key, + callable $map, + ): mixed + { + $previousEntries = $this->cache->get($key) ?? []; + /** @var array $result */ + $result = []; + $entries = []; + + // The size of the cache may be huge, so let's avoid writes when unnecessary. + $changed = false; + + $classFinder = $classFinder->withPathFilter(static function (string $filename) use (&$entries, &$result, &$changed, $previousEntries) { + /** @var array{ data: TEntry, dependencies: FilesSnapshot, matching: bool } $entry */ + $entry = $previousEntries[$filename] ?? null; + + // If there's no entry in cache for this filename (new file or previously uncached), + // or if it the file has been modified since caching, we'll try to autoload + // the class and collect the cached information (again). + if (! $entry || $entry['dependencies']->changed()) { + // In case this file isn't a class, or doesn't match the provided namespace filter, + // it will not be emitted in the iterator and won't reach the `foreach()` below. + // So to avoid iterating over these files again, we'll mark them as non-matching. + // If they are matching, it'll be overwritten in the `foreach` loop below. + $entries[$filename] = [ + 'dependencies' => FilesSnapshot::for([$filename]), + 'matching' => false, + ]; + + $changed = true; + + return true; + } + + if ($entry['matching']) { + $result[$filename] = $entry['data']; + } + + $entries[$filename] = $entry; + + return false; + }); + + foreach ($classFinder as $classReflection) { + $filename = $classReflection->getFileName(); + + $result[$filename] = $map($classReflection); + $entries[$filename] = [ + 'dependencies' => FilesSnapshot::forClass($classReflection, true), + 'data' => $result[$filename], + 'matching' => true, + ]; + + $changed = true; + } + + if ($changed) { + $this->cache->set($key, $entries); + } + + /** @phpstan-ignore return.type */ + return $result; + } +} diff --git a/src/Discovery/ClassFinder.php b/src/Discovery/ClassFinder.php new file mode 100644 index 0000000000..67d2cf3b1a --- /dev/null +++ b/src/Discovery/ClassFinder.php @@ -0,0 +1,14 @@ +> */ +interface ClassFinder extends IteratorAggregate +{ + public function withPathFilter(callable $filter): self; +} diff --git a/src/Discovery/KcsClassFinder.php b/src/Discovery/KcsClassFinder.php new file mode 100644 index 0000000000..d54616b8ec --- /dev/null +++ b/src/Discovery/KcsClassFinder.php @@ -0,0 +1,32 @@ +finder = (clone $that->finder)->pathFilter($filter); + + return $that; + } + + /** @return Traversable */ + public function getIterator(): Traversable + { + return $this->finder->getIterator(); + } +} diff --git a/src/Discovery/StaticClassFinder.php b/src/Discovery/StaticClassFinder.php new file mode 100644 index 0000000000..7eedb617b0 --- /dev/null +++ b/src/Discovery/StaticClassFinder.php @@ -0,0 +1,44 @@ + $classes */ + public function __construct( + private readonly array $classes, + ) + { + } + + public function withPathFilter(callable $filter): ClassFinder + { + $that = clone $this; + $that->pathFilter = $filter; + + return $that; + } + + /** @return Traversable */ + public function getIterator(): Traversable + { + foreach ($this->classes as $class) { + $classReflection = new ReflectionClass($class); + + /** @phpstan-ignore-next-line */ + if ($this->pathFilter && ! ($this->pathFilter)($classReflection->getFileName())) { + continue; + } + + yield $class => $classReflection; + } + } +} diff --git a/src/FactoryContext.php b/src/FactoryContext.php index be64043ade..0aaf40ac42 100644 --- a/src/FactoryContext.php +++ b/src/FactoryContext.php @@ -6,7 +6,9 @@ use Psr\Container\ContainerInterface; use Psr\SimpleCache\CacheInterface; -use TheCodingMachine\GraphQLite\Cache\ClassBoundCacheContractFactoryInterface; +use TheCodingMachine\GraphQLite\Cache\ClassBoundCache; +use TheCodingMachine\GraphQLite\Discovery\Cache\ClassFinderComputedCache; +use TheCodingMachine\GraphQLite\Discovery\ClassFinder; use TheCodingMachine\GraphQLite\Mappers\RecursiveTypeMapperInterface; use TheCodingMachine\GraphQLite\Types\InputTypeValidatorInterface; use TheCodingMachine\GraphQLite\Types\TypeResolver; @@ -30,9 +32,9 @@ public function __construct( private readonly ContainerInterface $container, private readonly CacheInterface $cache, private readonly InputTypeValidatorInterface|null $inputTypeValidator, - private readonly int|null $globTTL, - private readonly int|null $mapTTL = null, - private readonly ClassBoundCacheContractFactoryInterface|null $classBoundCacheContractFactory = null, + private readonly ClassFinder $classFinder, + private readonly ClassFinderComputedCache $classFinderComputedCache, + private readonly ClassBoundCache $classBoundCache, ) { } @@ -86,23 +88,23 @@ public function getCache(): CacheInterface return $this->cache; } - public function getClassBoundCacheContractFactory(): ClassBoundCacheContractFactoryInterface|null + public function getInputTypeValidator(): InputTypeValidatorInterface|null { - return $this->classBoundCacheContractFactory; + return $this->inputTypeValidator; } - public function getInputTypeValidator(): InputTypeValidatorInterface|null + public function getClassFinder(): ClassFinder { - return $this->inputTypeValidator; + return $this->classFinder; } - public function getGlobTTL(): int|null + public function getClassFinderComputedCache(): ClassFinderComputedCache { - return $this->globTTL; + return $this->classFinderComputedCache; } - public function getMapTTL(): int|null + public function getClassBoundCache(): ClassBoundCache|null { - return $this->mapTTL; + return $this->classBoundCache; } } diff --git a/src/FieldsBuilder.php b/src/FieldsBuilder.php index de99011166..d8ab99cc6b 100644 --- a/src/FieldsBuilder.php +++ b/src/FieldsBuilder.php @@ -46,7 +46,7 @@ use TheCodingMachine\GraphQLite\Parameters\InputTypeParameterInterface; use TheCodingMachine\GraphQLite\Parameters\ParameterInterface; use TheCodingMachine\GraphQLite\Parameters\PrefetchDataParameter; -use TheCodingMachine\GraphQLite\Reflection\CachedDocBlockFactory; +use TheCodingMachine\GraphQLite\Reflection\DocBlock\DocBlockFactory; use TheCodingMachine\GraphQLite\Types\ArgumentResolver; use TheCodingMachine\GraphQLite\Types\MutableObjectType; use TheCodingMachine\GraphQLite\Types\TypeResolver; @@ -84,7 +84,7 @@ public function __construct( private readonly RecursiveTypeMapperInterface $recursiveTypeMapper, private readonly ArgumentResolver $argumentResolver, private readonly TypeResolver $typeResolver, - private readonly CachedDocBlockFactory $cachedDocBlockFactory, + private readonly DocBlockFactory $docBlockFactory, private readonly NamingStrategyInterface $namingStrategy, private readonly RootTypeMapperInterface $rootTypeMapper, private readonly ParameterMiddlewareInterface $parameterMapper, @@ -96,7 +96,7 @@ public function __construct( $this->argumentResolver, $this->rootTypeMapper, $this->typeResolver, - $this->cachedDocBlockFactory, + $this->docBlockFactory, ); } @@ -309,7 +309,7 @@ public function getSelfFields(string $className, string|null $typeName = null): */ public function getParameters(ReflectionMethod $refMethod, int $skip = 0): array { - $docBlockObj = $this->cachedDocBlockFactory->getDocBlock($refMethod); + $docBlockObj = $this->docBlockFactory->create($refMethod); //$docBlockComment = $docBlockObj->getSummary()."\n".$docBlockObj->getDescription()->render(); $parameters = array_slice($refMethod->getParameters(), $skip); @@ -455,7 +455,7 @@ private function getFieldsByMethodAnnotations( $description = $queryAnnotation->getDescription(); } - $docBlockObj = $this->cachedDocBlockFactory->getDocBlock($refMethod); + $docBlockObj = $this->docBlockFactory->create($refMethod); $name = $queryAnnotation->getName() ?: $this->namingStrategy->getFieldNameFromMethodName($methodName); @@ -565,7 +565,7 @@ private function getFieldsByPropertyAnnotations( $description = $queryAnnotation->getDescription(); } - $docBlock = $this->cachedDocBlockFactory->getDocBlock($refProperty); + $docBlock = $this->docBlockFactory->create($refProperty); $name = $queryAnnotation->getName() ?: $refProperty->getName(); @@ -684,7 +684,7 @@ private function getQueryFieldsFromSourceFields( throw FieldNotFoundException::wrapWithCallerInfo($e, $refClass->getName()); } - $docBlockObj = $this->cachedDocBlockFactory->getDocBlock($refMethod); + $docBlockObj = $this->docBlockFactory->create($refMethod); $docBlockComment = rtrim($docBlockObj->getSummary() . "\n" . $docBlockObj->getDescription()->render()); $deprecated = $docBlockObj->getTagsByName('deprecated'); @@ -820,7 +820,7 @@ private function resolvePhpType( { $typeResolver = new \phpDocumentor\Reflection\TypeResolver(); - $context = $this->cachedDocBlockFactory->getContextFromClass($refClass); + $context = $this->docBlockFactory->createContext($refClass); $phpdocType = $typeResolver->resolve($phpTypeStr, $context); assert($phpdocType !== null); @@ -969,7 +969,7 @@ private function getPrefetchParameter( $prefetchParameters = $prefetchRefMethod->getParameters(); array_shift($prefetchParameters); - $prefetchDocBlockObj = $this->cachedDocBlockFactory->getDocBlock($prefetchRefMethod); + $prefetchDocBlockObj = $this->docBlockFactory->create($prefetchRefMethod); $prefetchArgs = $this->mapParameters($prefetchParameters, $prefetchDocBlockObj); return new PrefetchDataParameter( @@ -1025,7 +1025,7 @@ private function getInputFieldsByMethodAnnotations( continue; } - $docBlockObj = $this->cachedDocBlockFactory->getDocBlock($refMethod); + $docBlockObj = $this->docBlockFactory->create($refMethod); $methodName = $refMethod->getName(); if (! str_starts_with($methodName, 'set')) { continue; @@ -1122,7 +1122,7 @@ private function getInputFieldsByPropertyAnnotations( $fields = []; $annotations = $this->annotationReader->getPropertyAnnotations($refProperty, $annotationName); - $docBlock = $this->cachedDocBlockFactory->getDocBlock($refProperty); + $docBlock = $this->docBlockFactory->create($refProperty); foreach ($annotations as $annotation) { $description = null; diff --git a/src/GlobControllerQueryProvider.php b/src/GlobControllerQueryProvider.php index f7ed17170e..69f66e0d70 100644 --- a/src/GlobControllerQueryProvider.php +++ b/src/GlobControllerQueryProvider.php @@ -5,22 +5,17 @@ namespace TheCodingMachine\GraphQLite; use GraphQL\Type\Definition\FieldDefinition; -use InvalidArgumentException; -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\GraphQLite\Annotations\Mutation; use TheCodingMachine\GraphQLite\Annotations\Query; use TheCodingMachine\GraphQLite\Annotations\Subscription; +use TheCodingMachine\GraphQLite\Discovery\Cache\ClassFinderComputedCache; +use TheCodingMachine\GraphQLite\Discovery\ClassFinder; -use function class_exists; -use function interface_exists; -use function is_array; -use function str_replace; +use function array_filter; +use function array_values; /** * Scans all the classes in a given namespace of the main project (not the vendor directory). @@ -30,36 +25,25 @@ */ final class GlobControllerQueryProvider implements QueryProviderInterface { - /** @var array|null */ - private array|null $instancesList = null; + /** @var array */ + private array $classList; 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 ContainerInterface $container The container we will fetch controllers from. */ public function __construct( - private readonly string $namespace, private readonly FieldsBuilder $fieldsBuilder, private readonly ContainerInterface $container, private readonly AnnotationReader $annotationReader, - private readonly CacheInterface $cache, - private readonly FinderInterface $finder, - int|null $cacheTtl = null, + private readonly ClassFinder $classFinder, + private readonly ClassFinderComputedCache $classFinderComputedCache, ) { - $this->cacheContract = new Psr16Adapter( - $this->cache, - str_replace(['\\', '{', '}', '(', ')', '/', '@', ':'], '_', $namespace), - $cacheTtl ?? 0, - ); } private function getAggregateControllerQueryProvider(): AggregateControllerQueryProvider { $this->aggregateControllerQueryProvider ??= new AggregateControllerQueryProvider( - $this->getInstancesList(), + $this->getClassList(), $this->fieldsBuilder, $this->container, ); @@ -70,46 +54,30 @@ private function getAggregateControllerQueryProvider(): AggregateControllerQuery /** * Returns an array of fully qualified class names. * - * @return array + * @return array */ - private function getInstancesList(): array + private function getClassList(): array { - if ($this->instancesList === null) { - $this->instancesList = $this->cacheContract->get( - 'globQueryProvider', - fn () => $this->buildInstancesList(), - ); - - if (! is_array($this->instancesList)) { - throw new InvalidArgumentException('The instance list returned is not an array. There might be an issue with your PSR-16 cache implementation.'); - } - } - - return $this->instancesList; - } - - /** @return array */ - private function buildInstancesList(): array - { - $instances = []; - foreach ((clone $this->finder)->inNamespace($this->namespace) as $className => $refClass) { - if (! class_exists($className) && ! interface_exists($className)) { - continue; - } - if (! $refClass instanceof ReflectionClass || ! $refClass->isInstantiable()) { - continue; - } - if (! $this->hasOperations($refClass)) { - continue; - } - if (! $this->container->has($className)) { - continue; - } - - $instances[] = $className; - } + /** @phpstan-ignore assign.propertyType */ + $this->classList ??= $this->classFinderComputedCache->compute( + $this->classFinder, + 'globQueryProvider', + function (ReflectionClass $classReflection): string|null { + if ( + ! $classReflection->isInstantiable() || + ! $this->hasOperations($classReflection) || + ! $this->container->has($classReflection->getName()) + ) { + return null; + } + + return $classReflection->getName(); + }, + static fn (array $entries) => array_values(array_filter($entries)), + ); - return $instances; + /** @phpstan-ignore return.type */ + return $this->classList; } /** @param ReflectionClass $reflectionClass */ diff --git a/src/Mappers/AbstractTypeMapper.php b/src/Mappers/ClassFinderTypeMapper.php similarity index 68% rename from src/Mappers/AbstractTypeMapper.php rename to src/Mappers/ClassFinderTypeMapper.php index 7e09f01ed5..c9911d4b28 100644 --- a/src/Mappers/AbstractTypeMapper.php +++ b/src/Mappers/ClassFinderTypeMapper.php @@ -9,16 +9,12 @@ use GraphQL\Type\Definition\OutputType; use GraphQL\Type\Definition\Type; use Psr\Container\ContainerInterface; -use Psr\SimpleCache\CacheInterface; use ReflectionClass; use ReflectionException; use ReflectionMethod; -use Symfony\Component\Cache\Adapter\Psr16Adapter; -use Symfony\Contracts\Cache\CacheInterface as CacheContractInterface; use TheCodingMachine\GraphQLite\AnnotationReader; -use TheCodingMachine\GraphQLite\Cache\ClassBoundCacheContractFactory; -use TheCodingMachine\GraphQLite\Cache\ClassBoundCacheContractFactoryInterface; -use TheCodingMachine\GraphQLite\Cache\ClassBoundCacheContractInterface; +use TheCodingMachine\GraphQLite\Discovery\Cache\ClassFinderComputedCache; +use TheCodingMachine\GraphQLite\Discovery\ClassFinder; use TheCodingMachine\GraphQLite\InputTypeGenerator; use TheCodingMachine\GraphQLite\InputTypeUtils; use TheCodingMachine\GraphQLite\NamingStrategyInterface; @@ -28,32 +24,19 @@ use TheCodingMachine\GraphQLite\Types\MutableObjectType; use TheCodingMachine\GraphQLite\Types\ResolvableMutableInputInterface; +use function array_reduce; use function assert; /** * Analyzes classes and uses the @Type annotation to find the types automatically. - * - * Assumes that the container contains a class whose identifier is the same as the class name. */ -abstract class AbstractTypeMapper implements TypeMapperInterface +class ClassFinderTypeMapper implements TypeMapperInterface { - /** - * Cache storing the GlobAnnotationsCache objects linked to a given ReflectionClass. - */ - private ClassBoundCacheContractInterface $mapClassToAnnotationsCache; - /** - * Cache storing the GlobAnnotationsCache objects linked to a given ReflectionClass. - */ - private ClassBoundCacheContractInterface $mapClassToExtendAnnotationsCache; - - private CacheContractInterface $cacheContract; private GlobTypeMapperCache|null $globTypeMapperCache = null; private GlobExtendTypeMapperCache|null $globExtendTypeMapperCache = null; - /** @var array> */ - private array $registeredInputs; public function __construct( - string $cachePrefix, + private readonly ClassFinder $classFinder, private readonly TypeGenerator $typeGenerator, private readonly InputTypeGenerator $inputTypeGenerator, private readonly InputTypeUtils $inputTypeUtils, @@ -61,18 +44,9 @@ public function __construct( private readonly AnnotationReader $annotationReader, private readonly NamingStrategyInterface $namingStrategy, private readonly RecursiveTypeMapperInterface $recursiveTypeMapper, - private readonly CacheInterface $cache, - protected int|null $globTTL = 2, - private readonly int|null $mapTTL = null, - ClassBoundCacheContractFactoryInterface|null $classBoundCacheContractFactory = null, + private readonly ClassFinderComputedCache $classFinderComputedCache, ) { - $this->cacheContract = new Psr16Adapter($this->cache, $cachePrefix, $this->globTTL ?? 0); - - $classBoundCacheContractFactory = $classBoundCacheContractFactory ?? new ClassBoundCacheContractFactory(); - - $this->mapClassToAnnotationsCache = $classBoundCacheContractFactory->make($cache, 'classToAnnotations_' . $cachePrefix); - $this->mapClassToExtendAnnotationsCache = $classBoundCacheContractFactory->make($cache, 'classToExtendAnnotations_' . $cachePrefix); } /** @@ -80,48 +54,17 @@ public function __construct( */ private function getMaps(): GlobTypeMapperCache { - if ($this->globTypeMapperCache === null) { - $this->globTypeMapperCache = $this->cacheContract->get('fullMapComputed', function () { - return $this->buildMap(); - }); - } - - return $this->globTypeMapperCache; - } - - private function getMapClassToExtendTypeArray(): GlobExtendTypeMapperCache - { - if ($this->globExtendTypeMapperCache === null) { - $this->globExtendTypeMapperCache = $this->cacheContract->get('fullExtendMapComputed', function () { - return $this->buildMapClassToExtendTypeArray(); - }); - } - - return $this->globExtendTypeMapperCache; - } - - /** - * Returns the array of globbed classes. - * Only instantiable classes are returned. - * - * @return array> Key: fully qualified class name - */ - abstract protected function getClassList(): array; - - private function buildMap(): GlobTypeMapperCache - { - $globTypeMapperCache = new GlobTypeMapperCache(); - - /** @var array,ReflectionClass> $classes */ - $classes = $this->getClassList(); + $this->globTypeMapperCache ??= $this->classFinderComputedCache->compute( + $this->classFinder, + 'classToAnnotations', + function (ReflectionClass $refClass): GlobAnnotationsCache|null { + if ($refClass->isEnum()) { + return null; + } - foreach ($classes as $className => $refClass) { - // Enum's are not types - if ($refClass->isEnum()) { - continue; - } - $annotationsCache = $this->mapClassToAnnotationsCache->get($refClass, function () use ($refClass, $className) { - $annotationsCache = new GlobAnnotationsCache(); + $annotationsCache = new GlobAnnotationsCache( + $className = $refClass->getName(), + ); $containsAnnotations = false; @@ -135,11 +78,6 @@ private function buildMap(): GlobTypeMapperCache $inputs = $this->annotationReader->getInputAnnotations($refClass); foreach ($inputs as $input) { $inputName = $this->namingStrategy->getInputTypeName($className, $input); - if (isset($this->registeredInputs[$inputName])) { - throw DuplicateMappingException::createForTwoInputs($inputName, $this->registeredInputs[$inputName], $refClass->getName()); - } - - $this->registeredInputs[$inputName] = $refClass->getName(); $annotationsCache = $annotationsCache->registerInput($inputName, $className, $input); $containsAnnotations = true; } @@ -169,71 +107,73 @@ private function buildMap(): GlobTypeMapperCache $containsAnnotations = true; } - if (! $containsAnnotations) { - return 'nothing'; + return $containsAnnotations ? $annotationsCache : null; + }, + static fn (array $entries) => array_reduce($entries, static function (GlobTypeMapperCache $globTypeMapperCache, GlobAnnotationsCache|null $annotationsCache) { + if ($annotationsCache === null) { + return $globTypeMapperCache; } - return $annotationsCache; - }, '', $this->mapTTL); + $globTypeMapperCache->registerAnnotations($annotationsCache->sourceClass, $annotationsCache); - if ($annotationsCache === 'nothing') { - continue; - } + return $globTypeMapperCache; + }, new GlobTypeMapperCache()), + ); - $globTypeMapperCache->registerAnnotations($refClass, $annotationsCache); - } - - return $globTypeMapperCache; + return $this->globTypeMapperCache; } - private function buildMapClassToExtendTypeArray(): GlobExtendTypeMapperCache + private function getMapClassToExtendTypeArray(): GlobExtendTypeMapperCache { - $globExtendTypeMapperCache = new GlobExtendTypeMapperCache(); - - $classes = $this->getClassList(); - foreach ($classes as $refClass) { - // Enum's are not types - if ($refClass->isEnum()) { - continue; - } - $annotationsCache = $this->mapClassToExtendAnnotationsCache->get($refClass, function () use ($refClass) { + $this->globExtendTypeMapperCache ??= $this->classFinderComputedCache->compute( + $this->classFinder, + 'classToExtendAnnotations', + function (ReflectionClass $refClass): GlobExtendAnnotationsCache|null { + // Enum's are not types + if ($refClass->isEnum()) { + return null; + } + $extendType = $this->annotationReader->getExtendTypeAnnotation($refClass); - if ($extendType !== null) { - $extendClassName = $extendType->getClass(); - if ($extendClassName !== null) { - try { - $targetType = $this->recursiveTypeMapper->mapClassToType($extendClassName, null); - } catch (CannotMapTypeException $e) { - $e->addExtendTypeInfo($refClass, $extendType); - throw $e; - } - $typeName = $targetType->name; - } else { - $typeName = $extendType->getName(); - assert($typeName !== null); - $targetType = $this->recursiveTypeMapper->mapNameToType($typeName); - if (! $targetType instanceof MutableObjectType) { - throw CannotMapTypeException::extendTypeWithBadTargetedClass($refClass->getName(), $extendType); - } - $extendClassName = $targetType->getMappedClassName(); - } + if ($extendType === null) { + return null; + } - // FIXME: $extendClassName === NULL!!!!!! - return new GlobExtendAnnotationsCache($extendClassName, $typeName); + $extendClassName = $extendType->getClass(); + if ($extendClassName !== null) { + try { + $targetType = $this->recursiveTypeMapper->mapClassToType($extendClassName, null); + } catch (CannotMapTypeException $e) { + $e->addExtendTypeInfo($refClass, $extendType); + throw $e; + } + $typeName = $targetType->name; + } else { + $typeName = $extendType->getName(); + assert($typeName !== null); + $targetType = $this->recursiveTypeMapper->mapNameToType($typeName); + if (! $targetType instanceof MutableObjectType) { + throw CannotMapTypeException::extendTypeWithBadTargetedClass($refClass->getName(), $extendType); + } + $extendClassName = $targetType->getMappedClassName(); } - return 'nothing'; - }, '', $this->mapTTL); + // FIXME: $extendClassName === NULL!!!!!! + return new GlobExtendAnnotationsCache($refClass->getName(), $extendClassName, $typeName); + }, + static fn (array $entries) => array_reduce($entries, static function (GlobExtendTypeMapperCache $globExtendTypeMapperCache, GlobExtendAnnotationsCache|null $annotationsCache) { + if ($annotationsCache === null) { + return $globExtendTypeMapperCache; + } - if ($annotationsCache === 'nothing') { - continue; - } + $globExtendTypeMapperCache->registerAnnotations($annotationsCache->sourceClass, $annotationsCache); - $globExtendTypeMapperCache->registerAnnotations($refClass, $annotationsCache); - } + return $globExtendTypeMapperCache; + }, new GlobExtendTypeMapperCache()), + ); - return $globExtendTypeMapperCache; + return $this->globExtendTypeMapperCache; } /** diff --git a/src/Mappers/GlobAnnotationsCache.php b/src/Mappers/GlobAnnotationsCache.php index dd41c665e4..0ecef554a3 100644 --- a/src/Mappers/GlobAnnotationsCache.php +++ b/src/Mappers/GlobAnnotationsCache.php @@ -17,6 +17,7 @@ final class GlobAnnotationsCache use Cloneable; /** + * @param class-string $sourceClass * @param class-string|null $typeClassName * @param array|null, 2:bool, 3:class-string}> $factories * An array mapping a factory method name to an input name / class name / default flag / @@ -27,6 +28,7 @@ final class GlobAnnotationsCache * An array mapping an input type name to an input name / declaring class */ public function __construct( + public readonly string $sourceClass, private readonly string|null $typeClassName = null, private readonly string|null $typeName = null, private readonly bool $default = false, @@ -108,6 +110,10 @@ public function getDecorators(): array */ public function registerInput(string $name, string $className, Input $input): self { + if (isset($this->inputs[$name])) { + throw DuplicateMappingException::createForTwoInputs($name, $this->inputs[$name][0], $className); + } + return $this->with( inputs: [ ...$this->inputs, diff --git a/src/Mappers/GlobExtendAnnotationsCache.php b/src/Mappers/GlobExtendAnnotationsCache.php index b99d1acd28..ca62e9536a 100644 --- a/src/Mappers/GlobExtendAnnotationsCache.php +++ b/src/Mappers/GlobExtendAnnotationsCache.php @@ -11,7 +11,9 @@ */ final class GlobExtendAnnotationsCache { + /** @param class-string $sourceClass */ public function __construct( + public readonly string $sourceClass, private string|null $extendTypeClassName, private string $extendTypeName, ) diff --git a/src/Mappers/GlobExtendTypeMapperCache.php b/src/Mappers/GlobExtendTypeMapperCache.php index 590fe49220..046f5444d5 100644 --- a/src/Mappers/GlobExtendTypeMapperCache.php +++ b/src/Mappers/GlobExtendTypeMapperCache.php @@ -19,11 +19,11 @@ class GlobExtendTypeMapperCache /** * Merges annotations of a given class in the global cache. * - * @param ReflectionClass $refClass + * @param ReflectionClass|class-string $sourceClass */ - public function registerAnnotations(ReflectionClass $refClass, GlobExtendAnnotationsCache $globExtendAnnotationsCache): void + public function registerAnnotations(ReflectionClass|string $sourceClass, GlobExtendAnnotationsCache $globExtendAnnotationsCache): void { - $className = $refClass->getName(); + $className = $sourceClass instanceof ReflectionClass ? $sourceClass->getName() : $sourceClass; $typeClassName = $globExtendAnnotationsCache->getExtendTypeClassName(); if ($typeClassName !== null) { diff --git a/src/Mappers/GlobTypeMapper.php b/src/Mappers/GlobTypeMapper.php deleted file mode 100644 index f094eb7359..0000000000 --- a/src/Mappers/GlobTypeMapper.php +++ /dev/null @@ -1,82 +0,0 @@ -getNamespace(), - ); - - parent::__construct( - $cachePrefix, - $typeGenerator, - $inputTypeGenerator, - $inputTypeUtils, - $container, - $annotationReader, - $namingStrategy, - $recursiveTypeMapper, - $cache, - $globTTL, - $mapTTL, - $classBoundCacheContractFactory, - ); - } - - /** - * Returns the array of globbed classes. - * Only instantiable classes are returned. - * - * @return array> Key: fully qualified class name - */ - protected function getClassList(): array - { - return $this->namespace->getClassList(); - } -} diff --git a/src/Mappers/GlobTypeMapperCache.php b/src/Mappers/GlobTypeMapperCache.php index 24be49646c..032f0f51c9 100644 --- a/src/Mappers/GlobTypeMapperCache.php +++ b/src/Mappers/GlobTypeMapperCache.php @@ -31,11 +31,11 @@ class GlobTypeMapperCache /** * Merges annotations of a given class in the global cache. * - * @param ReflectionClass $refClass + * @param ReflectionClass|class-string $sourceClass */ - public function registerAnnotations(ReflectionClass $refClass, GlobAnnotationsCache $globAnnotationsCache): void + public function registerAnnotations(ReflectionClass|string $sourceClass, GlobAnnotationsCache $globAnnotationsCache): void { - $className = $refClass->getName(); + $className = $sourceClass instanceof ReflectionClass ? $sourceClass->getName() : $sourceClass; $typeClassName = $globAnnotationsCache->getTypeClassName(); if ($typeClassName !== null) { @@ -55,7 +55,7 @@ public function registerAnnotations(ReflectionClass $refClass, GlobAnnotationsCa foreach ($globAnnotationsCache->getFactories() as $methodName => [$inputName, $inputClassName, $isDefault, $declaringClass]) { if ($isDefault) { if ($inputClassName !== null && isset($this->mapClassToFactory[$inputClassName])) { - throw DuplicateMappingException::createForFactory($inputClassName, $this->mapClassToFactory[$inputClassName][0], $this->mapClassToFactory[$inputClassName][1], $refClass->getName(), $methodName); + throw DuplicateMappingException::createForFactory($inputClassName, $this->mapClassToFactory[$inputClassName][0], $this->mapClassToFactory[$inputClassName][1], $className, $methodName); } } else { // If this is not the default factory, let's not map the class name to the factory. @@ -72,12 +72,16 @@ public function registerAnnotations(ReflectionClass $refClass, GlobAnnotationsCa foreach ($globAnnotationsCache->getInputs() as $inputName => [$inputClassName, $isDefault, $description, $isUpdate]) { if ($isDefault) { if (isset($this->mapClassToInput[$inputClassName])) { - throw DuplicateMappingException::createForDefaultInput($refClass->getName()); + throw DuplicateMappingException::createForDefaultInput($className); } $this->mapClassToInput[$inputClassName] = [$className, $inputName, $description, $isUpdate]; } + if (isset($this->mapNameToInput[$inputName])) { + throw DuplicateMappingException::createForTwoInputs($inputName, $this->mapNameToInput[$inputName][0], $inputClassName); + } + $this->mapNameToInput[$inputName] = [$inputClassName, $description, $isUpdate]; } diff --git a/src/Mappers/Parameters/TypeHandler.php b/src/Mappers/Parameters/TypeHandler.php index 8a8eadfe1d..b4b8d098ee 100644 --- a/src/Mappers/Parameters/TypeHandler.php +++ b/src/Mappers/Parameters/TypeHandler.php @@ -42,7 +42,7 @@ use TheCodingMachine\GraphQLite\Parameters\InputTypeParameter; use TheCodingMachine\GraphQLite\Parameters\InputTypeProperty; use TheCodingMachine\GraphQLite\Parameters\ParameterInterface; -use TheCodingMachine\GraphQLite\Reflection\CachedDocBlockFactory; +use TheCodingMachine\GraphQLite\Reflection\DocBlock\DocBlockFactory; use TheCodingMachine\GraphQLite\Types\ArgumentResolver; use TheCodingMachine\GraphQLite\Types\TypeResolver; @@ -68,7 +68,7 @@ public function __construct( private readonly ArgumentResolver $argumentResolver, private readonly RootTypeMapperInterface $rootTypeMapper, private readonly TypeResolver $typeResolver, - private readonly CachedDocBlockFactory $cachedDocBlockFactory, + private readonly DocBlockFactory $docBlockFactory, ) { $this->phpDocumentorTypeResolver = new PhpDocumentorTypeResolver(); @@ -135,7 +135,7 @@ private function getDocBlockPropertyType(DocBlock $docBlock, ReflectionProperty return null; } - $docBlock = $this->cachedDocBlockFactory->getDocBlock($refConstructor); + $docBlock = $this->docBlockFactory->create($refConstructor); $paramTags = $docBlock->getTagsByName('param'); foreach ($paramTags as $paramTag) { if (! $paramTag instanceof Param) { diff --git a/src/Mappers/Root/EnumTypeMapper.php b/src/Mappers/Root/EnumTypeMapper.php index 89a7e041e9..64d4696b9c 100644 --- a/src/Mappers/Root/EnumTypeMapper.php +++ b/src/Mappers/Root/EnumTypeMapper.php @@ -10,19 +10,22 @@ use GraphQL\Type\Definition\Type as GraphQLType; use MyCLabs\Enum\Enum; use phpDocumentor\Reflection\DocBlock; -use phpDocumentor\Reflection\DocBlockFactory; use phpDocumentor\Reflection\Type; use phpDocumentor\Reflection\Types\Object_; use ReflectionClass; use ReflectionEnum; use ReflectionMethod; use ReflectionProperty; -use Symfony\Contracts\Cache\CacheInterface; use TheCodingMachine\GraphQLite\AnnotationReader; +use TheCodingMachine\GraphQLite\Discovery\Cache\ClassFinderComputedCache; +use TheCodingMachine\GraphQLite\Discovery\ClassFinder; +use TheCodingMachine\GraphQLite\Reflection\DocBlock\DocBlockFactory; use TheCodingMachine\GraphQLite\Types\EnumType; -use TheCodingMachine\GraphQLite\Utils\Namespaces\NS; use UnitEnum; +use function array_filter; +use function array_merge; +use function array_values; use function assert; use function enum_exists; use function ltrim; @@ -33,18 +36,18 @@ class EnumTypeMapper implements RootTypeMapperInterface { /** @var array, EnumType> */ - private array $cache = []; + private array $cacheByClass = []; /** @var array */ private array $cacheByName = []; /** @var array> */ - private array|null $nameToClassMapping = null; + private array $nameToClassMapping; - /** @param NS[] $namespaces List of namespaces containing enums. Used when searching an enum by name. */ public function __construct( private readonly RootTypeMapperInterface $next, private readonly AnnotationReader $annotationReader, - private readonly CacheInterface $cacheService, - private readonly array $namespaces, + private readonly DocBlockFactory $docBlockFactory, + private readonly ClassFinder $classFinder, + private readonly ClassFinderComputedCache $classFinderComputedCache, ) { } @@ -102,8 +105,8 @@ private function mapByClassName(string $enumClass): EnumType|null } /** @var class-string $enumClass */ $enumClass = ltrim($enumClass, '\\'); - if (isset($this->cache[$enumClass])) { - return $this->cache[$enumClass]; + if (isset($this->cacheByClass[$enumClass])) { + return $this->cacheByClass[$enumClass]; } // phpcs:disable SlevomatCodingStandard.Commenting.InlineDocCommentDeclaration.MissingVariable @@ -121,14 +124,9 @@ private function mapByClassName(string $enumClass): EnumType|null $reflectionEnum->isBacked() && (string) $reflectionEnum->getBackingType() === 'string'; - $docBlockFactory = DocBlockFactory::createInstance(); - - $enumDescription = null; - $docComment = $reflectionEnum->getDocComment(); - if ($docComment) { - $docBlock = $docBlockFactory->create($docComment); - $enumDescription = $docBlock->getSummary(); - } + $enumDescription = $this->docBlockFactory + ->create($reflectionEnum) + ->getSummary() ?: null; /** @var array $enumCaseDescriptions */ $enumCaseDescriptions = []; @@ -136,15 +134,9 @@ private function mapByClassName(string $enumClass): EnumType|null $enumCaseDeprecationReasons = []; foreach ($reflectionEnum->getCases() as $reflectionEnumCase) { - $docComment = $reflectionEnumCase->getDocComment(); - if (! $docComment) { - continue; - } - - $docBlock = $docBlockFactory->create($docComment); - $enumCaseDescription = $docBlock->getSummary(); + $docBlock = $this->docBlockFactory->create($reflectionEnumCase); - $enumCaseDescriptions[$reflectionEnumCase->getName()] = $enumCaseDescription; + $enumCaseDescriptions[$reflectionEnumCase->getName()] = $docBlock->getSummary() ?: null; $deprecation = $docBlock->getTagsByName('deprecated')[0] ?? null; // phpcs:ignore @@ -155,7 +147,7 @@ private function mapByClassName(string $enumClass): EnumType|null $type = new EnumType($enumClass, $typeName, $enumDescription, $enumCaseDescriptions, $enumCaseDeprecationReasons, $useValues); - return $this->cacheByName[$type->name] = $this->cache[$enumClass] = $type; + return $this->cacheByName[$type->name] = $this->cacheByClass[$enumClass] = $type; } private function getTypeName(ReflectionClass $reflectionClass): string @@ -199,21 +191,19 @@ public function mapNameToType(string $typeName): NamedType&GraphQLType */ private function getNameToClassMapping(): array { - if ($this->nameToClassMapping === null) { - $this->nameToClassMapping = $this->cacheService->get('enum_name_to_class', function () { - $nameToClassMapping = []; - foreach ($this->namespaces as $ns) { - foreach ($ns->getClassList() as $className => $classRef) { - if (! enum_exists($className)) { - continue; - } - - $nameToClassMapping[$this->getTypeName($classRef)] = $className; - } + $this->nameToClassMapping ??= $this->classFinderComputedCache->compute( + $this->classFinder, + 'enum_name_to_class', + function (ReflectionClass $classReflection): array|null { + if (! $classReflection->isEnum()) { + return null; } - return $nameToClassMapping; - }); - } + + return [$this->getTypeName($classReflection) => $classReflection->getName()]; + }, + static fn (array $entries) => array_merge(...array_values(array_filter($entries))), + ); + return $this->nameToClassMapping; } } diff --git a/src/Mappers/Root/MyCLabsEnumTypeMapper.php b/src/Mappers/Root/MyCLabsEnumTypeMapper.php index 7a2d7eae2c..2a3bed7906 100644 --- a/src/Mappers/Root/MyCLabsEnumTypeMapper.php +++ b/src/Mappers/Root/MyCLabsEnumTypeMapper.php @@ -15,11 +15,14 @@ use ReflectionClass; use ReflectionMethod; use ReflectionProperty; -use Symfony\Contracts\Cache\CacheInterface; use TheCodingMachine\GraphQLite\AnnotationReader; +use TheCodingMachine\GraphQLite\Discovery\Cache\ClassFinderComputedCache; +use TheCodingMachine\GraphQLite\Discovery\ClassFinder; use TheCodingMachine\GraphQLite\Types\MyCLabsEnumType; -use TheCodingMachine\GraphQLite\Utils\Namespaces\NS; +use function array_filter; +use function array_merge; +use function array_values; use function assert; use function is_a; use function ltrim; @@ -30,19 +33,18 @@ class MyCLabsEnumTypeMapper implements RootTypeMapperInterface { /** @var array, EnumType> */ - private array $cache = []; + private array $cacheByClass = []; /** @var array */ private array $cacheByName = []; /** @var array> */ - private array|null $nameToClassMapping = null; + private array $nameToClassMapping; - /** @param NS[] $namespaces List of namespaces containing enums. Used when searching an enum by name. */ public function __construct( private readonly RootTypeMapperInterface $next, private readonly AnnotationReader $annotationReader, - private readonly CacheInterface $cacheService, - private readonly array $namespaces, + private readonly ClassFinder $classFinder, + private readonly ClassFinderComputedCache $classFinderComputedCache, ) { } @@ -98,13 +100,13 @@ private function mapByClassName(string $enumClass): EnumType|null } /** @var class-string $enumClass */ $enumClass = ltrim($enumClass, '\\'); - if (isset($this->cache[$enumClass])) { - return $this->cache[$enumClass]; + if (isset($this->cacheByClass[$enumClass])) { + return $this->cacheByClass[$enumClass]; } $refClass = new ReflectionClass($enumClass); $type = new MyCLabsEnumType($enumClass, $this->getTypeName($refClass)); - return $this->cacheByName[$type->name] = $this->cache[$enumClass] = $type; + return $this->cacheByName[$type->name] = $this->cacheByClass[$enumClass] = $type; } private function getTypeName(ReflectionClass $refClass): string @@ -154,23 +156,18 @@ public function mapNameToType(string $typeName): NamedType&\GraphQL\Type\Definit */ private function getNameToClassMapping(): array { - if ($this->nameToClassMapping === null) { - $this->nameToClassMapping = $this->cacheService->get('myclabsenum_name_to_class', function () { - $nameToClassMapping = []; - foreach ($this->namespaces as $ns) { - /** @var class-string $className */ - foreach ($ns->getClassList() as $className => $classRef) { - if (! $classRef->isSubclassOf(Enum::class)) { - continue; - } - - $nameToClassMapping[$this->getTypeName($classRef)] = $className; - } + $this->nameToClassMapping ??= $this->classFinderComputedCache->compute( + $this->classFinder, + 'myclabsenum_name_to_class', + function (ReflectionClass $classReflection): array|null { + if (! $classReflection->isSubclassOf(Enum::class)) { + return null; } - return $nameToClassMapping; - }); - } + return [$this->getTypeName($classReflection) => $classReflection->getName()]; + }, + static fn (array $entries) => array_merge(...array_values(array_filter($entries))), + ); return $this->nameToClassMapping; } diff --git a/src/Mappers/Root/RootTypeMapperFactoryContext.php b/src/Mappers/Root/RootTypeMapperFactoryContext.php index a5e1c1cb11..7786d6fdcf 100644 --- a/src/Mappers/Root/RootTypeMapperFactoryContext.php +++ b/src/Mappers/Root/RootTypeMapperFactoryContext.php @@ -7,11 +7,13 @@ use Psr\Container\ContainerInterface; use Psr\SimpleCache\CacheInterface; use TheCodingMachine\GraphQLite\AnnotationReader; +use TheCodingMachine\GraphQLite\Cache\ClassBoundCache; +use TheCodingMachine\GraphQLite\Discovery\Cache\ClassFinderComputedCache; +use TheCodingMachine\GraphQLite\Discovery\ClassFinder; use TheCodingMachine\GraphQLite\Mappers\RecursiveTypeMapperInterface; use TheCodingMachine\GraphQLite\NamingStrategyInterface; use TheCodingMachine\GraphQLite\TypeRegistry; use TheCodingMachine\GraphQLite\Types\TypeResolver; -use TheCodingMachine\GraphQLite\Utils\Namespaces\NS; /** * A context class containing a number of classes created on the fly by SchemaFactory. @@ -19,11 +21,6 @@ */ final class RootTypeMapperFactoryContext { - /** - * Constructor - * - * @param iterable $typeNamespaces - */ public function __construct( private readonly AnnotationReader $annotationReader, private readonly TypeResolver $typeResolver, @@ -32,9 +29,9 @@ public function __construct( private readonly RecursiveTypeMapperInterface $recursiveTypeMapper, private readonly ContainerInterface $container, private readonly CacheInterface $cache, - private readonly iterable $typeNamespaces, - private readonly int|null $globTTL, - private readonly int|null $mapTTL = null, + private readonly ClassFinder $classFinder, + private readonly ClassFinderComputedCache $classFinderComputedCache, + private readonly ClassBoundCache $classBoundCache, ) { } @@ -73,19 +70,18 @@ public function getCache(): CacheInterface return $this->cache; } - /** @return iterable */ - public function getTypeNamespaces(): iterable + public function getClassFinder(): ClassFinder { - return $this->typeNamespaces; + return $this->classFinder; } - public function getGlobTTL(): int|null + public function getClassFinderComputedCache(): ClassFinderComputedCache { - return $this->globTTL; + return $this->classFinderComputedCache; } - public function getMapTTL(): int|null + public function getClassBoundCache(): ClassBoundCache { - return $this->mapTTL; + return $this->classBoundCache; } } diff --git a/src/Mappers/StaticClassListTypeMapper.php b/src/Mappers/StaticClassListTypeMapper.php deleted file mode 100644 index f5fb0139b0..0000000000 --- a/src/Mappers/StaticClassListTypeMapper.php +++ /dev/null @@ -1,93 +0,0 @@ -> - */ - private array|null $classes = null; - - /** @param array $classList The list of classes to analyze. */ - public function __construct( - private array $classList, - TypeGenerator $typeGenerator, - InputTypeGenerator $inputTypeGenerator, - InputTypeUtils $inputTypeUtils, - ContainerInterface $container, - AnnotationReader $annotationReader, - NamingStrategyInterface $namingStrategy, - RecursiveTypeMapperInterface $recursiveTypeMapper, - CacheInterface $cache, - int|null $globTTL = 2, - int|null $mapTTL = null, - ClassBoundCacheContractFactoryInterface|null $classBoundCacheContractFactory = null, - ) { - $cachePrefix = str_replace( - ['\\', '{', '}', '(', ')', '/', '@', ':'], - '_', - implode('_', $classList), - ); - - parent::__construct( - $cachePrefix, - $typeGenerator, - $inputTypeGenerator, - $inputTypeUtils, - $container, - $annotationReader, - $namingStrategy, - $recursiveTypeMapper, - $cache, - $globTTL, - $mapTTL, - $classBoundCacheContractFactory, - ); - } - - /** - * Returns the array of globbed classes. - * Only instantiable classes are returned. - * - * @return array> Key: fully qualified class name - */ - protected function getClassList(): array - { - if ($this->classes === null) { - $this->classes = []; - foreach ($this->classList as $className) { - if (! class_exists($className) && ! interface_exists($className)) { - throw new GraphQLRuntimeException('Could not find class "' . $className . '"'); - } - $this->classes[$className] = new ReflectionClass($className); - } - } - - return $this->classes; - } -} diff --git a/src/Mappers/StaticClassListTypeMapperFactory.php b/src/Mappers/StaticClassListTypeMapperFactory.php index 774c59c2b7..09fca187ae 100644 --- a/src/Mappers/StaticClassListTypeMapperFactory.php +++ b/src/Mappers/StaticClassListTypeMapperFactory.php @@ -4,18 +4,19 @@ namespace TheCodingMachine\GraphQLite\Mappers; +use TheCodingMachine\GraphQLite\Discovery\StaticClassFinder; use TheCodingMachine\GraphQLite\FactoryContext; use TheCodingMachine\GraphQLite\InputTypeUtils; /** - * A type mapper that is passed the list of classes that it must scan (unlike the GlobTypeMapper that find those automatically). + * A type mapper that is passed the list of classes that it must scan. */ final class StaticClassListTypeMapperFactory implements TypeMapperFactoryInterface { /** * StaticClassListTypeMapperFactory constructor. * - * @param array $classList The list of classes to be scanned. + * @param array $classList The list of classes to be scanned. */ public function __construct( private array $classList, @@ -26,8 +27,8 @@ public function create(FactoryContext $context): TypeMapperInterface { $inputTypeUtils = new InputTypeUtils($context->getAnnotationReader(), $context->getNamingStrategy()); - return new StaticClassListTypeMapper( - $this->classList, + return new ClassFinderTypeMapper( + new StaticClassFinder($this->classList), $context->getTypeGenerator(), $context->getInputTypeGenerator(), $inputTypeUtils, @@ -35,10 +36,7 @@ public function create(FactoryContext $context): TypeMapperInterface $context->getAnnotationReader(), $context->getNamingStrategy(), $context->getRecursiveTypeMapper(), - $context->getCache(), - $context->getGlobTTL(), - $context->getMapTTL(), - $context->getClassBoundCacheContractFactory(), + $context->getClassFinderComputedCache(), ); } } diff --git a/src/Reflection/CachedDocBlockFactory.php b/src/Reflection/CachedDocBlockFactory.php deleted file mode 100644 index 978e1e344d..0000000000 --- a/src/Reflection/CachedDocBlockFactory.php +++ /dev/null @@ -1,133 +0,0 @@ - */ - private array $docBlockArrayCache = []; - /** @var array */ - private array $contextArrayCache = []; - private ContextFactory $contextFactory; - - /** @param CacheInterface $cache The cache we fetch data from. Note this is a SAFE cache. It does not need to be purged. */ - public function __construct(private readonly CacheInterface $cache, DocBlockFactoryInterface|null $docBlockFactory = null) - { - $this->docBlockFactory = $docBlockFactory ?: DocBlockFactory::createInstance(); - $this->contextFactory = new ContextFactory(); - } - - /** - * Fetches a DocBlock object from a ReflectionMethod - * - * @throws InvalidArgumentException - */ - public function getDocBlock(ReflectionMethod|ReflectionProperty $reflector): DocBlock - { - $key = 'docblock_' . md5($reflector->getDeclaringClass()->getName() . '::' . $reflector->getName() . '::' . $reflector::class); - if (isset($this->docBlockArrayCache[$key])) { - return $this->docBlockArrayCache[$key]; - } - - $fileName = $reflector->getDeclaringClass()->getFileName(); - assert(is_string($fileName)); - - $cacheItem = $this->cache->get($key); - if ($cacheItem !== null) { - [ - 'time' => $time, - 'docblock' => $docBlock, - ] = $cacheItem; - - if (filemtime($fileName) === $time) { - $this->docBlockArrayCache[$key] = $docBlock; - - return $docBlock; - } - } - - $docBlock = $this->doGetDocBlock($reflector); - - $this->cache->set($key, [ - 'time' => filemtime($fileName), - 'docblock' => $docBlock, - ]); - $this->docBlockArrayCache[$key] = $docBlock; - - return $docBlock; - } - - private function doGetDocBlock(ReflectionMethod|ReflectionProperty $reflector): DocBlock - { - $docComment = $reflector->getDocComment() ?: '/** */'; - - $refClass = $reflector->getDeclaringClass(); - $refClassName = $refClass->getName(); - - if (! isset($this->contextArrayCache[$refClassName])) { - $this->contextArrayCache[$refClassName] = $this->contextFactory->createFromReflector($reflector); - } - - return $this->docBlockFactory->create($docComment, $this->contextArrayCache[$refClassName]); - } - - /** @param ReflectionClass $reflectionClass */ - public function getContextFromClass(ReflectionClass $reflectionClass): Context - { - $className = $reflectionClass->getName(); - if (isset($this->contextArrayCache[$className])) { - return $this->contextArrayCache[$className]; - } - - $key = 'docblockcontext_' . md5($className); - - $fileName = $reflectionClass->getFileName(); - assert(is_string($fileName)); - - $cacheItem = $this->cache->get($key); - if ($cacheItem !== null) { - [ - 'time' => $time, - 'context' => $context, - ] = $cacheItem; - - if (filemtime($fileName) === $time) { - $this->contextArrayCache[$className] = $context; - - return $context; - } - } - - $context = $this->contextFactory->createFromReflector($reflectionClass); - - $this->cache->set($key, [ - 'time' => filemtime($fileName), - 'context' => $context, - ]); - - $this->contextArrayCache[$className] = $context; - return $context; - } -} diff --git a/src/Reflection/DocBlock/CachedDocBlockFactory.php b/src/Reflection/DocBlock/CachedDocBlockFactory.php new file mode 100644 index 0000000000..b6fb1a8a09 --- /dev/null +++ b/src/Reflection/DocBlock/CachedDocBlockFactory.php @@ -0,0 +1,52 @@ +getDeclaringClass(); + + return $this->classBoundCache->get( + $class, + fn () => $this->docBlockFactory->create($reflector, $context ?? $this->createContext($class)), + 'reflection.docBlock.' . md5($reflector::class . '.' . $reflector->getName()), + ); + } + + public function createContext(ReflectionClass|ReflectionMethod|ReflectionProperty|ReflectionClassConstant $reflector): Context + { + $reflector = $reflector instanceof ReflectionClass ? $reflector : $reflector->getDeclaringClass(); + + return $this->classBoundCache->get( + $reflector, + fn () => $this->docBlockFactory->createContext($reflector), + 'reflection.docBlockContext', + ); + } +} diff --git a/src/Reflection/DocBlock/DocBlockFactory.php b/src/Reflection/DocBlock/DocBlockFactory.php new file mode 100644 index 0000000000..5a0009a5f3 --- /dev/null +++ b/src/Reflection/DocBlock/DocBlockFactory.php @@ -0,0 +1,25 @@ +getDocComment() ?: '/** */'; + + return $this->docBlockFactory->create( + $docblock, + $context ?? $this->createContext($reflector), + ); + } + + public function createContext(ReflectionClass|ReflectionMethod|ReflectionProperty|ReflectionClassConstant $reflector): Context + { + return $this->contextFactory->createFromReflector($reflector); + } +} diff --git a/src/SchemaFactory.php b/src/SchemaFactory.php index 029c5eada8..3991341b3f 100644 --- a/src/SchemaFactory.php +++ b/src/SchemaFactory.php @@ -5,17 +5,27 @@ namespace TheCodingMachine\GraphQLite; use GraphQL\Type\SchemaConfig; +use Kcs\ClassFinder\FileFinder\CachedFileFinder; +use Kcs\ClassFinder\FileFinder\DefaultFileFinder; use Kcs\ClassFinder\Finder\ComposerFinder; use Kcs\ClassFinder\Finder\FinderInterface; use MyCLabs\Enum\Enum; use PackageVersions\Versions; use Psr\Container\ContainerInterface; use Psr\SimpleCache\CacheInterface; +use Symfony\Component\Cache\Adapter\ArrayAdapter; use Symfony\Component\Cache\Adapter\Psr16Adapter; use Symfony\Component\ExpressionLanguage\ExpressionLanguage; -use TheCodingMachine\GraphQLite\Cache\ClassBoundCacheContractFactoryInterface; +use TheCodingMachine\GraphQLite\Cache\ClassBoundCache; +use TheCodingMachine\GraphQLite\Cache\FilesSnapshot; +use TheCodingMachine\GraphQLite\Cache\SnapshotClassBoundCache; +use TheCodingMachine\GraphQLite\Discovery\Cache\HardClassFinderComputedCache; +use TheCodingMachine\GraphQLite\Discovery\Cache\SnapshotClassFinderComputedCache; +use TheCodingMachine\GraphQLite\Discovery\ClassFinder; +use TheCodingMachine\GraphQLite\Discovery\KcsClassFinder; +use TheCodingMachine\GraphQLite\Discovery\StaticClassFinder; +use TheCodingMachine\GraphQLite\Mappers\ClassFinderTypeMapper; use TheCodingMachine\GraphQLite\Mappers\CompositeTypeMapper; -use TheCodingMachine\GraphQLite\Mappers\GlobTypeMapper; use TheCodingMachine\GraphQLite\Mappers\Parameters\ContainerParameterHandler; use TheCodingMachine\GraphQLite\Mappers\Parameters\InjectUserParameterHandler; use TheCodingMachine\GraphQLite\Mappers\Parameters\ParameterMiddlewareInterface; @@ -46,7 +56,8 @@ use TheCodingMachine\GraphQLite\Middlewares\InputFieldMiddlewarePipe; use TheCodingMachine\GraphQLite\Middlewares\SecurityFieldMiddleware; use TheCodingMachine\GraphQLite\Middlewares\SecurityInputFieldMiddleware; -use TheCodingMachine\GraphQLite\Reflection\CachedDocBlockFactory; +use TheCodingMachine\GraphQLite\Reflection\DocBlock\CachedDocBlockFactory; +use TheCodingMachine\GraphQLite\Reflection\DocBlock\PhpDocumentorDocBlockFactory; use TheCodingMachine\GraphQLite\Security\AuthenticationServiceInterface; use TheCodingMachine\GraphQLite\Security\AuthorizationServiceInterface; use TheCodingMachine\GraphQLite\Security\FailAuthenticationService; @@ -56,13 +67,14 @@ use TheCodingMachine\GraphQLite\Types\InputTypeValidatorInterface; use TheCodingMachine\GraphQLite\Types\TypeResolver; use TheCodingMachine\GraphQLite\Utils\NamespacedCache; -use TheCodingMachine\GraphQLite\Utils\Namespaces\NamespaceFactory; -use function array_map; use function array_reverse; use function class_exists; use function md5; use function substr; +use function trigger_error; + +use const E_USER_DEPRECATED; /** * A class to help getting started with GraphQLite. @@ -70,14 +82,8 @@ */ class SchemaFactory { - public const GLOB_CACHE_SECONDS = 2; - - /** @var array */ - private array $controllerNamespaces = []; - - /** @var array */ - private array $typeNamespaces = []; + private array $namespaces = []; /** @var QueryProviderInterface[] */ private array $queryProviders = []; @@ -105,11 +111,11 @@ class SchemaFactory private NamingStrategyInterface|null $namingStrategy = null; - private FinderInterface|null $finder = null; + private ClassFinder|FinderInterface|null $finder = null; private SchemaConfig|null $schemaConfig = null; - private int|null $globTTL = self::GLOB_CACHE_SECONDS; + private bool $devMode = true; /** @var array */ private array $fieldMiddlewares = []; @@ -121,27 +127,50 @@ class SchemaFactory private string $cacheNamespace; - public function __construct(private readonly CacheInterface $cache, private readonly ContainerInterface $container, private ClassBoundCacheContractFactoryInterface|null $classBoundCacheContractFactory = null) - { + public function __construct( + private readonly CacheInterface $cache, + private readonly ContainerInterface $container, + private ClassBoundCache|null $classBoundCache = null, + ) { $this->cacheNamespace = substr(md5(Versions::getVersion('thecodingmachine/graphqlite')), 0, 8); } /** * Registers a namespace that can contain GraphQL controllers. + * + * @deprecated Using SchemaFactory::addControllerNamespace() is deprecated in favor of SchemaFactory::addNamespace() */ public function addControllerNamespace(string $namespace): self { - $this->controllerNamespaces[] = $namespace; + trigger_error( + 'Using SchemaFactory::addControllerNamespace() is deprecated in favor of SchemaFactory::addNamespace().', + E_USER_DEPRECATED, + ); - return $this; + return $this->addNamespace($namespace); } /** * Registers a namespace that can contain GraphQL types. + * + * @deprecated Using SchemaFactory::addTypeNamespace() is deprecated in favor of SchemaFactory::addNamespace() */ public function addTypeNamespace(string $namespace): self { - $this->typeNamespaces[] = $namespace; + trigger_error( + 'Using SchemaFactory::addTypeNamespace() is deprecated in favor of SchemaFactory::addNamespace().', + E_USER_DEPRECATED, + ); + + return $this->addNamespace($namespace); + } + + /** + * Registers a namespace that can contain GraphQL types or controllers. + */ + public function addNamespace(string $namespace): self + { + $this->namespaces[] = $namespace; return $this; } @@ -241,55 +270,52 @@ public function setSchemaConfig(SchemaConfig $schemaConfig): self return $this; } - public function setFinder(FinderInterface $finder): self + public function setFinder(ClassFinder|FinderInterface $finder): self { $this->finder = $finder; return $this; } - /** - * Sets the time to live time of the cache for annotations in files. - * By default this is set to 2 seconds which is ok for development environments. - * Set this to "null" (i.e. infinity) for production environments. - */ + /** @deprecated setGlobTTL(null) or setGlobTTL(0) is equivalent to prodMode(), and any other values are equivalent to devMode() */ public function setGlobTTL(int|null $globTTL): self { - $this->globTTL = $globTTL; + trigger_error( + 'Using SchemaFactory::setGlobTTL() is deprecated in favor of SchemaFactory::devMode() and SchemaFactory::prodMode().', + E_USER_DEPRECATED, + ); - return $this; + return $globTTL ? $this->devMode() : $this->prodMode(); } /** - * Set a custom ClassBoundCacheContractFactory. - * This is used to create CacheContracts that store reflection results. - * Set this to "null" to use the default fallback factory. + * Set a custom class bound cache. By default in dev mode it looks at file modification times. */ - public function setClassBoundCacheContractFactory(ClassBoundCacheContractFactoryInterface|null $classBoundCacheContractFactory): self + public function setClassBoundCache(ClassBoundCache|null $classBoundCache): self { - $this->classBoundCacheContractFactory = $classBoundCacheContractFactory; + $this->classBoundCache = $classBoundCache; return $this; } /** * Sets GraphQLite in "prod" mode (cache settings optimized for best performance). - * - * This is a shortcut for `$schemaFactory->setGlobTTL(null)` */ public function prodMode(): self { - return $this->setGlobTTL(null); + $this->devMode = false; + + return $this; } /** * Sets GraphQLite in "dev" mode (this is the default mode: cache settings optimized for best developer experience). - * - * This is a shortcut for `$schemaFactory->setGlobTTL(2)` */ public function devMode(): self { - return $this->setGlobTTL(self::GLOB_CACHE_SECONDS); + $this->devMode = true; + + return $this; } /** @@ -331,16 +357,20 @@ public function createSchema(): Schema $authorizationService = $this->authorizationService ?: new FailAuthorizationService(); $typeResolver = new TypeResolver(); $namespacedCache = new NamespacedCache($this->cache); - $cachedDocBlockFactory = new CachedDocBlockFactory($namespacedCache); + $classBoundCache = $this->classBoundCache ?: new SnapshotClassBoundCache( + $this->cache, + $this->devMode ? FilesSnapshot::forClass(...) : FilesSnapshot::alwaysUnchanged(...), + ); + $docBlockFactory = new CachedDocBlockFactory( + $classBoundCache, + PhpDocumentorDocBlockFactory::default(), + ); $namingStrategy = $this->namingStrategy ?: new NamingStrategy(); $typeRegistry = new TypeRegistry(); - $finder = $this->finder ?? new ComposerFinder(); - - $namespaceFactory = new NamespaceFactory($namespacedCache, $finder, $this->globTTL); - $nsList = array_map( - static fn (string $namespace) => $namespaceFactory->createNamespace($namespace), - $this->typeNamespaces, - ); + $classFinder = $this->createClassFinder(); + $classFinderComputedCache = $this->devMode ? + new SnapshotClassFinderComputedCache($this->cache) : + new HardClassFinderComputedCache($this->cache); $expressionLanguage = $this->expressionLanguage ?: new ExpressionLanguage($symfonyCache); $expressionLanguage->registerProvider(new SecurityExpressionLanguageProvider()); @@ -371,11 +401,11 @@ public function createSchema(): Schema $errorRootTypeMapper = new FinalRootTypeMapper($recursiveTypeMapper); $rootTypeMapper = new BaseTypeMapper($errorRootTypeMapper, $recursiveTypeMapper, $topRootTypeMapper); - $rootTypeMapper = new EnumTypeMapper($rootTypeMapper, $annotationReader, $symfonyCache, $nsList); + $rootTypeMapper = new EnumTypeMapper($rootTypeMapper, $annotationReader, $docBlockFactory, $classFinder, $classFinderComputedCache); if (class_exists(Enum::class)) { // Annotation support - deprecated - $rootTypeMapper = new MyCLabsEnumTypeMapper($rootTypeMapper, $annotationReader, $symfonyCache, $nsList); + $rootTypeMapper = new MyCLabsEnumTypeMapper($rootTypeMapper, $annotationReader, $classFinder, $classFinderComputedCache); } if (! empty($this->rootTypeMapperFactories)) { @@ -387,8 +417,9 @@ public function createSchema(): Schema $recursiveTypeMapper, $this->container, $namespacedCache, - $nsList, - $this->globTTL, + classFinder: $classFinder, + classFinderComputedCache: $classFinderComputedCache, + classBoundCache: $classBoundCache, ); $reversedRootTypeMapperFactories = array_reverse($this->rootTypeMapperFactories); @@ -410,7 +441,7 @@ public function createSchema(): Schema $recursiveTypeMapper, $argumentResolver, $typeResolver, - $cachedDocBlockFactory, + $docBlockFactory, $namingStrategy, $topRootTypeMapper, $parameterMiddlewarePipe, @@ -431,9 +462,9 @@ public function createSchema(): Schema $inputTypeUtils = new InputTypeUtils($annotationReader, $namingStrategy); $inputTypeGenerator = new InputTypeGenerator($inputTypeUtils, $fieldsBuilder, $this->inputTypeValidator); - foreach ($nsList as $ns) { - $compositeTypeMapper->addTypeMapper(new GlobTypeMapper( - $ns, + if ($this->namespaces) { + $compositeTypeMapper->addTypeMapper(new ClassFinderTypeMapper( + $classFinder, $typeGenerator, $inputTypeGenerator, $inputTypeUtils, @@ -441,9 +472,7 @@ public function createSchema(): Schema $annotationReader, $namingStrategy, $recursiveTypeMapper, - $namespacedCache, - $this->globTTL, - classBoundCacheContractFactory: $this->classBoundCacheContractFactory, + $classFinderComputedCache, )); } @@ -460,8 +489,9 @@ classBoundCacheContractFactory: $this->classBoundCacheContractFactory, $this->container, $namespacedCache, $this->inputTypeValidator, - $this->globTTL, - classBoundCacheContractFactory: $this->classBoundCacheContractFactory, + classFinder: $classFinder, + classFinderComputedCache: $classFinderComputedCache, + classBoundCache: $classBoundCache, ); } @@ -469,8 +499,8 @@ classBoundCacheContractFactory: $this->classBoundCacheContractFactory, $this->typeMappers[] = $typeMapperFactory->create($context); } - if (empty($this->typeNamespaces) && empty($this->typeMappers)) { - throw new GraphQLRuntimeException('Cannot create schema: no namespace for types found (You must call the SchemaFactory::addTypeNamespace() at least once).'); + if (empty($this->namespaces) && empty($this->typeMappers)) { + throw new GraphQLRuntimeException('Cannot create schema: no namespace for types found (You must call the SchemaFactory::addNamespace() at least once).'); } foreach ($this->typeMappers as $typeMapper) { @@ -480,15 +510,14 @@ classBoundCacheContractFactory: $this->classBoundCacheContractFactory, $compositeTypeMapper->addTypeMapper(new PorpaginasTypeMapper($recursiveTypeMapper)); $queryProviders = []; - foreach ($this->controllerNamespaces as $controllerNamespace) { + + if ($this->namespaces) { $queryProviders[] = new GlobControllerQueryProvider( - $controllerNamespace, $fieldsBuilder, $this->container, $annotationReader, - $namespacedCache, - $finder, - $this->globTTL, + $classFinder, + $classFinderComputedCache, ); } @@ -501,11 +530,41 @@ classBoundCacheContractFactory: $this->classBoundCacheContractFactory, } if ($queryProviders === []) { - throw new GraphQLRuntimeException('Cannot create schema: no namespace for controllers found (You must call the SchemaFactory::addControllerNamespace() at least once).'); + throw new GraphQLRuntimeException('Cannot create schema: no namespace for controllers found (You must call the SchemaFactory::addNamespace() at least once).'); } $aggregateQueryProvider = new AggregateQueryProvider($queryProviders); return new Schema($aggregateQueryProvider, $recursiveTypeMapper, $typeResolver, $topRootTypeMapper, $this->schemaConfig); } + + private function createClassFinder(): ClassFinder + { + if ($this->finder instanceof ClassFinder) { + return $this->finder; + } + + // When no namespaces are specified, class finder uses all available namespaces to discover classes. + // While this is technically okay, it doesn't follow SchemaFactory's semantics that allow it's + // users to manually specify classes (see SchemaFactory::testCreateSchemaOnlyWithFactories()), + // without having to specify namespaces to glob. This solves it by providing an empty iterator. + if (! $this->namespaces) { + return new StaticClassFinder([]); + } + + $finder = (clone ($this->finder ?? new ComposerFinder())); + + // Because this finder may be iterated more than once, we need to make + // sure that the filesystem is only hit once in the lifetime of the application, + // as that may be expensive for larger projects or non-native filesystems. + if ($finder instanceof ComposerFinder) { + $finder = $finder->withFileFinder(new CachedFileFinder(new DefaultFileFinder(), new ArrayAdapter())); + } + + foreach ($this->namespaces as $namespace) { + $finder = $finder->inNamespace($namespace); + } + + return new KcsClassFinder($finder); + } } diff --git a/src/Utils/Namespaces/NS.php b/src/Utils/Namespaces/NS.php deleted file mode 100644 index f1c51f3a18..0000000000 --- a/src/Utils/Namespaces/NS.php +++ /dev/null @@ -1,104 +0,0 @@ -> - */ - 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 FinderInterface $finder, - private readonly int|null $globTTL, - ) - { - } - - /** - * Returns the array of globbed classes. - * Only instantiable classes are returned. - * - * @return array> Key: fully qualified class name - */ - public function getClassList(): array - { - if ($this->classes === null) { - $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; - } - - if ($this->classes === null) { - $this->classes = []; - /** @var class-string $className */ - /** @var ReflectionClass $reflector */ - foreach ((clone $this->finder)->inNamespace($this->namespace) as $className => $reflector) { - if (! ($reflector instanceof ReflectionClass)) { - continue; - } - - $this->classes[$className] = $reflector; - } - try { - $this->cache->set($cacheKey, array_keys($this->classes), $this->globTTL); - } catch (InvalidArgumentException) { - // @ignoreException - } - } - } - - return $this->classes; - } - - public function getNamespace(): string - { - return $this->namespace; - } -} diff --git a/src/Utils/Namespaces/NamespaceFactory.php b/src/Utils/Namespaces/NamespaceFactory.php deleted file mode 100644 index 719c80bb22..0000000000 --- a/src/Utils/Namespaces/NamespaceFactory.php +++ /dev/null @@ -1,29 +0,0 @@ -cache, clone $this->finder, $this->globTTL); - } -} diff --git a/tests/AbstractQueryProvider.php b/tests/AbstractQueryProvider.php index 129068079c..8e8c09fe72 100644 --- a/tests/AbstractQueryProvider.php +++ b/tests/AbstractQueryProvider.php @@ -10,6 +10,8 @@ use GraphQL\Type\Definition\OutputType; use GraphQL\Type\Definition\Type; use GraphQL\Type\Schema; +use Kcs\ClassFinder\FileFinder\CachedFileFinder; +use Kcs\ClassFinder\FileFinder\DefaultFileFinder; use Kcs\ClassFinder\Finder\ComposerFinder; use phpDocumentor\Reflection\TypeResolver as PhpDocumentorTypeResolver; use PHPUnit\Framework\TestCase; @@ -18,9 +20,17 @@ use Symfony\Component\Cache\Adapter\Psr16Adapter; use Symfony\Component\Cache\Psr16Cache; use Symfony\Component\ExpressionLanguage\ExpressionLanguage; +use TheCodingMachine\GraphQLite\Cache\ClassBoundCache; +use TheCodingMachine\GraphQLite\Cache\FilesSnapshot; +use TheCodingMachine\GraphQLite\Cache\SnapshotClassBoundCache; use TheCodingMachine\GraphQLite\Containers\BasicAutoWiringContainer; use TheCodingMachine\GraphQLite\Containers\EmptyContainer; use TheCodingMachine\GraphQLite\Containers\LazyContainer; +use TheCodingMachine\GraphQLite\Discovery\Cache\ClassFinderComputedCache; +use TheCodingMachine\GraphQLite\Discovery\Cache\HardClassFinderComputedCache; +use TheCodingMachine\GraphQLite\Discovery\ClassFinder; +use TheCodingMachine\GraphQLite\Discovery\StaticClassFinder; +use TheCodingMachine\GraphQLite\Discovery\KcsClassFinder; use TheCodingMachine\GraphQLite\Fixtures\Mocks\MockResolvableInputObjectType; use TheCodingMachine\GraphQLite\Fixtures\TestObject; use TheCodingMachine\GraphQLite\Fixtures\TestObject2; @@ -45,7 +55,9 @@ use TheCodingMachine\GraphQLite\Middlewares\FieldMiddlewarePipe; use TheCodingMachine\GraphQLite\Middlewares\InputFieldMiddlewarePipe; use TheCodingMachine\GraphQLite\Middlewares\SecurityFieldMiddleware; -use TheCodingMachine\GraphQLite\Reflection\CachedDocBlockFactory; +use TheCodingMachine\GraphQLite\Reflection\DocBlock\CachedDocBlockFactory; +use TheCodingMachine\GraphQLite\Reflection\DocBlock\DocBlockFactory; +use TheCodingMachine\GraphQLite\Reflection\DocBlock\PhpDocumentorDocBlockFactory; use TheCodingMachine\GraphQLite\Security\SecurityExpressionLanguageProvider; use TheCodingMachine\GraphQLite\Security\VoidAuthenticationService; use TheCodingMachine\GraphQLite\Security\VoidAuthorizationService; @@ -54,7 +66,6 @@ use TheCodingMachine\GraphQLite\Types\MutableObjectType; use TheCodingMachine\GraphQLite\Types\ResolvableMutableInputInterface; use TheCodingMachine\GraphQLite\Types\TypeResolver; -use TheCodingMachine\GraphQLite\Utils\Namespaces\NamespaceFactory; abstract class AbstractQueryProvider extends TestCase { @@ -73,7 +84,6 @@ abstract class AbstractQueryProvider extends TestCase private $typeRegistry; private $parameterMiddlewarePipe; private $rootTypeMapper; - private $namespaceFactory; protected function getTestObjectType(): MutableObjectType { @@ -265,13 +275,21 @@ protected function getParameterMiddlewarePipe(): ParameterMiddlewarePipe return $this->parameterMiddlewarePipe; } - protected function getCachedDocBlockFactory(): CachedDocBlockFactory + protected function getDocBlockFactory(): DocBlockFactory + { + return new CachedDocBlockFactory( + $this->getClassBoundCache(), + PhpDocumentorDocBlockFactory::default(), + ); + } + + private function getClassBoundCache(): ClassBoundCache { $arrayAdapter = new ArrayAdapter(); $arrayAdapter->setLogger(new ExceptionLogger()); $psr16Cache = new Psr16Cache($arrayAdapter); - return new CachedDocBlockFactory($psr16Cache); + return new SnapshotClassBoundCache($psr16Cache, FilesSnapshot::alwaysUnchanged(...)); } protected function buildFieldsBuilder(): FieldsBuilder @@ -307,7 +325,7 @@ protected function buildFieldsBuilder(): FieldsBuilder $this->getTypeMapper(), $this->getArgumentResolver(), $this->getTypeResolver(), - $this->getCachedDocBlockFactory(), + $this->getDocBlockFactory(), new NamingStrategy(), $this->buildRootTypeMapper(), $parameterMiddlewarePipe, @@ -350,15 +368,16 @@ protected function buildRootTypeMapper(): RootTypeMapperInterface $rootTypeMapper = new MyCLabsEnumTypeMapper( $rootTypeMapper, $this->getAnnotationReader(), - $arrayAdapter, - [], + new StaticClassFinder([]), + new HardClassFinderComputedCache(new Psr16Cache($arrayAdapter)), ); $rootTypeMapper = new EnumTypeMapper( $rootTypeMapper, $this->getAnnotationReader(), - $arrayAdapter, - [], + $this->getDocBlockFactory(), + new StaticClassFinder([]), + new HardClassFinderComputedCache(new Psr16Cache($arrayAdapter)), ); $rootTypeMapper = new CompoundTypeMapper( @@ -449,15 +468,28 @@ protected static function resolveType(string $type): \phpDocumentor\Reflection\T return (new PhpDocumentorTypeResolver())->resolve($type); } - protected function getNamespaceFactory(): NamespaceFactory + protected function getClassFinder(array|string $namespaces): ClassFinder { - if ($this->namespaceFactory === null) { - $arrayAdapter = new ArrayAdapter(); - $arrayAdapter->setLogger(new ExceptionLogger()); - $psr16Cache = new Psr16Cache($arrayAdapter); + $finder = new ComposerFinder(); - $this->namespaceFactory = new NamespaceFactory($psr16Cache, new ComposerFinder()); + foreach ((array) $namespaces as $namespace) { + $finder->inNamespace($namespace); } - return $this->namespaceFactory; + + $arrayAdapter = new ArrayAdapter(); + $arrayAdapter->setLogger(new ExceptionLogger()); + + $finder = $finder->withFileFinder(new CachedFileFinder(new DefaultFileFinder(), $arrayAdapter)); + + return new KcsClassFinder($finder); + } + + protected function getClassFinderComputedCache(): ClassFinderComputedCache + { + $arrayAdapter = new ArrayAdapter(); + $arrayAdapter->setLogger(new ExceptionLogger()); + $psr16Cache = new Psr16Cache($arrayAdapter); + + return new HardClassFinderComputedCache($psr16Cache); } } diff --git a/tests/Cache/FilesSnapshotTest.php b/tests/Cache/FilesSnapshotTest.php new file mode 100644 index 0000000000..f45aa2c75e --- /dev/null +++ b/tests/Cache/FilesSnapshotTest.php @@ -0,0 +1,77 @@ +changed()); + + // Make sure it serializes properly + /** @var FilesSnapshot $snapshot */ + $snapshot = unserialize(serialize($snapshot)); + + self::assertFalse($snapshot->changed()); + + $this->touch($fooReflection->getFileName()); + + self::assertTrue($snapshot->changed()); + } + + public function testDoesNotTrackChangesInSuperTypesWithoutUsingInheritance(): void + { + $fooReflection = new \ReflectionClass(FooType::class); + $snapshot = FilesSnapshot::forClass($fooReflection); + + self::assertFalse($snapshot->changed()); + + $this->touch($fooReflection->getParentClass()->getFileName()); + + self::assertFalse($snapshot->changed()); + } + + public function testTracksChangesInSuperTypesUsingInheritance(): void + { + $fooReflection = new \ReflectionClass(FooType::class); + $snapshot = FilesSnapshot::forClass($fooReflection, true); + + self::assertFalse($snapshot->changed()); + + $this->touch($fooReflection->getParentClass()->getFileName()); + + self::assertTrue($snapshot->changed()); + } + + public function testTracksChangesInFile(): void + { + $fileName = (new \ReflectionClass(FooType::class))->getFileName(); + $snapshot = FilesSnapshot::for([$fileName]); + + self::assertFalse($snapshot->changed()); + + $this->touch($fileName); + + self::assertTrue($snapshot->changed()); + } + + private function touch(string $fileName): void + { + touch($fileName, filemtime($fileName) + 1); + clearstatcache(); + } +} \ No newline at end of file diff --git a/tests/Cache/SnapshotClassBoundCacheTest.php b/tests/Cache/SnapshotClassBoundCacheTest.php new file mode 100644 index 0000000000..d9e9390e74 --- /dev/null +++ b/tests/Cache/SnapshotClassBoundCacheTest.php @@ -0,0 +1,58 @@ +get($fooReflection, fn () => 'foo_key', 'key', true); + + self::assertSame('foo_key', $fooKeyResult); + self::assertSame('foo_key', $classBoundCache->get($fooReflection, fn () => self::fail('Should not be called.'), 'key', true)); + + $fooDifferentKeyResult = $classBoundCache->get($fooReflection, fn () => 'foo_different_key', 'different_key', true); + + self::assertSame('foo_different_key', $fooDifferentKeyResult); + self::assertSame('foo_different_key', $classBoundCache->get($fooReflection, fn () => self::fail('Should not be called.'), 'different_key', true)); + + $barReflection = new \ReflectionClass(NoTypeAnnotation::class); + $barKeyResult = $classBoundCache->get($barReflection, fn () => 'bar_key', 'key', true); + + self::assertSame('bar_key', $barKeyResult); + self::assertSame('bar_key', $classBoundCache->get($barReflection, fn () => self::fail('Should not be called.'), 'key', true)); + + self::assertCount(3, $arrayCache->getValues()); + + $this->touch($fooReflection->getParentClass()->getFileName()); + + self::assertSame( + 'foo_key_updated', + $classBoundCache->get($fooReflection, fn () => 'foo_key_updated', 'key', true) + ); + } + + private function touch(string $fileName): void + { + touch($fileName, filemtime($fileName) + 1); + clearstatcache(); + } +} \ No newline at end of file diff --git a/tests/Discovery/Cache/HardClassFinderComputedCacheTest.php b/tests/Discovery/Cache/HardClassFinderComputedCacheTest.php new file mode 100644 index 0000000000..407550a0b3 --- /dev/null +++ b/tests/Discovery/Cache/HardClassFinderComputedCacheTest.php @@ -0,0 +1,58 @@ +setLogger(new ExceptionLogger()); + $cache = new Psr16Cache($arrayAdapter); + + $classFinderComputedCache = new HardClassFinderComputedCache($cache); + + [$result] = $classFinderComputedCache->compute( + new StaticClassFinder([ + FooType::class, + FooExtendType::class, + TestType::class, + ]), + 'key', + fn (\ReflectionClass $reflection) => $reflection->getShortName(), + fn (array $entries) => [array_values($entries)], + ); + + $this->assertSame([ + 'FooType', + 'FooExtendType', + 'TestType', + ], $result); + + // Even though the class finder and both functions have changed - the result should still be cached. + // This is useful in production, where code and file structure doesn't change. + [$result] = $classFinderComputedCache->compute( + new StaticClassFinder([]), + 'key', + fn (\ReflectionClass $reflection) => self::fail('Should not be called.'), + fn (array $entries) => self::fail('Should not be called.'), + ); + + $this->assertSame([ + 'FooType', + 'FooExtendType', + 'TestType', + ], $result); + } +} \ No newline at end of file diff --git a/tests/Discovery/Cache/SnapshotClassFinderComputedCacheTest.php b/tests/Discovery/Cache/SnapshotClassFinderComputedCacheTest.php new file mode 100644 index 0000000000..bbf50389d1 --- /dev/null +++ b/tests/Discovery/Cache/SnapshotClassFinderComputedCacheTest.php @@ -0,0 +1,89 @@ +setLogger(new ExceptionLogger()); + $cache = new Psr16Cache($arrayAdapter); + + $classFinderComputedCache = new SnapshotClassFinderComputedCache($cache); + + [$result] = $classFinderComputedCache->compute( + new StaticClassFinder([ + FooType::class, + FooExtendType::class, + TestType::class, + ]), + 'key', + fn (\ReflectionClass $reflection) => $reflection->getShortName(), + fn (array $entries) => [array_values($entries)], + ); + + $this->assertSame([ + 'FooType', + 'FooExtendType', + 'TestType', + ], $result); + + [$result] = $classFinderComputedCache->compute( + new StaticClassFinder([ + FooType::class, + FooExtendType::class, + TestType::class, + ]), + 'key', + fn (\ReflectionClass $reflection) => self::fail('Should not be called.'), + fn (array $entries) => [array_values($entries)], + ); + + $this->assertSame([ + 'FooType', + 'FooExtendType', + 'TestType', + ], $result); + + $this->touch((new \ReflectionClass(FooType::class))->getFileName()); + + [$result] = $classFinderComputedCache->compute( + new StaticClassFinder([ + FooType::class, + TestType::class, + EnumType::class, + ]), + 'key', + fn (\ReflectionClass $reflection) => $reflection->getShortName() . ' Modified', + fn (array $entries) => [array_values($entries)], + ); + + $this->assertSame([ + 'FooType Modified', + 'TestType', + 'EnumType Modified', + ], $result); + } + + private function touch(string $fileName): void + { + touch($fileName, filemtime($fileName) + 1); + clearstatcache(); + } +} \ No newline at end of file diff --git a/tests/Discovery/KcsClassFinderTest.php b/tests/Discovery/KcsClassFinderTest.php new file mode 100644 index 0000000000..589308236c --- /dev/null +++ b/tests/Discovery/KcsClassFinderTest.php @@ -0,0 +1,53 @@ +inNamespace('TheCodingMachine\GraphQLite\Fixtures\Types') + ); + + $finderWithPath = $finder->withPathFilter(fn (string $path) => str_contains($path, 'FooExtendType.php')); + + $this->assertFoundClasses([ + TestFactory::class, + GetterSetterType::class, + FooType::class, + MagicGetterSetterType::class, + FooExtendType::class, + NoTypeAnnotation::class, + AbstractFooType::class, + EnumType::class, + ], $finder); + + $this->assertFoundClasses([ + FooExtendType::class, + ], $finderWithPath); + } + + private function assertFoundClasses(array $expectedClasses, ClassFinder $classFinder): void + { + $result = iterator_to_array($classFinder); + + $this->assertContainsOnlyInstancesOf(\ReflectionClass::class, $result); + $this->assertEqualsCanonicalizing($expectedClasses, array_keys($result)); + } +} \ No newline at end of file diff --git a/tests/Discovery/StaticClassFinderTest.php b/tests/Discovery/StaticClassFinderTest.php new file mode 100644 index 0000000000..51b6a00501 --- /dev/null +++ b/tests/Discovery/StaticClassFinderTest.php @@ -0,0 +1,42 @@ +withPathFilter(fn (string $path) => str_contains($path, 'FooExtendType.php')); + + $this->assertFoundClasses([ + FooType::class, + TestType::class, + FooExtendType::class, + ], $finder); + + $this->assertFoundClasses([ + FooExtendType::class, + ], $finderWithPath); + } + + private function assertFoundClasses(array $expectedClasses, ClassFinder $classFinder): void + { + $result = iterator_to_array($classFinder); + + $this->assertContainsOnlyInstancesOf(\ReflectionClass::class, $result); + $this->assertEqualsCanonicalizing($expectedClasses, array_keys($result)); + } +} \ No newline at end of file diff --git a/tests/FactoryContextTest.php b/tests/FactoryContextTest.php index 9f02729924..3ab131851a 100644 --- a/tests/FactoryContextTest.php +++ b/tests/FactoryContextTest.php @@ -4,8 +4,11 @@ use Symfony\Component\Cache\Adapter\ArrayAdapter; use Symfony\Component\Cache\Psr16Cache; -use TheCodingMachine\GraphQLite\Cache\ClassBoundCacheContractFactory; +use TheCodingMachine\GraphQLite\Cache\FilesSnapshot; +use TheCodingMachine\GraphQLite\Cache\SnapshotClassBoundCache; use TheCodingMachine\GraphQLite\Containers\EmptyContainer; +use TheCodingMachine\GraphQLite\Discovery\Cache\HardClassFinderComputedCache; +use TheCodingMachine\GraphQLite\Discovery\StaticClassFinder; use TheCodingMachine\GraphQLite\Fixtures\Inputs\Validator; class FactoryContextTest extends AbstractQueryProvider @@ -17,7 +20,9 @@ public function testContext(): void $namingStrategy = new NamingStrategy(); $container = new EmptyContainer(); $arrayCache = new Psr16Cache(new ArrayAdapter()); - $classBoundCacheContractFactory = new ClassBoundCacheContractFactory(); + $classFinder = new StaticClassFinder([]); + $classFinderComputedCache = new HardClassFinderComputedCache($arrayCache); + $classBoundCache = new SnapshotClassBoundCache($arrayCache, FilesSnapshot::alwaysUnchanged(...)); $validator = new Validator(); $context = new FactoryContext( @@ -32,8 +37,9 @@ public function testContext(): void $container, $arrayCache, $validator, - self::GLOB_TTL_SECONDS, - classBoundCacheContractFactory: $classBoundCacheContractFactory, + classFinder: $classFinder, + classFinderComputedCache: $classFinderComputedCache, + classBoundCache: $classBoundCache, ); $this->assertSame($this->getAnnotationReader(), $context->getAnnotationReader()); @@ -47,7 +53,8 @@ classBoundCacheContractFactory: $classBoundCacheContractFactory, $this->assertSame($container, $context->getContainer()); $this->assertSame($arrayCache, $context->getCache()); $this->assertSame($validator, $context->getInputTypeValidator()); - $this->assertSame(self::GLOB_TTL_SECONDS, $context->getGlobTTL()); - $this->assertNull($context->getMapTTL()); + $this->assertSame($classFinder, $context->getClassFinder()); + $this->assertSame($classFinderComputedCache, $context->getClassFinderComputedCache()); + $this->assertSame($classBoundCache, $context->getClassBoundCache()); } } diff --git a/tests/FieldsBuilderTest.php b/tests/FieldsBuilderTest.php index ffcc0b522f..0cd11d0d49 100644 --- a/tests/FieldsBuilderTest.php +++ b/tests/FieldsBuilderTest.php @@ -67,7 +67,7 @@ use TheCodingMachine\GraphQLite\Middlewares\InputFieldMiddlewarePipe; use TheCodingMachine\GraphQLite\Middlewares\MissingMagicGetException; use TheCodingMachine\GraphQLite\Parameters\MissingArgumentException; -use TheCodingMachine\GraphQLite\Reflection\CachedDocBlockFactory; +use TheCodingMachine\GraphQLite\Reflection\DocBlock\CachedDocBlockFactory; use TheCodingMachine\GraphQLite\Security\AuthenticationServiceInterface; use TheCodingMachine\GraphQLite\Security\AuthorizationServiceInterface; use TheCodingMachine\GraphQLite\Security\VoidAuthenticationService; @@ -353,7 +353,7 @@ public function getUser(): object|null $this->getTypeMapper(), $this->getArgumentResolver(), $this->getTypeResolver(), - new CachedDocBlockFactory(new Psr16Cache(new ArrayAdapter())), + $this->getDocBlockFactory(), new NamingStrategy(), $this->getRootTypeMapper(), $this->getParameterMiddlewarePipe(), @@ -384,7 +384,7 @@ public function isAllowed(string $right, $subject = null): bool $this->getTypeMapper(), $this->getArgumentResolver(), $this->getTypeResolver(), - new CachedDocBlockFactory(new Psr16Cache(new ArrayAdapter())), + $this->getDocBlockFactory(), new NamingStrategy(), $this->getRootTypeMapper(), $this->getParameterMiddlewarePipe(), @@ -445,7 +445,7 @@ public function testFromSourceFieldsInterface(): void $this->getTypeMapper(), $this->getArgumentResolver(), $this->getTypeResolver(), - new CachedDocBlockFactory(new Psr16Cache(new ArrayAdapter())), + $this->getDocBlockFactory(), new NamingStrategy(), $this->getRootTypeMapper(), $this->getParameterMiddlewarePipe(), diff --git a/tests/GlobControllerQueryProviderTest.php b/tests/GlobControllerQueryProviderTest.php index 25f824b2f5..da72610c7b 100644 --- a/tests/GlobControllerQueryProviderTest.php +++ b/tests/GlobControllerQueryProviderTest.php @@ -9,6 +9,8 @@ use ReflectionClass; use Symfony\Component\Cache\Adapter\NullAdapter; use Symfony\Component\Cache\Psr16Cache; +use TheCodingMachine\GraphQLite\Discovery\Cache\HardClassFinderComputedCache; +use TheCodingMachine\GraphQLite\Discovery\KcsClassFinder; use TheCodingMachine\GraphQLite\Fixtures\TestController; class GlobControllerQueryProviderTest extends AbstractQueryProvider @@ -38,15 +40,14 @@ public function has($id): bool }; $finder = new ComposerFinder(); + $finder->inNamespace('TheCodingMachine\\GraphQLite\\Fixtures'); $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()), - $finder, - 0, + new KcsClassFinder($finder), + new HardClassFinderComputedCache(new Psr16Cache(new NullAdapter())) ); $queries = $globControllerQueryProvider->getQueries(); diff --git a/tests/Http/Psr15GraphQLMiddlewareBuilderTest.php b/tests/Http/Psr15GraphQLMiddlewareBuilderTest.php index 885b64f891..d72062ac30 100644 --- a/tests/Http/Psr15GraphQLMiddlewareBuilderTest.php +++ b/tests/Http/Psr15GraphQLMiddlewareBuilderTest.php @@ -39,9 +39,7 @@ public function testCreateMiddleware() $factory = new SchemaFactory($cache, $container); $factory->setAuthenticationService(new VoidAuthenticationService()); $factory->setAuthorizationService(new VoidAuthorizationService()); - - $factory->addControllerNamespace('TheCodingMachine\\GraphQLite\\Fixtures\\Integration\\Controllers'); - $factory->addTypeNamespace('TheCodingMachine\\GraphQLite\\Fixtures\\Integration'); + $factory->addNamespace('TheCodingMachine\\GraphQLite\\Fixtures\\Integration'); $schema = $factory->createSchema(); diff --git a/tests/Integration/AnnotatedInterfaceTest.php b/tests/Integration/AnnotatedInterfaceTest.php index d3c81b64ce..b24666de2b 100644 --- a/tests/Integration/AnnotatedInterfaceTest.php +++ b/tests/Integration/AnnotatedInterfaceTest.php @@ -24,8 +24,7 @@ public function setUp(): void $container = new BasicAutoWiringContainer(new EmptyContainer()); $schemaFactory = new SchemaFactory(new Psr16Cache(new ArrayAdapter()), $container); - $schemaFactory->addControllerNamespace('TheCodingMachine\\GraphQLite\\Fixtures\\AnnotatedInterfaces\\Controllers'); - $schemaFactory->addTypeNamespace('TheCodingMachine\\GraphQLite\\Fixtures\\AnnotatedInterfaces\\Types'); + $schemaFactory->addNamespace('TheCodingMachine\\GraphQLite\\Fixtures\\AnnotatedInterfaces'); $this->schema = $schemaFactory->createSchema(); } diff --git a/tests/Integration/EndToEndTest.php b/tests/Integration/EndToEndTest.php index ca9eb58033..0d9f2086c4 100644 --- a/tests/Integration/EndToEndTest.php +++ b/tests/Integration/EndToEndTest.php @@ -5,23 +5,16 @@ namespace TheCodingMachine\GraphQLite\Integration; use GraphQL\Error\DebugFlag; -use GraphQL\Executor\ExecutionResult; use GraphQL\GraphQL; use GraphQL\Server\Helper; use GraphQL\Server\OperationParams; use GraphQL\Server\ServerConfig; -use PHPUnit\Framework\TestCase; use Psr\Container\ContainerInterface; use stdClass; use Symfony\Component\Cache\Adapter\ArrayAdapter; -use Symfony\Component\Cache\Adapter\Psr16Adapter; use Symfony\Component\Cache\Psr16Cache; -use Symfony\Component\ExpressionLanguage\ExpressionLanguage; -use TheCodingMachine\GraphQLite\AggregateQueryProvider; -use TheCodingMachine\GraphQLite\AnnotationReader; use TheCodingMachine\GraphQLite\Containers\BasicAutoWiringContainer; use TheCodingMachine\GraphQLite\Containers\EmptyContainer; -use TheCodingMachine\GraphQLite\Containers\LazyContainer; use TheCodingMachine\GraphQLite\Context\Context; use TheCodingMachine\GraphQLite\Exceptions\WebonyxErrorHandler; use TheCodingMachine\GraphQLite\FieldsBuilder; @@ -30,73 +23,25 @@ use TheCodingMachine\GraphQLite\Fixtures\Integration\Models\Color; use TheCodingMachine\GraphQLite\Fixtures\Integration\Models\Position; use TheCodingMachine\GraphQLite\Fixtures\Integration\Models\Size; -use TheCodingMachine\GraphQLite\GlobControllerQueryProvider; use TheCodingMachine\GraphQLite\GraphQLRuntimeException; use TheCodingMachine\GraphQLite\InputTypeGenerator; use TheCodingMachine\GraphQLite\InputTypeUtils; use TheCodingMachine\GraphQLite\Loggers\ExceptionLogger; use TheCodingMachine\GraphQLite\Mappers\CannotMapTypeException; -use TheCodingMachine\GraphQLite\Mappers\CompositeTypeMapper; -use TheCodingMachine\GraphQLite\Mappers\GlobTypeMapper; -use TheCodingMachine\GraphQLite\Mappers\Parameters\ContainerParameterHandler; -use TheCodingMachine\GraphQLite\Mappers\Parameters\InjectUserParameterHandler; -use TheCodingMachine\GraphQLite\Mappers\Parameters\ParameterMiddlewareInterface; -use TheCodingMachine\GraphQLite\Mappers\Parameters\ParameterMiddlewarePipe; -use TheCodingMachine\GraphQLite\Mappers\Parameters\PrefetchParameterMiddleware; -use TheCodingMachine\GraphQLite\Mappers\Parameters\ResolveInfoParameterHandler; -use TheCodingMachine\GraphQLite\Mappers\PorpaginasTypeMapper; -use TheCodingMachine\GraphQLite\Mappers\RecursiveTypeMapper; -use TheCodingMachine\GraphQLite\Mappers\RecursiveTypeMapperInterface; -use TheCodingMachine\GraphQLite\Mappers\Root\BaseTypeMapper; -use TheCodingMachine\GraphQLite\Mappers\Root\CompoundTypeMapper; -use TheCodingMachine\GraphQLite\Mappers\Root\EnumTypeMapper; -use TheCodingMachine\GraphQLite\Mappers\Root\FinalRootTypeMapper; -use TheCodingMachine\GraphQLite\Mappers\Root\IteratorTypeMapper; -use TheCodingMachine\GraphQLite\Mappers\Root\LastDelegatingTypeMapper; -use TheCodingMachine\GraphQLite\Mappers\Root\MyCLabsEnumTypeMapper; -use TheCodingMachine\GraphQLite\Mappers\Root\NullableTypeMapperAdapter; -use TheCodingMachine\GraphQLite\Mappers\Root\RootTypeMapperInterface; -use TheCodingMachine\GraphQLite\Mappers\Root\VoidTypeMapper; -use TheCodingMachine\GraphQLite\Mappers\TypeMapperInterface; -use TheCodingMachine\GraphQLite\Middlewares\AuthorizationFieldMiddleware; -use TheCodingMachine\GraphQLite\Middlewares\AuthorizationInputFieldMiddleware; -use TheCodingMachine\GraphQLite\Middlewares\CostFieldMiddleware; -use TheCodingMachine\GraphQLite\Middlewares\FieldMiddlewareInterface; -use TheCodingMachine\GraphQLite\Middlewares\FieldMiddlewarePipe; -use TheCodingMachine\GraphQLite\Middlewares\InputFieldMiddlewareInterface; -use TheCodingMachine\GraphQLite\Middlewares\InputFieldMiddlewarePipe; use TheCodingMachine\GraphQLite\Middlewares\MissingAuthorizationException; use TheCodingMachine\GraphQLite\Middlewares\PrefetchFieldMiddleware; -use TheCodingMachine\GraphQLite\Middlewares\SecurityFieldMiddleware; -use TheCodingMachine\GraphQLite\Middlewares\SecurityInputFieldMiddleware; -use TheCodingMachine\GraphQLite\NamingStrategy; -use TheCodingMachine\GraphQLite\NamingStrategyInterface; -use TheCodingMachine\GraphQLite\ParameterizedCallableResolver; -use TheCodingMachine\GraphQLite\QueryProviderInterface; -use TheCodingMachine\GraphQLite\Reflection\CachedDocBlockFactory; use TheCodingMachine\GraphQLite\Schema; use TheCodingMachine\GraphQLite\SchemaFactory; use TheCodingMachine\GraphQLite\Security\AuthenticationServiceInterface; use TheCodingMachine\GraphQLite\Security\AuthorizationServiceInterface; -use TheCodingMachine\GraphQLite\Security\SecurityExpressionLanguageProvider; use TheCodingMachine\GraphQLite\Security\VoidAuthenticationService; -use TheCodingMachine\GraphQLite\Security\VoidAuthorizationService; -use TheCodingMachine\GraphQLite\TypeGenerator; use TheCodingMachine\GraphQLite\TypeMismatchRuntimeException; -use TheCodingMachine\GraphQLite\TypeRegistry; -use TheCodingMachine\GraphQLite\Types\ArgumentResolver; -use TheCodingMachine\GraphQLite\Types\TypeResolver; use TheCodingMachine\GraphQLite\Utils\AccessPropertyException; -use TheCodingMachine\GraphQLite\Utils\Namespaces\NamespaceFactory; -use UnitEnum; - use function array_filter; use function assert; use function count; use function in_array; -use function interface_exists; use function json_encode; - use const JSON_PRETTY_PRINT; class EndToEndTest extends IntegrationTestCase @@ -687,7 +632,7 @@ public function testEndToEndStaticFactories(): void 'echoFilters' => ['foo', 'bar', '12', '42', '62'], ], $this->getSuccessResult($result)); - // Call again to test GlobTypeMapper cache + // Call again to test ClassFinderTypeMapper cache $result = GraphQL::executeQuery( $schema, $queryString, @@ -1548,8 +1493,7 @@ public function testInputOutputNameConflict(): void $arrayAdapter = new ArrayAdapter(); $arrayAdapter->setLogger(new ExceptionLogger()); $schemaFactory = new SchemaFactory(new Psr16Cache($arrayAdapter), new BasicAutoWiringContainer(new EmptyContainer())); - $schemaFactory->addControllerNamespace('TheCodingMachine\\GraphQLite\\Fixtures\\InputOutputNameConflict\\Controllers'); - $schemaFactory->addTypeNamespace('TheCodingMachine\\GraphQLite\\Fixtures\\InputOutputNameConflict\\Types'); + $schemaFactory->addNamespace('TheCodingMachine\\GraphQLite\\Fixtures\\InputOutputNameConflict'); $schema = $schemaFactory->createSchema(); @@ -1872,9 +1816,7 @@ public function testEndToEndInputTypeValidation(): void $arrayAdapter = new ArrayAdapter(); $arrayAdapter->setLogger(new ExceptionLogger()); $schemaFactory = new SchemaFactory(new Psr16Cache($arrayAdapter), new BasicAutoWiringContainer(new EmptyContainer())); - $schemaFactory->addControllerNamespace('TheCodingMachine\\GraphQLite\\Fixtures\\Integration\\Controllers'); - $schemaFactory->addTypeNamespace('TheCodingMachine\\GraphQLite\\Fixtures\\Integration\\Models'); - $schemaFactory->addTypeNamespace('TheCodingMachine\\GraphQLite\\Fixtures\\Integration\\Types'); + $schemaFactory->addNamespace('TheCodingMachine\\GraphQLite\\Fixtures\\Integration'); $schemaFactory->setAuthenticationService($container->get(AuthenticationServiceInterface::class)); $schemaFactory->setAuthorizationService($container->get(AuthorizationServiceInterface::class)); $schemaFactory->setInputTypeValidator($validator); @@ -2250,8 +2192,7 @@ public function testCircularInput(): void $arrayAdapter = new ArrayAdapter(); $arrayAdapter->setLogger(new ExceptionLogger()); $schemaFactory = new SchemaFactory(new Psr16Cache($arrayAdapter), new BasicAutoWiringContainer(new EmptyContainer())); - $schemaFactory->addControllerNamespace('TheCodingMachine\\GraphQLite\\Fixtures\\CircularInputReference\\Controllers'); - $schemaFactory->addTypeNamespace('TheCodingMachine\\GraphQLite\\Fixtures\\CircularInputReference\\Types'); + $schemaFactory->addNamespace('TheCodingMachine\\GraphQLite\\Fixtures\\CircularInputReference'); $schema = $schemaFactory->createSchema(); diff --git a/tests/Integration/IntegrationTestCase.php b/tests/Integration/IntegrationTestCase.php index 06db73cce9..200247f56e 100644 --- a/tests/Integration/IntegrationTestCase.php +++ b/tests/Integration/IntegrationTestCase.php @@ -5,7 +5,6 @@ use GraphQL\Error\DebugFlag; use GraphQL\Executor\ExecutionResult; use Kcs\ClassFinder\Finder\ComposerFinder; -use Kcs\ClassFinder\Finder\FinderInterface; use PHPUnit\Framework\TestCase; use Psr\Container\ContainerInterface; use stdClass; @@ -15,16 +14,23 @@ use Symfony\Component\ExpressionLanguage\ExpressionLanguage; use TheCodingMachine\GraphQLite\AggregateQueryProvider; use TheCodingMachine\GraphQLite\AnnotationReader; +use TheCodingMachine\GraphQLite\Cache\ClassBoundCache; +use TheCodingMachine\GraphQLite\Cache\FilesSnapshot; +use TheCodingMachine\GraphQLite\Cache\SnapshotClassBoundCache; use TheCodingMachine\GraphQLite\Containers\BasicAutoWiringContainer; use TheCodingMachine\GraphQLite\Containers\EmptyContainer; use TheCodingMachine\GraphQLite\Containers\LazyContainer; +use TheCodingMachine\GraphQLite\Discovery\Cache\ClassFinderComputedCache; +use TheCodingMachine\GraphQLite\Discovery\Cache\HardClassFinderComputedCache; +use TheCodingMachine\GraphQLite\Discovery\ClassFinder; +use TheCodingMachine\GraphQLite\Discovery\KcsClassFinder; use TheCodingMachine\GraphQLite\FieldsBuilder; use TheCodingMachine\GraphQLite\GlobControllerQueryProvider; use TheCodingMachine\GraphQLite\InputTypeGenerator; use TheCodingMachine\GraphQLite\InputTypeUtils; use TheCodingMachine\GraphQLite\Loggers\ExceptionLogger; +use TheCodingMachine\GraphQLite\Mappers\ClassFinderTypeMapper; use TheCodingMachine\GraphQLite\Mappers\CompositeTypeMapper; -use TheCodingMachine\GraphQLite\Mappers\GlobTypeMapper; use TheCodingMachine\GraphQLite\Mappers\Parameters\ContainerParameterHandler; use TheCodingMachine\GraphQLite\Mappers\Parameters\InjectUserParameterHandler; use TheCodingMachine\GraphQLite\Mappers\Parameters\ParameterMiddlewareInterface; @@ -58,7 +64,9 @@ use TheCodingMachine\GraphQLite\NamingStrategyInterface; use TheCodingMachine\GraphQLite\ParameterizedCallableResolver; use TheCodingMachine\GraphQLite\QueryProviderInterface; -use TheCodingMachine\GraphQLite\Reflection\CachedDocBlockFactory; +use TheCodingMachine\GraphQLite\Reflection\DocBlock\CachedDocBlockFactory; +use TheCodingMachine\GraphQLite\Reflection\DocBlock\DocBlockFactory; +use TheCodingMachine\GraphQLite\Reflection\DocBlock\PhpDocumentorDocBlockFactory; use TheCodingMachine\GraphQLite\Schema; use TheCodingMachine\GraphQLite\Security\AuthenticationServiceInterface; use TheCodingMachine\GraphQLite\Security\AuthorizationServiceInterface; @@ -69,8 +77,6 @@ use TheCodingMachine\GraphQLite\TypeRegistry; use TheCodingMachine\GraphQLite\Types\ArgumentResolver; use TheCodingMachine\GraphQLite\Types\TypeResolver; -use TheCodingMachine\GraphQLite\Utils\Namespaces\NamespaceFactory; -use UnitEnum; class IntegrationTestCase extends TestCase { @@ -88,26 +94,36 @@ public function createContainer(array $overloadedServices = []): ContainerInterf Schema::class => static function (ContainerInterface $container) { return new Schema($container->get(QueryProviderInterface::class), $container->get(RecursiveTypeMapperInterface::class), $container->get(TypeResolver::class), $container->get(RootTypeMapperInterface::class)); }, - FinderInterface::class => fn () => new ComposerFinder(), + ClassFinder::class => function () { + $composerFinder = new ComposerFinder(); + $composerFinder->inNamespace('TheCodingMachine\\GraphQLite\\Fixtures\\Integration\\Types'); + $composerFinder->inNamespace('TheCodingMachine\\GraphQLite\\Fixtures\\Integration\\Models'); + $composerFinder->inNamespace('TheCodingMachine\\GraphQLite\\Fixtures\\Integration\\Controllers'); + + return new KcsClassFinder($composerFinder); + }, + ClassFinderComputedCache::class => function () { + return new HardClassFinderComputedCache( + new Psr16Cache(new ArrayAdapter()), + ); + }, QueryProviderInterface::class => static function (ContainerInterface $container) { $queryProvider = new GlobControllerQueryProvider( - 'TheCodingMachine\\GraphQLite\\Fixtures\\Integration\\Controllers', $container->get(FieldsBuilder::class), $container->get(BasicAutoWiringContainer::class), $container->get(AnnotationReader::class), - new Psr16Cache(new ArrayAdapter()), - $container->get(FinderInterface::class), + $container->get(ClassFinder::class), + $container->get(ClassFinderComputedCache::class), ); $queryProvider = new AggregateQueryProvider([ $queryProvider, new GlobControllerQueryProvider( - 'TheCodingMachine\\GraphQLite\\Fixtures\\Integration\\Controllers', $container->get(FieldsBuilder::class), $container->get(BasicAutoWiringContainer::class), $container->get(AnnotationReader::class), - new Psr16Cache(new ArrayAdapter()), - $container->get(FinderInterface::class), + $container->get(ClassFinder::class), + $container->get(ClassFinderComputedCache::class), ), ]); @@ -120,7 +136,7 @@ public function createContainer(array $overloadedServices = []): ContainerInterf $container->get(RecursiveTypeMapperInterface::class), $container->get(ArgumentResolver::class), $container->get(TypeResolver::class), - $container->get(CachedDocBlockFactory::class), + $container->get(DocBlockFactory::class), $container->get(NamingStrategyInterface::class), $container->get(RootTypeMapperInterface::class), $parameterMiddlewarePipe, @@ -202,19 +218,11 @@ public function createContainer(array $overloadedServices = []): ContainerInterf TypeMapperInterface::class => static function (ContainerInterface $container) { return new CompositeTypeMapper(); }, - NamespaceFactory::class => static function (ContainerInterface $container) { - $arrayAdapter = new ArrayAdapter(); - $arrayAdapter->setLogger(new ExceptionLogger()); - return new NamespaceFactory( - new Psr16Cache($arrayAdapter), - $container->get(FinderInterface::class), - ); - }, - GlobTypeMapper::class => static function (ContainerInterface $container) { + ClassFinderTypeMapper::class => static function (ContainerInterface $container) { $arrayAdapter = new ArrayAdapter(); $arrayAdapter->setLogger(new ExceptionLogger()); - return new GlobTypeMapper( - $container->get(NamespaceFactory::class)->createNamespace('TheCodingMachine\\GraphQLite\\Fixtures\\Integration\\Types'), + return new ClassFinderTypeMapper( + $container->get(ClassFinder::class), $container->get(TypeGenerator::class), $container->get(InputTypeGenerator::class), $container->get(InputTypeUtils::class), @@ -222,23 +230,7 @@ public function createContainer(array $overloadedServices = []): ContainerInterf $container->get(AnnotationReader::class), $container->get(NamingStrategyInterface::class), $container->get(RecursiveTypeMapperInterface::class), - new Psr16Cache($arrayAdapter), - ); - }, - // We use a second type mapper here so we can target the Models dir - GlobTypeMapper::class . '2' => static function (ContainerInterface $container) { - $arrayAdapter = new ArrayAdapter(); - $arrayAdapter->setLogger(new ExceptionLogger()); - return new GlobTypeMapper( - $container->get(NamespaceFactory::class)->createNamespace('TheCodingMachine\\GraphQLite\\Fixtures\\Integration\\Models'), - $container->get(TypeGenerator::class), - $container->get(InputTypeGenerator::class), - $container->get(InputTypeUtils::class), - $container->get(BasicAutoWiringContainer::class), - $container->get(AnnotationReader::class), - $container->get(NamingStrategyInterface::class), - $container->get(RecursiveTypeMapperInterface::class), - new Psr16Cache($arrayAdapter), + $container->get(ClassFinderComputedCache::class), ); }, PorpaginasTypeMapper::class => static function (ContainerInterface $container) { @@ -248,11 +240,8 @@ public function createContainer(array $overloadedServices = []): ContainerInterf return new EnumTypeMapper( $container->get(RootTypeMapperInterface::class), $container->get(AnnotationReader::class), - new ArrayAdapter(), - [ - $container->get(NamespaceFactory::class) - ->createNamespace('TheCodingMachine\\GraphQLite\\Fixtures\\Integration\\Models'), - ], + $container->get(ClassFinder::class), + $container->get(ClassFinderComputedCache::class), ); }, TypeGenerator::class => static function (ContainerInterface $container) { @@ -286,10 +275,18 @@ public function createContainer(array $overloadedServices = []): ContainerInterf NamingStrategyInterface::class => static function () { return new NamingStrategy(); }, - CachedDocBlockFactory::class => static function () { + ClassBoundCache::class => static function () { $arrayAdapter = new ArrayAdapter(); $arrayAdapter->setLogger(new ExceptionLogger()); - return new CachedDocBlockFactory(new Psr16Cache($arrayAdapter)); + $psr16Cache = new Psr16Cache($arrayAdapter); + + return new SnapshotClassBoundCache($psr16Cache, FilesSnapshot::alwaysUnchanged(...)); + }, + DocBlockFactory::class => static function (ContainerInterface $container) { + return new CachedDocBlockFactory( + $container->get(ClassBoundCache::class), + PhpDocumentorDocBlockFactory::default(), + ); }, RootTypeMapperInterface::class => static function (ContainerInterface $container) { return new VoidTypeMapper( @@ -305,8 +302,8 @@ public function createContainer(array $overloadedServices = []): ContainerInterf // These are in reverse order of execution $errorRootTypeMapper = new FinalRootTypeMapper($container->get(RecursiveTypeMapperInterface::class)); $rootTypeMapper = new BaseTypeMapper($errorRootTypeMapper, $container->get(RecursiveTypeMapperInterface::class), $container->get(RootTypeMapperInterface::class)); - $rootTypeMapper = new MyCLabsEnumTypeMapper($rootTypeMapper, $container->get(AnnotationReader::class), new ArrayAdapter(), [ $container->get(NamespaceFactory::class)->createNamespace('TheCodingMachine\\GraphQLite\\Fixtures\\Integration\\Models') ]); - $rootTypeMapper = new EnumTypeMapper($rootTypeMapper, $container->get(AnnotationReader::class), new ArrayAdapter(), [ $container->get(NamespaceFactory::class)->createNamespace('TheCodingMachine\\GraphQLite\\Fixtures\\Integration\\Models') ]); + $rootTypeMapper = new MyCLabsEnumTypeMapper($rootTypeMapper, $container->get(AnnotationReader::class), $container->get(ClassFinder::class), $container->get(ClassFinderComputedCache::class)); + $rootTypeMapper = new EnumTypeMapper($rootTypeMapper, $container->get(AnnotationReader::class), $container->get(DocBlockFactory::class), $container->get(ClassFinder::class), $container->get(ClassFinderComputedCache::class)); $rootTypeMapper = new CompoundTypeMapper($rootTypeMapper, $container->get(RootTypeMapperInterface::class), $container->get(NamingStrategyInterface::class), $container->get(TypeRegistry::class), $container->get(RecursiveTypeMapperInterface::class)); $rootTypeMapper = new IteratorTypeMapper($rootTypeMapper, $container->get(RootTypeMapperInterface::class)); return $rootTypeMapper; @@ -336,8 +333,7 @@ public function createContainer(array $overloadedServices = []): ContainerInterf $container = new LazyContainer($overloadedServices + $services); $container->get(TypeResolver::class)->registerSchema($container->get(Schema::class)); - $container->get(TypeMapperInterface::class)->addTypeMapper($container->get(GlobTypeMapper::class)); - $container->get(TypeMapperInterface::class)->addTypeMapper($container->get(GlobTypeMapper::class . '2')); + $container->get(TypeMapperInterface::class)->addTypeMapper($container->get(ClassFinderTypeMapper::class)); $container->get(TypeMapperInterface::class)->addTypeMapper($container->get(PorpaginasTypeMapper::class)); $container->get('topRootTypeMapper')->setNext($container->get('rootTypeMapper')); diff --git a/tests/Mappers/GlobTypeMapperTest.php b/tests/Mappers/ClassFinderTypeMapperTest.php similarity index 65% rename from tests/Mappers/GlobTypeMapperTest.php rename to tests/Mappers/ClassFinderTypeMapperTest.php index e7e12053ae..1657c271c0 100644 --- a/tests/Mappers/GlobTypeMapperTest.php +++ b/tests/Mappers/ClassFinderTypeMapperTest.php @@ -31,9 +31,9 @@ use TheCodingMachine\GraphQLite\NamingStrategy; use TheCodingMachine\GraphQLite\Types\MutableObjectType; -class GlobTypeMapperTest extends AbstractQueryProvider +class ClassFinderTypeMapperTest extends AbstractQueryProvider { - public function testGlobTypeMapper(): void + public function testClassFinderTypeMapper(): void { $container = new LazyContainer([ FooType::class => static function () { @@ -44,9 +44,9 @@ public function testGlobTypeMapper(): void $typeGenerator = $this->getTypeGenerator(); $inputTypeGenerator = $this->getInputTypeGenerator(); - $cache = new Psr16Cache(new ArrayAdapter()); + $classFinderComputedCache = $this->getClassFinderComputedCache(); - $mapper = new GlobTypeMapper($this->getNamespaceFactory()->createNamespace('TheCodingMachine\GraphQLite\Fixtures\Types'), $typeGenerator, $inputTypeGenerator, $this->getInputTypeUtils(), $container, new AnnotationReader(), new NamingStrategy(), $this->getTypeMapper(), $cache); + $mapper = new ClassFinderTypeMapper($this->getClassFinder('TheCodingMachine\GraphQLite\Fixtures\Types'), $typeGenerator, $inputTypeGenerator, $this->getInputTypeUtils(), $container, new AnnotationReader(), new NamingStrategy(), $this->getTypeMapper(), $classFinderComputedCache); $this->assertSame([TestObject::class], $mapper->getSupportedClasses()); $this->assertTrue($mapper->canMapClassToType(TestObject::class)); @@ -56,7 +56,7 @@ public function testGlobTypeMapper(): void $this->assertFalse($mapper->canMapNameToType('NotExists')); // Again to test cache - $anotherMapperSameCache = new GlobTypeMapper($this->getNamespaceFactory()->createNamespace('TheCodingMachine\GraphQLite\Fixtures\Types'), $typeGenerator, $this->getInputTypeGenerator(), $this->getInputTypeUtils(), $container, new AnnotationReader(), new NamingStrategy(), $this->getTypeMapper(), $cache); + $anotherMapperSameCache = new ClassFinderTypeMapper($this->getClassFinder('TheCodingMachine\GraphQLite\Fixtures\Types'), $typeGenerator, $this->getInputTypeGenerator(), $this->getInputTypeUtils(), $container, new AnnotationReader(), new NamingStrategy(), $this->getTypeMapper(), $classFinderComputedCache); $this->assertTrue($anotherMapperSameCache->canMapClassToType(TestObject::class)); $this->assertTrue($anotherMapperSameCache->canMapNameToType('Foo')); @@ -64,7 +64,7 @@ public function testGlobTypeMapper(): void $mapper->mapClassToType(stdClass::class, null); } - public function testGlobTypeMapperDuplicateTypesException(): void + public function testClassFinderTypeMapperDuplicateTypesException(): void { $container = new LazyContainer([ TestType::class => static function () { @@ -74,13 +74,13 @@ public function testGlobTypeMapperDuplicateTypesException(): void $typeGenerator = $this->getTypeGenerator(); - $mapper = new GlobTypeMapper($this->getNamespaceFactory()->createNamespace('TheCodingMachine\GraphQLite\Fixtures\DuplicateTypes'), $typeGenerator, $this->getInputTypeGenerator(), $this->getInputTypeUtils(), $container, new AnnotationReader(), new NamingStrategy(), $this->getTypeMapper(), new Psr16Cache(new NullAdapter())); + $mapper = new ClassFinderTypeMapper($this->getClassFinder('TheCodingMachine\GraphQLite\Fixtures\DuplicateTypes'), $typeGenerator, $this->getInputTypeGenerator(), $this->getInputTypeUtils(), $container, new AnnotationReader(), new NamingStrategy(), $this->getTypeMapper(), $this->getClassFinderComputedCache()); $this->expectException(DuplicateMappingException::class); $mapper->canMapClassToType(TestType::class); } - public function testGlobTypeMapperDuplicateInputsException(): void + public function testClassFinderTypeMapperDuplicateInputsException(): void { $container = new LazyContainer([ TestInput::class => static function () { @@ -90,13 +90,13 @@ public function testGlobTypeMapperDuplicateInputsException(): void $typeGenerator = $this->getTypeGenerator(); - $mapper = new GlobTypeMapper($this->getNamespaceFactory()->createNamespace('TheCodingMachine\GraphQLite\Fixtures\DuplicateInputs'), $typeGenerator, $this->getInputTypeGenerator(), $this->getInputTypeUtils(), $container, new AnnotationReader(), new NamingStrategy(), $this->getTypeMapper(), new Psr16Cache(new NullAdapter())); + $mapper = new ClassFinderTypeMapper($this->getClassFinder('TheCodingMachine\GraphQLite\Fixtures\DuplicateInputs'), $typeGenerator, $this->getInputTypeGenerator(), $this->getInputTypeUtils(), $container, new AnnotationReader(), new NamingStrategy(), $this->getTypeMapper(), $this->getClassFinderComputedCache()); $this->expectException(DuplicateMappingException::class); $mapper->canMapClassToInputType(TestInput::class); } - public function testGlobTypeMapperDuplicateInputTypesException(): void + public function testClassFinderTypeMapperDuplicateInputTypesException(): void { $container = new LazyContainer([ /*TestType::class => function() { @@ -106,7 +106,7 @@ public function testGlobTypeMapperDuplicateInputTypesException(): void $typeGenerator = $this->getTypeGenerator(); - $mapper = new GlobTypeMapper($this->getNamespaceFactory()->createNamespace('TheCodingMachine\GraphQLite\Fixtures\DuplicateInputTypes'), $typeGenerator, $this->getInputTypeGenerator(), $this->getInputTypeUtils(), $container, new AnnotationReader(), new NamingStrategy(), $this->getTypeMapper(), new Psr16Cache(new NullAdapter())); + $mapper = new ClassFinderTypeMapper($this->getClassFinder('TheCodingMachine\GraphQLite\Fixtures\DuplicateInputTypes'), $typeGenerator, $this->getInputTypeGenerator(), $this->getInputTypeUtils(), $container, new AnnotationReader(), new NamingStrategy(), $this->getTypeMapper(), $this->getClassFinderComputedCache()); $caught = false; try { @@ -125,7 +125,7 @@ public function testGlobTypeMapperDuplicateInputTypesException(): void $this->assertTrue($caught, 'DuplicateMappingException is thrown'); } - public function testGlobTypeMapperInheritedInputTypesException(): void + public function testClassFinderTypeMapperInheritedInputTypesException(): void { $container = new LazyContainer([ ChildTestFactory::class => static function () { @@ -135,7 +135,7 @@ public function testGlobTypeMapperInheritedInputTypesException(): void $typeGenerator = $this->getTypeGenerator(); - $mapper = new GlobTypeMapper($this->getNamespaceFactory()->createNamespace('TheCodingMachine\GraphQLite\Fixtures\InheritedInputTypes'), $typeGenerator, $this->getInputTypeGenerator(), $this->getInputTypeUtils(), $container, new AnnotationReader(), new NamingStrategy(), $this->getTypeMapper(), new Psr16Cache(new NullAdapter())); + $mapper = new ClassFinderTypeMapper($this->getClassFinder('TheCodingMachine\GraphQLite\Fixtures\InheritedInputTypes'), $typeGenerator, $this->getInputTypeGenerator(), $this->getInputTypeUtils(), $container, new AnnotationReader(), new NamingStrategy(), $this->getTypeMapper(), $this->getClassFinderComputedCache()); //$this->expectException(DuplicateMappingException::class); //$this->expectExceptionMessage('The class \'TheCodingMachine\GraphQLite\Fixtures\TestObject\' should be mapped to only one GraphQL Input type. Two methods are pointing via the @Factory annotation to this class: \'TheCodingMachine\GraphQLite\Fixtures\DuplicateInputTypes\TestFactory::myFactory\' and \'TheCodingMachine\GraphQLite\Fixtures\DuplicateInputTypes\TestFactory2::myFactory\''); @@ -143,7 +143,7 @@ public function testGlobTypeMapperInheritedInputTypesException(): void $mapper->mapClassToInputType(TestObject::class); } - public function testGlobTypeMapperClassNotFoundException(): void + public function testClassFinderTypeMapperClassNotFoundException(): void { $container = new LazyContainer([ TestType::class => static function () { @@ -153,14 +153,14 @@ public function testGlobTypeMapperClassNotFoundException(): void $typeGenerator = $this->getTypeGenerator(); - $mapper = new GlobTypeMapper($this->getNamespaceFactory()->createNamespace('TheCodingMachine\GraphQLite\Fixtures\BadClassType'), $typeGenerator, $this->getInputTypeGenerator(), $this->getInputTypeUtils(), $container, new AnnotationReader(), new NamingStrategy(), $this->getTypeMapper(), new Psr16Cache(new NullAdapter())); + $mapper = new ClassFinderTypeMapper($this->getClassFinder('TheCodingMachine\GraphQLite\Fixtures\BadClassType'), $typeGenerator, $this->getInputTypeGenerator(), $this->getInputTypeUtils(), $container, new AnnotationReader(), new NamingStrategy(), $this->getTypeMapper(), $this->getClassFinderComputedCache()); $this->expectException(ClassNotFoundException::class); $this->expectExceptionMessage("Could not autoload class 'Foobar' defined in #[Type] attribute of class 'TheCodingMachine\\GraphQLite\\Fixtures\\BadClassType\\TestType'"); $mapper->canMapClassToType(TestType::class); } - public function testGlobTypeMapperNameNotFoundException(): void + public function testClassFinderTypeMapperNameNotFoundException(): void { $container = new LazyContainer([ FooType::class => static function () { @@ -170,13 +170,13 @@ public function testGlobTypeMapperNameNotFoundException(): void $typeGenerator = $this->getTypeGenerator(); - $mapper = new GlobTypeMapper($this->getNamespaceFactory()->createNamespace('TheCodingMachine\GraphQLite\Fixtures\Types'), $typeGenerator, $this->getInputTypeGenerator(), $this->getInputTypeUtils(), $container, new AnnotationReader(), new NamingStrategy(), $this->getTypeMapper(), new Psr16Cache(new NullAdapter())); + $mapper = new ClassFinderTypeMapper($this->getClassFinder('TheCodingMachine\GraphQLite\Fixtures\Types'), $typeGenerator, $this->getInputTypeGenerator(), $this->getInputTypeUtils(), $container, new AnnotationReader(), new NamingStrategy(), $this->getTypeMapper(), $this->getClassFinderComputedCache()); $this->expectException(CannotMapTypeException::class); $mapper->mapNameToType('NotExists', $this->getTypeMapper()); } - public function testGlobTypeMapperInputType(): void + public function testClassFinderTypeMapperInputType(): void { $container = new LazyContainer([ FooType::class => static function () { @@ -189,9 +189,9 @@ public function testGlobTypeMapperInputType(): void $typeGenerator = $this->getTypeGenerator(); - $cache = new Psr16Cache(new ArrayAdapter()); + $classFinderComputedCache = $this->getClassFinderComputedCache(); - $mapper = new GlobTypeMapper($this->getNamespaceFactory()->createNamespace('TheCodingMachine\GraphQLite\Fixtures\Types'), $typeGenerator, $this->getInputTypeGenerator(), $this->getInputTypeUtils(), $container, new AnnotationReader(), new NamingStrategy(), $this->getTypeMapper(), $cache); + $mapper = new ClassFinderTypeMapper($this->getClassFinder('TheCodingMachine\GraphQLite\Fixtures\Types'), $typeGenerator, $this->getInputTypeGenerator(), $this->getInputTypeUtils(), $container, new AnnotationReader(), new NamingStrategy(), $this->getTypeMapper(), $classFinderComputedCache); $this->assertTrue($mapper->canMapClassToInputType(TestObject::class)); @@ -200,7 +200,7 @@ public function testGlobTypeMapperInputType(): void $this->assertSame('TestObjectInput', $inputType->name); // Again to test cache - $anotherMapperSameCache = new GlobTypeMapper($this->getNamespaceFactory()->createNamespace('TheCodingMachine\GraphQLite\Fixtures\Types'), $typeGenerator, $this->getInputTypeGenerator(), $this->getInputTypeUtils(), $container, new AnnotationReader(), new NamingStrategy(), $this->getTypeMapper(), $cache); + $anotherMapperSameCache = new ClassFinderTypeMapper($this->getClassFinder('TheCodingMachine\GraphQLite\Fixtures\Types'), $typeGenerator, $this->getInputTypeGenerator(), $this->getInputTypeUtils(), $container, new AnnotationReader(), new NamingStrategy(), $this->getTypeMapper(), $classFinderComputedCache); $this->assertTrue($anotherMapperSameCache->canMapClassToInputType(TestObject::class)); $this->assertSame('TestObjectInput', $anotherMapperSameCache->mapClassToInputType(TestObject::class, $this->getTypeMapper())->name); @@ -209,7 +209,7 @@ public function testGlobTypeMapperInputType(): void $mapper->mapClassToInputType(TestType::class, $this->getTypeMapper()); } - public function testGlobTypeMapperExtend(): void + public function testClassFinderTypeMapperExtend(): void { $container = new LazyContainer([ FooType::class => static function () { @@ -223,9 +223,9 @@ public function testGlobTypeMapperExtend(): void $typeGenerator = $this->getTypeGenerator(); $inputTypeGenerator = $this->getInputTypeGenerator(); - $cache = new Psr16Cache(new ArrayAdapter()); + $classFinderComputedCache = $this->getClassFinderComputedCache(); - $mapper = new GlobTypeMapper($this->getNamespaceFactory()->createNamespace('TheCodingMachine\GraphQLite\Fixtures\Types'), $typeGenerator, $inputTypeGenerator, $this->getInputTypeUtils(), $container, new AnnotationReader(), new NamingStrategy(), $this->getTypeMapper(), $cache); + $mapper = new ClassFinderTypeMapper($this->getClassFinder('TheCodingMachine\GraphQLite\Fixtures\Types'), $typeGenerator, $inputTypeGenerator, $this->getInputTypeUtils(), $container, new AnnotationReader(), new NamingStrategy(), $this->getTypeMapper(), $classFinderComputedCache); $type = $mapper->mapClassToType(TestObject::class, null); @@ -236,7 +236,7 @@ public function testGlobTypeMapperExtend(): void $this->assertFalse($mapper->canExtendTypeForName('NotExists', $type)); // Again to test cache - $anotherMapperSameCache = new GlobTypeMapper($this->getNamespaceFactory()->createNamespace('TheCodingMachine\GraphQLite\Fixtures\Types'), $typeGenerator, $this->getInputTypeGenerator(), $this->getInputTypeUtils(), $container, new AnnotationReader(), new NamingStrategy(), $this->getTypeMapper(), $cache); + $anotherMapperSameCache = new ClassFinderTypeMapper($this->getClassFinder('TheCodingMachine\GraphQLite\Fixtures\Types'), $typeGenerator, $this->getInputTypeGenerator(), $this->getInputTypeUtils(), $container, new AnnotationReader(), new NamingStrategy(), $this->getTypeMapper(), $classFinderComputedCache); $this->assertTrue($anotherMapperSameCache->canExtendTypeForClass(TestObject::class, $type)); $this->assertTrue($anotherMapperSameCache->canExtendTypeForName('TestObject', $type)); @@ -244,21 +244,21 @@ public function testGlobTypeMapperExtend(): void $mapper->extendTypeForClass(stdClass::class, $type); } - public function testEmptyGlobTypeMapper(): void + public function testEmptyClassFinderTypeMapper(): void { $container = new LazyContainer([]); $typeGenerator = $this->getTypeGenerator(); $inputTypeGenerator = $this->getInputTypeGenerator(); - $cache = new Psr16Cache(new ArrayAdapter()); + $classFinderComputedCache = $this->getClassFinderComputedCache(); - $mapper = new GlobTypeMapper($this->getNamespaceFactory()->createNamespace('TheCodingMachine\GraphQLite\Fixtures\Integration\Controllers'), $typeGenerator, $inputTypeGenerator, $this->getInputTypeUtils(), $container, new AnnotationReader(), new NamingStrategy(), $this->getTypeMapper(), $cache); + $mapper = new ClassFinderTypeMapper($this->getClassFinder('TheCodingMachine\GraphQLite\Fixtures\Integration\Controllers'), $typeGenerator, $inputTypeGenerator, $this->getInputTypeUtils(), $container, new AnnotationReader(), new NamingStrategy(), $this->getTypeMapper(), $classFinderComputedCache); $this->assertSame([], $mapper->getSupportedClasses()); } - public function testGlobTypeMapperDecorate(): void + public function testClassFinderTypeMapperDecorate(): void { $container = new LazyContainer([ FilterDecorator::class => static function () { @@ -269,9 +269,9 @@ public function testGlobTypeMapperDecorate(): void $typeGenerator = $this->getTypeGenerator(); $inputTypeGenerator = $this->getInputTypeGenerator(); - $cache = new Psr16Cache(new ArrayAdapter()); + $classFinderComputedCache = $this->getClassFinderComputedCache(); - $mapper = new GlobTypeMapper($this->getNamespaceFactory()->createNamespace('TheCodingMachine\GraphQLite\Fixtures\Integration\Types'), $typeGenerator, $inputTypeGenerator, $this->getInputTypeUtils(), $container, new AnnotationReader(), new NamingStrategy(), $this->getTypeMapper(), $cache); + $mapper = new ClassFinderTypeMapper($this->getClassFinder('TheCodingMachine\GraphQLite\Fixtures\Integration\Types'), $typeGenerator, $inputTypeGenerator, $this->getInputTypeUtils(), $container, new AnnotationReader(), new NamingStrategy(), $this->getTypeMapper(), $classFinderComputedCache); $inputType = new MockResolvableInputObjectType(['name' => 'FilterInput']); @@ -294,14 +294,14 @@ public function testInvalidName(): void $typeGenerator = $this->getTypeGenerator(); - $mapper = new GlobTypeMapper($this->getNamespaceFactory()->createNamespace('TheCodingMachine\GraphQLite\Fixtures\Types'), $typeGenerator, $this->getInputTypeGenerator(), $this->getInputTypeUtils(), $container, new AnnotationReader(), new NamingStrategy(), $this->getTypeMapper(), new Psr16Cache(new ArrayAdapter())); + $mapper = new ClassFinderTypeMapper($this->getClassFinder('TheCodingMachine\GraphQLite\Fixtures\Types'), $typeGenerator, $this->getInputTypeGenerator(), $this->getInputTypeUtils(), $container, new AnnotationReader(), new NamingStrategy(), $this->getTypeMapper(), $this->getClassFinderComputedCache()); $this->assertFalse($mapper->canExtendTypeForName('{}()/\\@:', new MutableObjectType(['name' => 'foo']))); $this->assertFalse($mapper->canDecorateInputTypeForName('{}()/\\@:', new MockResolvableInputObjectType(['name' => 'foo']))); $this->assertFalse($mapper->canMapNameToType('{}()/\\@:')); } - public function testGlobTypeMapperExtendBadName(): void + public function testClassFinderTypeMapperExtendBadName(): void { $container = new LazyContainer([ FooType::class => static function () { @@ -318,9 +318,9 @@ public function testGlobTypeMapperExtendBadName(): void $typeGenerator = $this->getTypeGenerator(); $inputTypeGenerator = $this->getInputTypeGenerator(); - $cache = new Psr16Cache(new ArrayAdapter()); + $classFinderComputedCache = $this->getClassFinderComputedCache(); - $mapper = new GlobTypeMapper($this->getNamespaceFactory()->createNamespace('TheCodingMachine\GraphQLite\Fixtures\BadExtendType'), $typeGenerator, $inputTypeGenerator, $this->getInputTypeUtils(), $container, new AnnotationReader(), new NamingStrategy(), $this->getTypeMapper(), $cache); + $mapper = new ClassFinderTypeMapper($this->getClassFinder('TheCodingMachine\GraphQLite\Fixtures\BadExtendType'), $typeGenerator, $inputTypeGenerator, $this->getInputTypeUtils(), $container, new AnnotationReader(), new NamingStrategy(), $this->getTypeMapper(), $classFinderComputedCache); $testObjectType = new MutableObjectType([ 'name' => 'TestObject', @@ -334,7 +334,7 @@ public function testGlobTypeMapperExtendBadName(): void $mapper->extendTypeForName('TestObject', $testObjectType); } - public function testGlobTypeMapperExtendBadClass(): void + public function testClassFinderTypeMapperExtendBadClass(): void { $container = new LazyContainer([ FooType::class => static function () { @@ -351,9 +351,9 @@ public function testGlobTypeMapperExtendBadClass(): void $typeGenerator = $this->getTypeGenerator(); $inputTypeGenerator = $this->getInputTypeGenerator(); - $cache = new Psr16Cache(new ArrayAdapter()); + $classFinderComputedCache = $this->getClassFinderComputedCache(); - $mapper = new GlobTypeMapper($this->getNamespaceFactory()->createNamespace('TheCodingMachine\GraphQLite\Fixtures\BadExtendType2'), $typeGenerator, $inputTypeGenerator, $this->getInputTypeUtils(), $container, new AnnotationReader(), new NamingStrategy(), $this->getTypeMapper(), $cache); + $mapper = new ClassFinderTypeMapper($this->getClassFinder('TheCodingMachine\GraphQLite\Fixtures\BadExtendType2'), $typeGenerator, $inputTypeGenerator, $this->getInputTypeUtils(), $container, new AnnotationReader(), new NamingStrategy(), $this->getTypeMapper(), $classFinderComputedCache); $testObjectType = new MutableObjectType([ 'name' => 'TestObject', @@ -378,9 +378,9 @@ public function testNonInstantiableType(): void $typeGenerator = $this->getTypeGenerator(); $inputTypeGenerator = $this->getInputTypeGenerator(); - $cache = new Psr16Cache(new ArrayAdapter()); + $classFinderComputedCache = $this->getClassFinderComputedCache(); - $mapper = new GlobTypeMapper($this->getNamespaceFactory()->createNamespace('TheCodingMachine\GraphQLite\Fixtures\NonInstantiableType'), $typeGenerator, $inputTypeGenerator, $this->getInputTypeUtils(), $container, new AnnotationReader(), new NamingStrategy(), $this->getTypeMapper(), $cache); + $mapper = new ClassFinderTypeMapper($this->getClassFinder('TheCodingMachine\GraphQLite\Fixtures\NonInstantiableType'), $typeGenerator, $inputTypeGenerator, $this->getInputTypeUtils(), $container, new AnnotationReader(), new NamingStrategy(), $this->getTypeMapper(), $classFinderComputedCache); $this->expectException(GraphQLRuntimeException::class); $this->expectExceptionMessage('Class "TheCodingMachine\GraphQLite\Fixtures\NonInstantiableType\AbstractFooType" annotated with @Type(class="TheCodingMachine\GraphQLite\Fixtures\TestObject") must be instantiable.'); @@ -394,8 +394,8 @@ public function testNonInstantiableInput(): void $typeGenerator = $this->getTypeGenerator(); $inputTypeGenerator = $this->getInputTypeGenerator(); - $cache = new Psr16Cache(new ArrayAdapter()); - $mapper = new GlobTypeMapper($this->getNamespaceFactory()->createNamespace('TheCodingMachine\GraphQLite\Fixtures\NonInstantiableInput'), $typeGenerator, $inputTypeGenerator, $this->getInputTypeUtils(), $container, new AnnotationReader(), new NamingStrategy(), $this->getTypeMapper(), $cache); + $classFinderComputedCache = $this->getClassFinderComputedCache(); + $mapper = new ClassFinderTypeMapper($this->getClassFinder('TheCodingMachine\GraphQLite\Fixtures\NonInstantiableInput'), $typeGenerator, $inputTypeGenerator, $this->getInputTypeUtils(), $container, new AnnotationReader(), new NamingStrategy(), $this->getTypeMapper(), $classFinderComputedCache); $this->expectException(FailedResolvingInputType::class); $this->expectExceptionMessage("Class 'TheCodingMachine\GraphQLite\Fixtures\NonInstantiableInput\AbstractFoo' annotated with @Input must be instantiable."); diff --git a/tests/Mappers/Parameters/TypeMapperTest.php b/tests/Mappers/Parameters/TypeMapperTest.php index 825cdaacb0..b0c6c09ea3 100644 --- a/tests/Mappers/Parameters/TypeMapperTest.php +++ b/tests/Mappers/Parameters/TypeMapperTest.php @@ -16,7 +16,7 @@ use TheCodingMachine\GraphQLite\Mappers\CannotMapTypeException; use TheCodingMachine\GraphQLite\Parameters\DefaultValueParameter; use TheCodingMachine\GraphQLite\Parameters\InputTypeParameter; -use TheCodingMachine\GraphQLite\Reflection\CachedDocBlockFactory; +use TheCodingMachine\GraphQLite\Reflection\DocBlock\CachedDocBlockFactory; use function assert; use function count; @@ -25,17 +25,17 @@ class TypeMapperTest extends AbstractQueryProvider { public function testMapScalarUnionException(): void { + $docBlockFactory = $this->getDocBlockFactory(); + $typeMapper = new TypeHandler( $this->getArgumentResolver(), $this->getRootTypeMapper(), $this->getTypeResolver(), - $this->getCachedDocBlockFactory(), + $docBlockFactory, ); - $cachedDocBlockFactory = new CachedDocBlockFactory(new Psr16Cache(new ArrayAdapter())); - $refMethod = new ReflectionMethod($this, 'dummy'); - $docBlockObj = $cachedDocBlockFactory->getDocBlock($refMethod); + $docBlockObj = $docBlockFactory->create($refMethod); $this->expectException(CannotMapTypeException::class); $this->expectExceptionMessage('For return type of TheCodingMachine\GraphQLite\Mappers\Parameters\TypeMapperTest::dummy, in GraphQL, you can only use union types between objects. These types cannot be used in union types: String!, Int!'); @@ -44,17 +44,17 @@ public function testMapScalarUnionException(): void public function testMapObjectUnionWorks(): void { - $cachedDocBlockFactory = $this->getCachedDocBlockFactory(); + $docBlockFactory = $this->getDocBlockFactory(); $typeMapper = new TypeHandler( $this->getArgumentResolver(), $this->getRootTypeMapper(), $this->getTypeResolver(), - $cachedDocBlockFactory, + $docBlockFactory, ); $refMethod = new ReflectionMethod(UnionOutputType::class, 'objectUnion'); - $docBlockObj = $cachedDocBlockFactory->getDocBlock($refMethod); + $docBlockObj = $docBlockFactory->create($refMethod); $gqType = $typeMapper->mapReturnType($refMethod, $docBlockObj); $this->assertInstanceOf(NonNull::class, $gqType); @@ -69,17 +69,17 @@ public function testMapObjectUnionWorks(): void public function testMapObjectNullableUnionWorks(): void { - $cachedDocBlockFactory = $this->getCachedDocBlockFactory(); + $docBlockFactory = $this->getDocBlockFactory(); $typeMapper = new TypeHandler( $this->getArgumentResolver(), $this->getRootTypeMapper(), $this->getTypeResolver(), - $cachedDocBlockFactory, + $docBlockFactory, ); $refMethod = new ReflectionMethod(UnionOutputType::class, 'nullableObjectUnion'); - $docBlockObj = $cachedDocBlockFactory->getDocBlock($refMethod); + $docBlockObj = $docBlockFactory->create($refMethod); $gqType = $typeMapper->mapReturnType($refMethod, $docBlockObj); $this->assertNotInstanceOf(NonNull::class, $gqType); @@ -94,18 +94,18 @@ public function testMapObjectNullableUnionWorks(): void public function testHideParameter(): void { - $cachedDocBlockFactory = $this->getCachedDocBlockFactory(); + $docBlockFactory = $this->getDocBlockFactory(); $typeMapper = new TypeHandler( $this->getArgumentResolver(), $this->getRootTypeMapper(), $this->getTypeResolver(), - $cachedDocBlockFactory, + $docBlockFactory, ); $refMethod = new ReflectionMethod($this, 'withDefaultValue'); $refParameter = $refMethod->getParameters()[0]; - $docBlockObj = $cachedDocBlockFactory->getDocBlock($refMethod); + $docBlockObj = $docBlockFactory->create($refMethod); $annotations = $this->getAnnotationReader()->getParameterAnnotationsPerParameter([$refParameter])['foo']; $param = $typeMapper->mapParameter($refParameter, $docBlockObj, null, $annotations); @@ -118,17 +118,17 @@ public function testHideParameter(): void public function testParameterWithDescription(): void { - $cachedDocBlockFactory = $this->getCachedDocBlockFactory(); + $docBlockFactory = $this->getDocBlockFactory(); $typeMapper = new TypeHandler( $this->getArgumentResolver(), $this->getRootTypeMapper(), $this->getTypeResolver(), - $cachedDocBlockFactory, + $docBlockFactory, ); $refMethod = new ReflectionMethod($this, 'withParamDescription'); - $docBlockObj = $cachedDocBlockFactory->getDocBlock($refMethod); + $docBlockObj = $docBlockFactory->create($refMethod); $refParameter = $refMethod->getParameters()[0]; $parameter = $typeMapper->mapParameter($refParameter, $docBlockObj, null, $this->getAnnotationReader()->getParameterAnnotationsPerParameter([$refParameter])['foo']); @@ -139,18 +139,18 @@ public function testParameterWithDescription(): void public function testHideParameterException(): void { - $cachedDocBlockFactory = $this->getCachedDocBlockFactory(); + $docBlockFactory = $this->getDocBlockFactory(); $typeMapper = new TypeHandler( $this->getArgumentResolver(), $this->getRootTypeMapper(), $this->getTypeResolver(), - $cachedDocBlockFactory, + $docBlockFactory, ); $refMethod = new ReflectionMethod($this, 'withoutDefaultValue'); $refParameter = $refMethod->getParameters()[0]; - $docBlockObj = $cachedDocBlockFactory->getDocBlock($refMethod); + $docBlockObj = $docBlockFactory->create($refMethod); $annotations = $this->getAnnotationReader()->getParameterAnnotationsPerParameter([$refParameter])['foo']; $this->expectException(CannotHideParameterRuntimeException::class); diff --git a/tests/Mappers/RecursiveTypeMapperTest.php b/tests/Mappers/RecursiveTypeMapperTest.php index 8fc14ee27c..a99b70fce9 100644 --- a/tests/Mappers/RecursiveTypeMapperTest.php +++ b/tests/Mappers/RecursiveTypeMapperTest.php @@ -133,7 +133,7 @@ protected function getTypeMapper(): RecursiveTypeMapper $typeGenerator = new TypeGenerator($this->getAnnotationReader(), $namingStrategy, $this->getTypeRegistry(), $this->getRegistry(), $this->typeMapper, $this->getFieldsBuilder()); - $mapper = new GlobTypeMapper($this->getNamespaceFactory()->createNamespace('TheCodingMachine\GraphQLite\Fixtures\Interfaces\Types'), $typeGenerator, $this->getInputTypeGenerator(), $this->getInputTypeUtils(), $container, new \TheCodingMachine\GraphQLite\AnnotationReader(), $namingStrategy, $this->typeMapper, new Psr16Cache(new NullAdapter())); + $mapper = new ClassFinderTypeMapper($this->getClassFinder('TheCodingMachine\GraphQLite\Fixtures\Interfaces\Types'), $typeGenerator, $this->getInputTypeGenerator(), $this->getInputTypeUtils(), $container, new \TheCodingMachine\GraphQLite\AnnotationReader(), $namingStrategy, $this->typeMapper, $this->getClassFinderComputedCache()); $compositeMapper->addTypeMapper($mapper); } return $this->typeMapper; @@ -230,9 +230,7 @@ public function testMapNameToTypeDecorators(): void $typeGenerator = $this->getTypeGenerator(); $inputTypeGenerator = $this->getInputTypeGenerator(); - $cache = new Psr16Cache(new ArrayAdapter()); - - $mapper = new GlobTypeMapper($this->getNamespaceFactory()->createNamespace('TheCodingMachine\GraphQLite\Fixtures\Integration'), $typeGenerator, $inputTypeGenerator, $this->getInputTypeUtils(), $this->getRegistry(), new \TheCodingMachine\GraphQLite\AnnotationReader(), new NamingStrategy(), $this->getTypeMapper(), $cache); + $mapper = new ClassFinderTypeMapper($this->getClassFinder('TheCodingMachine\GraphQLite\Fixtures\Integration'), $typeGenerator, $inputTypeGenerator, $this->getInputTypeUtils(), $this->getRegistry(), new \TheCodingMachine\GraphQLite\AnnotationReader(), new NamingStrategy(), $this->getTypeMapper(), $this->getClassFinderComputedCache()); $recursiveTypeMapper = new RecursiveTypeMapper( $mapper, diff --git a/tests/Mappers/Root/MyCLabsEnumTypeMapperTest.php b/tests/Mappers/Root/MyCLabsEnumTypeMapperTest.php index ecd711c945..8663d4f8a9 100644 --- a/tests/Mappers/Root/MyCLabsEnumTypeMapperTest.php +++ b/tests/Mappers/Root/MyCLabsEnumTypeMapperTest.php @@ -15,7 +15,7 @@ class MyCLabsEnumTypeMapperTest extends AbstractQueryProvider { public function testObjectTypeHint(): void { - $mapper = new MyCLabsEnumTypeMapper(new FinalRootTypeMapper($this->getTypeMapper()), $this->getAnnotationReader(), new ArrayAdapter(), []); + $mapper = new MyCLabsEnumTypeMapper(new FinalRootTypeMapper($this->getTypeMapper()), $this->getAnnotationReader(), $this->getClassFinder([]), $this->getClassFinderComputedCache()); $this->expectException(CannotMapTypeException::class); $this->expectExceptionMessage("don't know how to handle type object"); diff --git a/tests/Mappers/StaticClassListTypeMapperTest.php b/tests/Mappers/StaticClassListTypeMapperTest.php deleted file mode 100644 index c77beb3929..0000000000 --- a/tests/Mappers/StaticClassListTypeMapperTest.php +++ /dev/null @@ -1,31 +0,0 @@ -getTypeGenerator(); - $inputTypeGenerator = $this->getInputTypeGenerator(); - - $cache = new Psr16Cache(new ArrayAdapter()); - - $mapper = new StaticClassListTypeMapper(['NotExistsClass'], $typeGenerator, $inputTypeGenerator, $this->getInputTypeUtils(), $container, new \TheCodingMachine\GraphQLite\AnnotationReader(), new NamingStrategy(), $this->getTypeMapper(), $cache); - - $this->expectException(GraphQLRuntimeException::class); - $this->expectExceptionMessage('Could not find class "NotExistsClass"'); - - $mapper->getSupportedClasses(); - } -} diff --git a/tests/Mappers/StaticTypeMapperTest.php b/tests/Mappers/StaticTypeMapperTest.php index 7918181a2b..bbecb195bb 100644 --- a/tests/Mappers/StaticTypeMapperTest.php +++ b/tests/Mappers/StaticTypeMapperTest.php @@ -137,7 +137,7 @@ public function testEndToEnd(): void $arrayAdapter = new ArrayAdapter(); $arrayAdapter->setLogger(new ExceptionLogger()); $schemaFactory = new SchemaFactory(new Psr16Cache($arrayAdapter), new BasicAutoWiringContainer(new EmptyContainer())); - $schemaFactory->addControllerNamespace('TheCodingMachine\\GraphQLite\\Fixtures\\StaticTypeMapper\\Controllers'); + $schemaFactory->addNamespace('TheCodingMachine\\GraphQLite\\Fixtures\\StaticTypeMapper\\Controllers'); // Let's register a type that maps by default to the "MyClass" PHP class $staticTypeMapper = new StaticTypeMapper( diff --git a/tests/Reflection/CachedDocBlockFactoryTest.php b/tests/Reflection/CachedDocBlockFactoryTest.php deleted file mode 100644 index 1f3a5fbed8..0000000000 --- a/tests/Reflection/CachedDocBlockFactoryTest.php +++ /dev/null @@ -1,48 +0,0 @@ -getDocBlock($refMethod); - $this->assertSame('Fetches a DocBlock object from a ReflectionMethod', $docBlock->getSummary()); - $docBlock2 = $cachedDocBlockFactory->getDocBlock($refMethod); - $this->assertSame($docBlock2, $docBlock); - - $newCachedDocBlockFactory = new CachedDocBlockFactory($arrayCache); - $docBlock3 = $newCachedDocBlockFactory->getDocBlock($refMethod); - $this->assertEquals($docBlock3, $docBlock); - } - - public function testGetContext(): void - { - $arrayCache = new Psr16Cache(new ArrayAdapter()); - $cachedDocBlockFactory = new CachedDocBlockFactory($arrayCache); - - $refClass = new ReflectionClass(CachedDocBlockFactory::class); - - $context = $cachedDocBlockFactory->getContextFromClass($refClass); - - $context2 = $cachedDocBlockFactory->getContextFromClass($refClass); - $this->assertSame($context2, $context); - - $newCachedDocBlockFactory = new CachedDocBlockFactory($arrayCache); - $context3 = $newCachedDocBlockFactory->getContextFromClass($refClass); - $this->assertEquals($context3, $context); - } -} diff --git a/tests/Reflection/DocBlock/CachedDocBlockFactoryTest.php b/tests/Reflection/DocBlock/CachedDocBlockFactoryTest.php new file mode 100644 index 0000000000..f1b13ae077 --- /dev/null +++ b/tests/Reflection/DocBlock/CachedDocBlockFactoryTest.php @@ -0,0 +1,62 @@ +create($refMethod); + $this->assertSame('Fetches a DocBlock object from a ReflectionMethod', $docBlock->getSummary()); + $docBlock2 = $cachedDocBlockFactory->create($refMethod); + $this->assertSame($docBlock2, $docBlock); + + $newCachedDocBlockFactory = new CachedDocBlockFactory( + new SnapshotClassBoundCache($arrayCache, FilesSnapshot::alwaysUnchanged(...)), + PhpDocumentorDocBlockFactory::default(), + ); + $docBlock3 = $newCachedDocBlockFactory->create($refMethod); + $this->assertEquals($docBlock3, $docBlock); + } + + public function testCreatesContext(): void + { + $arrayCache = new Psr16Cache(new ArrayAdapter(storeSerialized: false)); + $cachedDocBlockFactory = new CachedDocBlockFactory( + new SnapshotClassBoundCache($arrayCache, FilesSnapshot::alwaysUnchanged(...)), + PhpDocumentorDocBlockFactory::default(), + ); + + $refMethod = new ReflectionMethod(DocBlockFactory::class, 'create'); + + $docBlock = $cachedDocBlockFactory->createContext($refMethod); + $this->assertSame('TheCodingMachine\GraphQLite\Reflection\DocBlock', $docBlock->getNamespace()); + $docBlock2 = $cachedDocBlockFactory->createContext($refMethod); + $this->assertSame($docBlock2, $docBlock); + + $newCachedDocBlockFactory = new CachedDocBlockFactory( + new SnapshotClassBoundCache($arrayCache, FilesSnapshot::alwaysUnchanged(...)), + PhpDocumentorDocBlockFactory::default(), + ); + $docBlock3 = $newCachedDocBlockFactory->createContext($refMethod); + $this->assertEquals($docBlock3, $docBlock); + } +} diff --git a/tests/Reflection/DocBlock/PhpDocumentorDocBlockFactoryTest.php b/tests/Reflection/DocBlock/PhpDocumentorDocBlockFactoryTest.php new file mode 100644 index 0000000000..d8ad70f36b --- /dev/null +++ b/tests/Reflection/DocBlock/PhpDocumentorDocBlockFactoryTest.php @@ -0,0 +1,47 @@ +create($refMethod); + + $this->assertCount(1, $docBlock->getTagsByName('param')); + + /** @var Param $paramTag */ + $paramTag = $docBlock->getTagsByName('param')[0]; + + $this->assertEquals( + new Array_( + new Object_(new Fqsen('\\' . TestObject::class)) + ), + $paramTag->getType(), + ); + } + + public function testCreatesContext(): void + { + $docBlockFactory = PhpDocumentorDocBlockFactory::default(); + + $refMethod = (new ReflectionMethod(TestController::class, 'test')); + $context = $docBlockFactory->createContext($refMethod); + + $this->assertSame('TheCodingMachine\GraphQLite\Fixtures', $context->getNamespace()); + } +} diff --git a/tests/RootTypeMapperFactoryContextTest.php b/tests/RootTypeMapperFactoryContextTest.php index a87f5b7b9a..97d56d1d3c 100644 --- a/tests/RootTypeMapperFactoryContextTest.php +++ b/tests/RootTypeMapperFactoryContextTest.php @@ -5,10 +5,10 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Cache\Adapter\ArrayAdapter; use Symfony\Component\Cache\Psr16Cache; -use Symfony\Component\Cache\Simple\ArrayCache; +use TheCodingMachine\GraphQLite\Cache\FilesSnapshot; +use TheCodingMachine\GraphQLite\Cache\SnapshotClassBoundCache; use TheCodingMachine\GraphQLite\Containers\EmptyContainer; use TheCodingMachine\GraphQLite\Mappers\Root\RootTypeMapperFactoryContext; -use TheCodingMachine\GraphQLite\Utils\Namespaces\NS; class RootTypeMapperFactoryContextTest extends AbstractQueryProvider { @@ -19,7 +19,9 @@ public function testContext(): void $namingStrategy = new NamingStrategy(); $container = new EmptyContainer(); $arrayCache = new Psr16Cache(new ArrayAdapter()); - $nsList = [$this->getNamespaceFactory()->createNamespace('namespace')]; + $classFinder = $this->getClassFinder('namespace'); + $classFinderComputedCache = $this->getClassFinderComputedCache(); + $classBoundCache = new SnapshotClassBoundCache($arrayCache, FilesSnapshot::alwaysUnchanged(...)); $context = new RootTypeMapperFactoryContext( $this->getAnnotationReader(), @@ -29,8 +31,9 @@ public function testContext(): void $this->getTypeMapper(), $container, $arrayCache, - $nsList, - self::GLOB_TTL_SECONDS + $classFinder, + $classFinderComputedCache, + $classBoundCache, ); $this->assertSame($this->getAnnotationReader(), $context->getAnnotationReader()); @@ -40,9 +43,8 @@ public function testContext(): void $this->assertSame($this->getTypeMapper(), $context->getRecursiveTypeMapper()); $this->assertSame($container, $context->getContainer()); $this->assertSame($arrayCache, $context->getCache()); - $this->assertSame($nsList, $context->getTypeNamespaces()); - $this->assertContainsOnlyInstancesOf(NS::class, $context->getTypeNamespaces()); - $this->assertSame(self::GLOB_TTL_SECONDS, $context->getGlobTTL()); - $this->assertNull($context->getMapTTL()); + $this->assertSame($classFinder, $context->getClassFinder()); + $this->assertSame($classFinderComputedCache, $context->getClassFinderComputedCache()); + $this->assertSame($classBoundCache, $context->getClassBoundCache()); } } diff --git a/tests/SchemaFactoryTest.php b/tests/SchemaFactoryTest.php index 31666902a7..78defd5b78 100644 --- a/tests/SchemaFactoryTest.php +++ b/tests/SchemaFactoryTest.php @@ -50,8 +50,7 @@ public function testCreateSchema(): void $factory->setAuthenticationService(new VoidAuthenticationService()); $factory->setAuthorizationService(new VoidAuthorizationService()); - $factory->addControllerNamespace('TheCodingMachine\\GraphQLite\\Fixtures\\Integration\\Controllers'); - $factory->addTypeNamespace('TheCodingMachine\\GraphQLite\\Fixtures\\Integration'); + $factory->addNamespace('TheCodingMachine\\GraphQLite\\Fixtures\\Integration'); $factory->addQueryProvider(new AggregateQueryProvider([])); $factory->addFieldMiddleware(new FieldMiddlewarePipe()); $factory->addInputFieldMiddleware(new InputFieldMiddlewarePipe()); @@ -68,8 +67,7 @@ public function testSetters(): void $factory = new SchemaFactory($cache, $container); - $factory->addControllerNamespace('TheCodingMachine\\GraphQLite\\Fixtures\\Integration\\Controllers'); - $factory->addTypeNamespace('TheCodingMachine\\GraphQLite\\Fixtures\\Integration'); + $factory->addNamespace('TheCodingMachine\\GraphQLite\\Fixtures\\Integration'); $factory->setAuthenticationService(new VoidAuthenticationService()) ->setAuthorizationService(new VoidAuthorizationService()) ->setNamingStrategy(new NamingStrategy()) @@ -99,8 +97,7 @@ public function testFinderInjectionWithValidMapper(): void $factory->setAuthenticationService(new VoidAuthenticationService()) ->setAuthorizationService(new VoidAuthorizationService()) ->setFinder(new ComposerFinder()) - ->addControllerNamespace('TheCodingMachine\\GraphQLite\\Fixtures\\Integration\\Controllers') - ->addTypeNamespace('TheCodingMachine\\GraphQLite\\Fixtures\\Integration'); + ->addNamespace('TheCodingMachine\\GraphQLite\\Fixtures\\Integration'); $schema = $factory->createSchema(); @@ -146,8 +143,7 @@ public function testFinderInjectionWithInvalidMapper(): void $factory->setAuthenticationService(new VoidAuthenticationService()) ->setAuthorizationService(new VoidAuthorizationService()) ->setFinder(new RecursiveFinder(__DIR__ . '/Annotations')) - ->addControllerNamespace('TheCodingMachine\\GraphQLite\\Fixtures\\Integration\\Controllers') - ->addTypeNamespace('TheCodingMachine\\GraphQLite\\Fixtures\\Integration'); + ->addNamespace('TheCodingMachine\\GraphQLite\\Fixtures\\Integration'); $this->doTestSchemaWithError($factory->createSchema()); } @@ -163,18 +159,6 @@ public function testException(): void $factory->createSchema(); } - public function testException2(): void - { - $container = new BasicAutoWiringContainer(new EmptyContainer()); - $cache = new Psr16Cache(new ArrayAdapter()); - - $factory = new SchemaFactory($cache, $container); - $factory->addTypeNamespace('TheCodingMachine\\GraphQLite\\Fixtures\\Integration'); - - $this->expectException(GraphQLRuntimeException::class); - $factory->createSchema(); - } - private function execTestQuery(Schema $schema): ExecutionResult { $schema->assertValid(); @@ -238,8 +222,8 @@ public function testDuplicateQueryException(): void ); $factory->setAuthenticationService(new VoidAuthenticationService()) ->setAuthorizationService(new VoidAuthorizationService()) - ->addControllerNamespace('TheCodingMachine\\GraphQLite\\Fixtures\\DuplicateQueries') - ->addTypeNamespace('TheCodingMachine\\GraphQLite\\Fixtures\\Integration'); + ->addNamespace('TheCodingMachine\\GraphQLite\\Fixtures\\DuplicateQueries') + ->addNamespace('TheCodingMachine\\GraphQLite\\Fixtures\\Integration'); $this->expectException(DuplicateMappingException::class); $this->expectExceptionMessage("The query/mutation/field 'duplicateQuery' is declared twice in class 'TheCodingMachine\GraphQLite\Fixtures\DuplicateQueries\TestControllerWithDuplicateQuery'. First in 'TheCodingMachine\GraphQLite\Fixtures\DuplicateQueries\TestControllerWithDuplicateQuery::testDuplicateQuery1()', second in 'TheCodingMachine\GraphQLite\Fixtures\DuplicateQueries\TestControllerWithDuplicateQuery::testDuplicateQuery2()'"); @@ -265,8 +249,8 @@ public function testDuplicateQueryInTwoControllersException(): void ); $factory->setAuthenticationService(new VoidAuthenticationService()) ->setAuthorizationService(new VoidAuthorizationService()) - ->addControllerNamespace('TheCodingMachine\\GraphQLite\\Fixtures\\DuplicateQueriesInTwoControllers') - ->addTypeNamespace('TheCodingMachine\\GraphQLite\\Fixtures\\Integration'); + ->addNamespace('TheCodingMachine\\GraphQLite\\Fixtures\\DuplicateQueriesInTwoControllers') + ->addNamespace('TheCodingMachine\\GraphQLite\\Fixtures\\Integration'); $this->expectException(DuplicateMappingException::class); $this->expectExceptionMessage("The query/mutation 'duplicateQuery' is declared twice: in class 'TheCodingMachine\\GraphQLite\\Fixtures\\DuplicateQueriesInTwoControllers\\TestControllerWithDuplicateQuery1' and in class 'TheCodingMachine\\GraphQLite\\Fixtures\\DuplicateQueriesInTwoControllers\\TestControllerWithDuplicateQuery2"); diff --git a/tests/Utils/NsTest.php b/tests/Utils/NsTest.php deleted file mode 100644 index b46d8bfe6d..0000000000 --- a/tests/Utils/NsTest.php +++ /dev/null @@ -1,145 +0,0 @@ -cache = new Psr16Cache(new ArrayAdapter()); - $this->namespace = 'TheCodingMachine\GraphQLite\Fixtures\Types'; - $this->finder = new ComposerFinder(); - $this->globTTL = 10; - } - - #[DataProvider('loadsClassListProvider')] - public function testLoadsClassList(array $expectedClasses, string $namespace): void - { - $ns = new NS( - namespace: $namespace, - cache: $this->cache, - finder: $this->finder, - globTTL: null, - ); - - self::assertEqualsCanonicalizing($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, - EnumType::class - ], - 'TheCodingMachine\GraphQLite\Fixtures\Types', - ]; - - // The class should be ignored. - yield 'incorrect namespace class without autoload' => [ - [], - 'TheCodingMachine\GraphQLite\Fixtures\BadNamespace', - ]; - } - - public function testCaching(): void - { - $ns = new NS( - namespace: $this->namespace, - cache: $this->cache, - finder: $this->finder, - globTTL: $this->globTTL, - ); - self::assertNotNull($ns->getClassList()); - - // create with mock finder to test cache - $finder = $this->createMock(FinderInterface::class); - $finder->expects(self::never())->method('inNamespace')->willReturnSelf(); - $ns = new NS( - namespace: $this->namespace, - cache: $this->cache, - finder: $finder, - globTTL: $this->globTTL, - ); - self::assertNotNull($ns->getClassList()); - } - - public function testCachingWithInvalidKey(): void - { - $exception = new class extends Exception implements InvalidArgumentException { - }; - $cache = $this->createMock(CacheInterface::class); - $cache->expects(self::once())->method('get')->willThrowException($exception); - $cache->expects(self::once())->method('set')->willThrowException($exception); - $ns = new NS( - namespace: $this->namespace, - cache: $cache, - finder: $this->finder, - globTTL: $this->globTTL, - ); - $ns->getClassList(); - } - - public function testCachingWithInvalidCache(): void - { - $cache = $this->createMock(CacheInterface::class); - $cache->expects(self::once())->method('get')->willReturn(['foo']); - $ns = new NS( - namespace: $this->namespace, - cache: $cache, - finder: $this->finder, - globTTL: $this->globTTL, - ); - $classList = $ns->getClassList(); - self::assertNotNull($classList); - self::assertNotEmpty($classList); - } - - public function testFinderWithUnexpectedOutput() { - - $finder = $this->createMock(FinderInterface::class); - $finder->expects(self::once())->method('inNamespace')->willReturnSelf(); - $finder->expects(self::once())->method('getIterator')->willReturn(new \ArrayIterator([ 'test' => new \ReflectionException()])); - $ns = new NS( - namespace: $this->namespace, - cache: $this->cache, - finder: $finder, - globTTL: $this->globTTL, - ); - $classList = $ns->getClassList(); - self::assertNotNull($classList); - self::assertEmpty($classList);} -} diff --git a/website/docs/CHANGELOG.md b/website/docs/CHANGELOG.md index fc3f57dba6..bb83ceb4a0 100644 --- a/website/docs/CHANGELOG.md +++ b/website/docs/CHANGELOG.md @@ -4,6 +4,24 @@ title: Changelog sidebar_label: Changelog --- +## 7.1.0 + +### Breaking Changes + +- #698 Removes some methods and classes, namely: + - Deprecated `SchemaFactory::addControllerNamespace()` and `SchemaFactory::addTypeNamespace()` in favor of `SchemaFactory::addNamespace()` + - Deprecated `SchemaFactory::setGlobTTL()` in favor of `SchemaFactory::devMode()` and `SchemaFactory::prodMode()` + - Removed `FactoryContext::get*TTL()` and `RootTypeMapperFactoryContext::get*TTL()` as GraphQLite no longer uses TTLs to invalidate caches + - Removed `StaticClassListTypeMapper` in favor of `ClassFinderTypeMapper` used with `StaticClassFinder` + - Renamed `GlobTypeMapper` to `ClassFinderTypeMapper` + - Renamed `SchemaFactory::setClassBoundCacheContractFactory()` to `SchemaFactory::setClassBoundCache()`, + `FactoryContext::getClassBoundCacheContractFactory()` to `FactoryContext::getClassBoundCache()` and changed their signatures + - Removed `RootTypeMapperFactoryContext::getTypeNamespaces()` in favor of `RootTypeMapperFactoryContext::getClassFinder()` + +### Improvements + +- #698 Performance optimizations and caching in development environments (`devMode()`). @oprypkhantc + ## 7.0.0 ### Breaking Changes diff --git a/website/docs/input-types.mdx b/website/docs/input-types.mdx index c0efa59e87..9b5be40278 100644 --- a/website/docs/input-types.mdx +++ b/website/docs/input-types.mdx @@ -55,7 +55,7 @@ You are running into this error because GraphQLite does not know how to handle t In GraphQL, an object passed in parameter of a query or mutation (or any field) is called an **Input Type**. -There are two ways for declaring that type, in GraphQLite: using the [`#[Input]` attribute](input-attribute) or a [Factory method](factory). +There are two ways for declaring that type, in GraphQLite: using the [`#[Input]` attribute](#input-attribute) or a [Factory method](#factory). ## #\[Input\] Attribute diff --git a/website/docs/other-frameworks.mdx b/website/docs/other-frameworks.mdx index 2bd411b0e1..e2684c0e9f 100644 --- a/website/docs/other-frameworks.mdx +++ b/website/docs/other-frameworks.mdx @@ -35,8 +35,7 @@ use TheCodingMachine\GraphQLite\SchemaFactory; // $cache is a PSR-16 compatible cache // $container is a PSR-11 compatible container $factory = new SchemaFactory($cache, $container); -$factory->addControllerNamespace('App\\Controllers') - ->addTypeNamespace('App'); +$factory->addNamespace('App'); $schema = $factory->createSchema(); ``` @@ -106,8 +105,7 @@ use Kcs\ClassFinder\Finder\ComposerFinder; use TheCodingMachine\GraphQLite\SchemaFactory; $factory = new SchemaFactory($cache, $container); -$factory->addControllerNamespace('App\\Controllers') - ->addTypeNamespace('App') +$factory->addNamespace('App') ->setFinder( (new ComposerFinder())->useAutoloading(false) ); @@ -129,8 +127,7 @@ use TheCodingMachine\GraphQLite\Context\Context; // $cache is a PSR-16 compatible cache. // $container is a PSR-11 compatible container. $factory = new SchemaFactory($cache, $container); -$factory->addControllerNamespace('App\\Controllers') - ->addTypeNamespace('App'); +$factory->addNamespace('App'); $schema = $factory->createSchema(); @@ -287,8 +284,7 @@ return new Picotainer([ Schema::class => function(ContainerInterface $container) { // The magic happens here. We create a schema using GraphQLite SchemaFactory. $factory = new SchemaFactory($container->get(CacheInterface::class), $container); - $factory->addControllerNamespace('App\\Controllers'); - $factory->addTypeNamespace('App'); + $factory->addNamespace('App'); return $factory->createSchema(); } ]); diff --git a/website/docs/validation.mdx b/website/docs/validation.mdx index 13a05188fd..dde8a9413a 100644 --- a/website/docs/validation.mdx +++ b/website/docs/validation.mdx @@ -154,8 +154,7 @@ To get started with validation on input types defined by an `#[Input]` attribute ```php $factory = new SchemaFactory($cache, $this->container); -$factory->addControllerNamespace('App\\Controllers'); -$factory->addTypeNamespace('App'); +$factory->addNamespace('App'); // Register your validator $factory->setInputTypeValidator($this->container->get('your_validator')); $factory->createSchema(); diff --git a/website/versioned_docs/version-6.0/input-types.mdx b/website/versioned_docs/version-6.0/input-types.mdx index f2c62afd40..c5737d9176 100644 --- a/website/versioned_docs/version-6.0/input-types.mdx +++ b/website/versioned_docs/version-6.0/input-types.mdx @@ -109,7 +109,7 @@ You are running into this error because GraphQLite does not know how to handle t In GraphQL, an object passed in parameter of a query or mutation (or any field) is called an **Input Type**. -There are two ways for declaring that type, in GraphQLite: using the [`#[Input]` attribute](input-attribute) or a [Factory method](factory). +There are two ways for declaring that type, in GraphQLite: using the [`#[Input]` attribute](#input-attribute) or a [Factory method](#factory). ## #\[Input\] Attribute diff --git a/website/versioned_docs/version-6.1/input-types.mdx b/website/versioned_docs/version-6.1/input-types.mdx index 06754d97a1..48369cda90 100644 --- a/website/versioned_docs/version-6.1/input-types.mdx +++ b/website/versioned_docs/version-6.1/input-types.mdx @@ -59,7 +59,7 @@ You are running into this error because GraphQLite does not know how to handle t In GraphQL, an object passed in parameter of a query or mutation (or any field) is called an **Input Type**. -There are two ways for declaring that type, in GraphQLite: using the [`#[Input]` attribute](input-attribute) or a [Factory method](factory). +There are two ways for declaring that type, in GraphQLite: using the [`#[Input]` attribute](#input-attribute) or a [Factory method](#factory). ## #\[Input\] Attribute diff --git a/website/versioned_docs/version-7.0.0/input-types.mdx b/website/versioned_docs/version-7.0.0/input-types.mdx index f2c62afd40..c5737d9176 100644 --- a/website/versioned_docs/version-7.0.0/input-types.mdx +++ b/website/versioned_docs/version-7.0.0/input-types.mdx @@ -109,7 +109,7 @@ You are running into this error because GraphQLite does not know how to handle t In GraphQL, an object passed in parameter of a query or mutation (or any field) is called an **Input Type**. -There are two ways for declaring that type, in GraphQLite: using the [`#[Input]` attribute](input-attribute) or a [Factory method](factory). +There are two ways for declaring that type, in GraphQLite: using the [`#[Input]` attribute](#input-attribute) or a [Factory method](#factory). ## #\[Input\] Attribute