diff --git a/.travis.yml b/.travis.yml index a92655d..061fb68 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,15 +1,18 @@ language: php -php: -- 7.1 -- 7.2 -- 7.3 cache: directories: - $HOME/.composer/cache -env: - matrix: - - PREFER_LOWEST="--prefer-lowest" - - PREFER_LOWEST="" +matrix: + include: + - php: 7.3 + env: PREFER_LOWEST="" + - php: 7.2 + env: PREFER_LOWEST="" + - php: 7.1 + env: PREFER_LOWEST="" + - php: 7.1 + env: PREFER_LOWEST="--prefer-lowest" + before_script: - composer update --prefer-dist $PREFER_LOWEST script: diff --git a/src/AnnotationReader.php b/src/AnnotationReader.php index 490ecb6..2f4c124 100644 --- a/src/AnnotationReader.php +++ b/src/AnnotationReader.php @@ -13,6 +13,7 @@ use function substr; use TheCodingMachine\GraphQL\Controllers\Annotations\AbstractRequest; use TheCodingMachine\GraphQL\Controllers\Annotations\Exceptions\ClassNotFoundException; +use TheCodingMachine\GraphQL\Controllers\Annotations\ExtendType; use TheCodingMachine\GraphQL\Controllers\Annotations\Factory; use TheCodingMachine\GraphQL\Controllers\Annotations\FailWith; use TheCodingMachine\GraphQL\Controllers\Annotations\Logged; @@ -64,14 +65,24 @@ public function __construct(Reader $reader, string $mode = self::STRICT_MODE, ar public function getTypeAnnotation(ReflectionClass $refClass): ?Type { - // TODO: customize the way errors are handled here! try { - /** @var Type|null $typeField */ - $typeField = $this->getClassAnnotation($refClass, Type::class); + /** @var Type|null $type */ + $type = $this->getClassAnnotation($refClass, Type::class); } catch (ClassNotFoundException $e) { throw ClassNotFoundException::wrapException($e, $refClass->getName()); } - return $typeField; + return $type; + } + + public function getExtendTypeAnnotation(ReflectionClass $refClass): ?ExtendType + { + try { + /** @var ExtendType|null $extendType */ + $extendType = $this->getClassAnnotation($refClass, ExtendType::class); + } catch (ClassNotFoundException $e) { + throw ClassNotFoundException::wrapException($e, $refClass->getName()); + } + return $extendType; } public function getRequestAnnotation(ReflectionMethod $refMethod, string $annotationName): ?AbstractRequest diff --git a/src/Annotations/ExtendType.php b/src/Annotations/ExtendType.php new file mode 100644 index 0000000..fcfdbc9 --- /dev/null +++ b/src/Annotations/ExtendType.php @@ -0,0 +1,51 @@ +className = $attributes['class']; + if (!class_exists($this->className)) { + throw ClassNotFoundException::couldNotFindClass($this->className); + } + } + + /** + * Returns the name of the GraphQL query/mutation/field. + * If not specified, the name of the method should be used instead. + * + * @return string + */ + public function getClass(): string + { + return ltrim($this->className, '\\'); + } +} diff --git a/src/Cache/CacheValidatorInterface.php b/src/Cache/CacheValidatorInterface.php new file mode 100644 index 0000000..cfc91a7 --- /dev/null +++ b/src/Cache/CacheValidatorInterface.php @@ -0,0 +1,19 @@ + + */ + protected $trackedFiles = []; + + /** + * Adds a file to track. + * All files must not have changed for the cache to be valid. + * + * @param string $fileName + */ + public function addTrackedFile(string $fileName): void + { + $this->trackedFiles[$fileName] = filemtime($fileName); + } + + /** + * Returns true if this item is valid (coming out of cache), false otherwise. + * + * @return bool + */ + public function isValid(): bool + { + foreach ($this->trackedFiles as $fileName => $mtime) { + if (filemtime($fileName) !== $mtime) { + return false; + } + } + return true; + } +} diff --git a/src/Cache/InvalidCacheItemException.php b/src/Cache/InvalidCacheItemException.php new file mode 100644 index 0000000..73b360e --- /dev/null +++ b/src/Cache/InvalidCacheItemException.php @@ -0,0 +1,13 @@ +cache = $cache; + } + + /** + * Fetches a value from the cache. + * + * @param string $key The unique key of this item in the cache. + * + * @return mixed The value of the item from the cache, or null in case of cache miss. + */ + public function get(string $key) + { + $item = $this->cache->get($key); + if (!$item instanceof CacheValidatorInterface) { + throw InvalidCacheItemException::create($key); + } + return $item; + } + + /** + * Persists data in the cache, uniquely referenced by a key with an optional expiration TTL time. + * + * @param string $key The key of the item to store. + * @param mixed $value The value of the item to store, must be serializable. + * + */ + public function set(string $key, $value) + { + if (!$value instanceof CacheValidatorInterface) { + throw InvalidCacheItemException::create($key); + } + $this->cache->set($key, $value); + } +} diff --git a/src/Cache/SelfValidatingCacheInterface.php b/src/Cache/SelfValidatingCacheInterface.php new file mode 100644 index 0000000..49c4dd7 --- /dev/null +++ b/src/Cache/SelfValidatingCacheInterface.php @@ -0,0 +1,28 @@ +name.'" mapped by class "'.$className.'". Check your TypeMapper configuration.'); + } + + public static function createForExtendName(string $name, ObjectType $type): self + { + return new self('cannot extend GraphQL type "'.$type->name.'" with type "'.$name.'". Check your TypeMapper configuration.'); + } } diff --git a/src/Mappers/CompositeTypeMapper.php b/src/Mappers/CompositeTypeMapper.php index c5fe9fe..1606048 100644 --- a/src/Mappers/CompositeTypeMapper.php +++ b/src/Mappers/CompositeTypeMapper.php @@ -14,6 +14,7 @@ use GraphQL\Type\Definition\Type; use function is_array; use function iterator_to_array; +use TheCodingMachine\GraphQL\Controllers\Types\MutableObjectType; class CompositeTypeMapper implements TypeMapperInterface { @@ -59,10 +60,10 @@ public function canMapClassToType(string $className): bool * @param string $className * @param OutputType|null $subType * @param RecursiveTypeMapperInterface $recursiveTypeMapper - * @return ObjectType + * @return MutableObjectType * @throws CannotMapTypeExceptionInterface */ - public function mapClassToType(string $className, ?OutputType $subType, RecursiveTypeMapperInterface $recursiveTypeMapper): ObjectType + public function mapClassToType(string $className, ?OutputType $subType, RecursiveTypeMapperInterface $recursiveTypeMapper): MutableObjectType { foreach ($this->typeMappers as $typeMapper) { if ($typeMapper->canMapClassToType($className)) { @@ -152,4 +153,72 @@ public function canMapNameToType(string $typeName): bool } return false; } + + /** + * Returns true if this type mapper can extend an existing type for the $className FQCN + * + * @param string $className + * @param MutableObjectType $type + * @return bool + */ + public function canExtendTypeForClass(string $className, MutableObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): bool + { + foreach ($this->typeMappers as $typeMapper) { + if ($typeMapper->canExtendTypeForClass($className, $type, $recursiveTypeMapper)) { + return true; + } + } + return false; + } + + /** + * Extends the existing GraphQL type that is mapped to $className. + * + * @param string $className + * @param MutableObjectType $type + * @param RecursiveTypeMapperInterface $recursiveTypeMapper + * @throws CannotMapTypeExceptionInterface + */ + public function extendTypeForClass(string $className, MutableObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): void + { + foreach ($this->typeMappers as $typeMapper) { + if ($typeMapper->canExtendTypeForClass($className, $type, $recursiveTypeMapper)) { + $typeMapper->extendTypeForClass($className, $type, $recursiveTypeMapper); + } + } + } + + /** + * Returns true if this type mapper can extend an existing type for the $typeName GraphQL type + * + * @param string $typeName + * @param MutableObjectType $type + * @return bool + */ + public function canExtendTypeForName(string $typeName, MutableObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): bool + { + foreach ($this->typeMappers as $typeMapper) { + if ($typeMapper->canExtendTypeForName($typeName, $type, $recursiveTypeMapper)) { + return true; + } + } + return false; + } + + /** + * Extends the existing GraphQL type that is mapped to the $typeName GraphQL type. + * + * @param string $typeName + * @param MutableObjectType $type + * @param RecursiveTypeMapperInterface $recursiveTypeMapper + * @throws CannotMapTypeExceptionInterface + */ + public function extendTypeForName(string $typeName, MutableObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): void + { + foreach ($this->typeMappers as $typeMapper) { + if ($typeMapper->canExtendTypeForName($typeName, $type, $recursiveTypeMapper)) { + $typeMapper->extendTypeForName($typeName, $type, $recursiveTypeMapper); + } + } + } } diff --git a/src/Mappers/GlobTypeMapper.php b/src/Mappers/GlobTypeMapper.php index 3e1a0e8..832cb2f 100644 --- a/src/Mappers/GlobTypeMapper.php +++ b/src/Mappers/GlobTypeMapper.php @@ -12,15 +12,18 @@ use Mouf\Composer\ClassNameMapper; use Psr\Container\ContainerInterface; use Psr\SimpleCache\CacheInterface; +use ReflectionClass; use ReflectionMethod; use TheCodingMachine\ClassExplorer\Glob\GlobClassExplorer; use TheCodingMachine\GraphQL\Controllers\AnnotationReader; +use TheCodingMachine\GraphQL\Controllers\Annotations\ExtendType; use TheCodingMachine\GraphQL\Controllers\Annotations\Factory; use TheCodingMachine\GraphQL\Controllers\Annotations\Type; use TheCodingMachine\GraphQL\Controllers\InputTypeGenerator; use TheCodingMachine\GraphQL\Controllers\InputTypeUtils; use TheCodingMachine\GraphQL\Controllers\NamingStrategy; use TheCodingMachine\GraphQL\Controllers\TypeGenerator; +use TheCodingMachine\GraphQL\Controllers\Types\MutableObjectType; /** * Scans all the classes in a given namespace of the main project (not the vendor directory). @@ -50,10 +53,18 @@ final class GlobTypeMapper implements TypeMapperInterface * @var array Maps a domain class to the GraphQL type annotated class */ private $mapClassToTypeArray = []; + /** + * @var array> Maps a domain class to one or many type extenders (with the @ExtendType annotation) The array of type extenders has a key and value equals to FQCN + */ + private $mapClassToExtendTypeArray = []; /** * @var array Maps a GraphQL type name to the GraphQL type annotated class */ private $mapNameToType = []; + /** + * @var array> Maps a GraphQL type name to one or many type extenders (with the @ExtendType annotation) The array of type extenders has a key and value equals to FQCN + */ + private $mapNameToExtendType = []; /** * @var array Maps a domain class to the factory method that creates the input type in the form [classname, methodname] */ @@ -78,6 +89,10 @@ final class GlobTypeMapper implements TypeMapperInterface * @var bool */ private $fullMapComputed = false; + /** + * @var bool + */ + private $fullExtendMapComputed = false; /** * @var NamingStrategy */ @@ -90,6 +105,14 @@ final class GlobTypeMapper implements TypeMapperInterface * @var InputTypeUtils */ private $inputTypeUtils; + /** + * The array of globbed classes. + * Only instantiable classes are returned. + * Key: fully qualified class name + * + * @var array + */ + private $classes; /** * @param string $namespace The namespace that contains the GraphQL types (they must have a `@Type` annotation) @@ -111,9 +134,9 @@ public function __construct(string $namespace, TypeGenerator $typeGenerator, Inp /** * Returns an array of fully qualified class names. * - * @return array + * @return array> */ - private function getMap(): array + private function getMaps(): array { if ($this->fullMapComputed === false) { $namespace = str_replace('\\', '_', $this->namespace); @@ -125,7 +148,11 @@ private function getMap(): array $this->mapNameToType = $this->cache->get($keyNameCache); $this->mapClassToFactory = $this->cache->get($keyInputClassCache); $this->mapInputNameToFactory = $this->cache->get($keyInputNameCache); - if ($this->mapClassToTypeArray === null || $this->mapNameToType === null || $this->mapClassToFactory === null || $this->mapInputNameToFactory) { + if ($this->mapClassToTypeArray === null || + $this->mapNameToType === null || + $this->mapClassToFactory === null || + $this->mapInputNameToFactory + ) { $this->buildMap(); // This is a very short lived cache. Useful to avoid overloading a server in case of heavy load. // Defaults to 2 seconds. @@ -134,23 +161,106 @@ private function getMap(): array $this->cache->set($keyInputClassCache, $this->mapClassToFactory, $this->globTtl); $this->cache->set($keyInputNameCache, $this->mapInputNameToFactory, $this->globTtl); } + $this->fullMapComputed = true; } - return $this->mapClassToTypeArray; + return [ + 'mapClassToTypeArray' => $this->mapClassToTypeArray, + 'mapNameToType' => $this->mapNameToType, + 'mapClassToFactory' => $this->mapClassToFactory, + 'mapInputNameToFactory' => $this->mapInputNameToFactory, + ]; } - private function buildMap(): void + private function getMapClassToType(): array { - $explorer = new GlobClassExplorer($this->namespace, $this->cache, $this->globTtl, ClassNameMapper::createFromComposerFile(null, null, true)); - $classes = $explorer->getClasses(); - foreach ($classes as $className) { - if (!\class_exists($className)) { - continue; + return $this->getMaps()['mapClassToTypeArray']; + } + + private function getMapNameToType(): array + { + return $this->getMaps()['mapNameToType']; + } + + private function getMapClassToFactory(): array + { + return $this->getMaps()['mapClassToFactory']; + } + + private function getMapInputNameToFactory(): array + { + return $this->getMaps()['mapInputNameToFactory']; + } + + /** + * Returns an array of fully qualified class names. + * + * @return array> + */ + private function getExtendMaps(RecursiveTypeMapperInterface $recursiveTypeMapper): array + { + if ($this->fullExtendMapComputed === false) { + $namespace = str_replace('\\', '_', $this->namespace); + $keyExtendClassCache = 'globTypeMapperExtend_'.$namespace; + $keyExtendNameCache = 'globTypeMapperExtend_names_'.$namespace; + $this->mapClassToExtendTypeArray = $this->cache->get($keyExtendClassCache); + $this->mapNameToExtendType = $this->cache->get($keyExtendNameCache); + if ($this->mapClassToExtendTypeArray === null || + $this->mapNameToExtendType === null + ) { + $this->buildExtendMap($recursiveTypeMapper); + // This is a very short lived cache. Useful to avoid overloading a server in case of heavy load. + // Defaults to 2 seconds. + $this->cache->set($keyExtendClassCache, $this->mapClassToExtendTypeArray, $this->globTtl); + $this->cache->set($keyExtendNameCache, $this->mapNameToExtendType, $this->globTtl); } - $refClass = new \ReflectionClass($className); - if (!$refClass->isInstantiable()) { - continue; + $this->fullExtendMapComputed = true; + } + return [ + 'mapClassToExtendTypeArray' => $this->mapClassToExtendTypeArray, + 'mapNameToExtendType' => $this->mapNameToExtendType, + ]; + } + + private function getMapClassToExtendTypeArray(RecursiveTypeMapperInterface $recursiveTypeMapper): array + { + return $this->getExtendMaps($recursiveTypeMapper)['mapClassToExtendTypeArray']; + } + + private function getMapNameToExtendType(RecursiveTypeMapperInterface $recursiveTypeMapper): array + { + return $this->getExtendMaps($recursiveTypeMapper)['mapNameToExtendType']; + } + + /** + * Returns the array of globbed classes. + * Only instantiable classes are returned. + * + * @return array Key: fully qualified class name + */ + private function getClassList(): array + { + if ($this->classes === null) { + $this->classes = []; + $explorer = new GlobClassExplorer($this->namespace, $this->cache, $this->globTtl, ClassNameMapper::createFromComposerFile(null, null, true)); + $classes = $explorer->getClasses(); + foreach ($classes as $className) { + if (!\class_exists($className)) { + continue; + } + $refClass = new \ReflectionClass($className); + if (!$refClass->isInstantiable()) { + continue; + } + $this->classes[$className] = $refClass; } + } + return $this->classes; + } + private function buildMap(): void + { + $classes = $this->getClassList(); + foreach ($classes as $className => $refClass) { $type = $this->annotationReader->getTypeAnnotation($refClass); if ($type !== null) { @@ -177,7 +287,18 @@ private function buildMap(): void } } - $this->fullMapComputed = true; + } + + private function buildExtendMap(RecursiveTypeMapperInterface $recursiveTypeMapper): void + { + $classes = $this->getClassList(); + foreach ($classes as $className => $refClass) { + $extendType = $this->annotationReader->getExtendTypeAnnotation($refClass); + + if ($extendType !== null) { + $this->storeExtendTypeInCache($className, $extendType, $refClass->getFileName(), $recursiveTypeMapper); + } + } } /** @@ -221,6 +342,49 @@ private function storeInputTypeInCache(ReflectionMethod $refMethod, string $inpu ], $this->mapTtl); } + /** + * Stores in cache the mapping ExtendTypeClass <=> Object class <=> GraphQL type name. + */ + private function storeExtendTypeInCache(string $extendTypeClassName, ExtendType $extendType, string $typeFileName, RecursiveTypeMapperInterface $recursiveTypeMapper): void + { + $objectClassName = $extendType->getClass(); + $this->mapClassToExtendTypeArray[$objectClassName][$extendTypeClassName] = $extendTypeClassName; + $this->cache->set('globExtendTypeMapperByClass_'.str_replace('\\', '_', $objectClassName), [ + 'filemtime' => filemtime($typeFileName), + 'fileName' => $typeFileName, + 'extendTypeClasses' => $this->mapClassToExtendTypeArray[$objectClassName] + ], $this->mapTtl); + + // TODO: this is kind of a hack. Ideally, we would need to find the GraphQL type name from the class name. + + // FIXME: this is WRONG! we need to get the NAME of the GraphQL type from the $extendTypeClassName + // The only thing we have is the name of the main class (in $extendType->getClass()) + // From there, we need to FIND the name of the type. We need a $recursiveTypeMapper->mapClassToTypeName method. + + // OOOOOOR: MAYBE WE DONT STORE THIS ASSOCIATION AT ALL!! => How??? + + // OOOOOOR again: ExtendType is targetting the GraphQL NAME and not the type!!! @ExtendType(name="Foo") => But that does not work, we also need the class name just above! + + + // YET ANOTHER IDEA: global refactor: + // Instead of returning types, we return TypeFactories. + // A type factory is an interface with: + // - className + // - typeName + // - list of field factories + // - list of files used to build it with timestamp. Any change in one file and the type is no longer valid + // A type factory is serializable. + + $targetType = $recursiveTypeMapper->mapClassToType($extendType->getClass(), null); + $typeName = $targetType->name; + + $this->mapNameToExtendType[$typeName][$extendTypeClassName] = $extendTypeClassName; + $this->cache->set('globExtendTypeMapperByName_'.$typeName, [ + 'filemtime' => filemtime($typeFileName), + 'fileName' => $typeFileName, + 'extendTypeClasses' => $this->mapClassToExtendTypeArray[$objectClassName] + ], $this->mapTtl); + } private function getTypeFromCacheByObjectClass(string $className): ?string { @@ -300,6 +464,64 @@ private function getFactoryFromCacheByObjectClass(string $className): ?array return null; } + /** + * @param string $className + * @return array|null An array of classes with the @ExtendType annotation (key and value = FQCN) + */ + private function getExtendTypesFromCacheByObjectClass(string $className): ?array + { + if (isset($this->mapClassToExtendTypeArray[$className])) { + return $this->mapClassToExtendTypeArray[$className]; + } + + // Let's try from the cache + $item = $this->cache->get('globExtendTypeMapperByClass_'.str_replace('\\', '_', $className)); + if ($item !== null) { + [ + 'filemtime' => $filemtime, + 'fileName' => $typeFileName, + 'extendTypeClasses' => $extendTypeClassNames + ] = $item; + + if ($filemtime === filemtime($typeFileName)) { + $this->mapClassToExtendTypeArray[$className] = $extendTypeClassNames; + return $extendTypeClassNames; + } + } + + // cache miss + return null; + } + + /** + * @param string $graphqlTypeName + * @return array|null An array of classes with the @ExtendType annotation (key and value = FQCN) + */ + private function getExtendTypesFromCacheByGraphQLTypeName(string $graphqlTypeName): ?array + { + if (isset($this->mapNameToExtendType[$graphqlTypeName])) { + return $this->mapNameToExtendType[$graphqlTypeName]; + } + + // Let's try from the cache + $item = $this->cache->get('globExtendTypeMapperByName_'.$graphqlTypeName); + if ($item !== null) { + [ + 'filemtime' => $filemtime, + 'fileName' => $typeFileName, + 'extendTypeClasses' => $extendTypeClassNames + ] = $item; + + if ($filemtime === filemtime($typeFileName)) { + $this->mapNameToExtendType[$graphqlTypeName] = $extendTypeClassNames; + return $extendTypeClassNames; + } + } + + // cache miss + return null; + } + /** * @return string[]|null A pointer to the factory [$className, $methodName] or null on cache miss */ @@ -339,7 +561,7 @@ public function canMapClassToType(string $className): bool $typeClassName = $this->getTypeFromCacheByObjectClass($className); if ($typeClassName === null) { - $this->getMap(); + $this->getMaps(); } return isset($this->mapClassToTypeArray[$className]); @@ -351,15 +573,15 @@ public function canMapClassToType(string $className): bool * @param string $className The exact class name to look for (this function does not look into parent classes). * @param OutputType|null $subType An optional sub-type if the main class is an iterator that needs to be typed. * @param RecursiveTypeMapperInterface $recursiveTypeMapper - * @return ObjectType + * @return MutableObjectType * @throws CannotMapTypeExceptionInterface */ - public function mapClassToType(string $className, ?OutputType $subType, RecursiveTypeMapperInterface $recursiveTypeMapper): ObjectType + public function mapClassToType(string $className, ?OutputType $subType, RecursiveTypeMapperInterface $recursiveTypeMapper): MutableObjectType { $typeClassName = $this->getTypeFromCacheByObjectClass($className); if ($typeClassName === null) { - $this->getMap(); + $this->getMaps(); } if (!isset($this->mapClassToTypeArray[$className])) { @@ -375,7 +597,7 @@ public function mapClassToType(string $className, ?OutputType $subType, Recursiv */ public function getSupportedClasses(): array { - return array_keys($this->getMap()); + return array_keys($this->getMapClassToType()); } /** @@ -389,7 +611,7 @@ public function canMapClassToInputType(string $className): bool $factory = $this->getFactoryFromCacheByObjectClass($className); if ($factory === null) { - $this->getMap(); + $this->getMaps(); } return isset($this->mapClassToFactory[$className]); } @@ -407,7 +629,7 @@ public function mapClassToInputType(string $className, RecursiveTypeMapperInterf $factory = $this->getFactoryFromCacheByObjectClass($className); if ($factory === null) { - $this->getMap(); + $this->getMaps(); } if (!isset($this->mapClassToFactory[$className])) { @@ -431,7 +653,7 @@ public function mapNameToType(string $typeName, RecursiveTypeMapperInterface $re if ($typeClassName === null) { $factory = $this->getFactoryFromCacheByGraphQLInputTypeName($typeName); if ($factory === null) { - $this->getMap(); + $this->getMaps(); } } @@ -465,8 +687,108 @@ public function canMapNameToType(string $typeName): bool return true; } - $this->getMap(); + $this->getMaps(); return isset($this->mapNameToType[$typeName]) || isset($this->mapInputNameToFactory[$typeName]); } + + /** + * Returns true if this type mapper can extend an existing type for the $className FQCN + * + * @param string $className + * @param MutableObjectType $type + * @return bool + */ + public function canExtendTypeForClass(string $className, MutableObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): bool + { + $extendTypeClassName = $this->getExtendTypesFromCacheByObjectClass($className); + + if ($extendTypeClassName === null) { + $this->getExtendMaps($recursiveTypeMapper); + } + + return isset($this->mapClassToExtendTypeArray[$className]); + } + + /** + * Extends the existing GraphQL type that is mapped to $className. + * + * @param string $className + * @param MutableObjectType $type + * @param RecursiveTypeMapperInterface $recursiveTypeMapper + * @throws CannotMapTypeExceptionInterface + */ + public function extendTypeForClass(string $className, MutableObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): void + { + $extendTypeClassNames = $this->getExtendTypesFromCacheByObjectClass($className); + + if ($extendTypeClassNames === null) { + $this->getExtendMaps($recursiveTypeMapper); + } + + if (!isset($this->mapClassToExtendTypeArray[$className])) { + throw CannotMapTypeException::createForExtendType($className, $type); + } + + foreach ($this->mapClassToExtendTypeArray[$className] as $extendedTypeClass) { + $this->typeGenerator->extendAnnotatedObject($this->container->get($extendedTypeClass), $type, $recursiveTypeMapper); + } + } + + /** + * Returns true if this type mapper can extend an existing type for the $typeName GraphQL type + * + * @param string $typeName + * @param MutableObjectType $type + * @return bool + */ + public function canExtendTypeForName(string $typeName, MutableObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): bool + { + $typeClassNames = $this->getExtendTypesFromCacheByGraphQLTypeName($typeName); + + if ($typeClassNames !== null) { + return true; + } + + /*$factory = $this->getFactoryFromCacheByGraphQLInputTypeName($typeName); + if ($factory !== null) { + return true; + }*/ + + $this->getExtendMaps($recursiveTypeMapper); + + return isset($this->mapNameToExtendType[$typeName])/* || isset($this->mapInputNameToFactory[$typeName])*/; + } + + /** + * Extends the existing GraphQL type that is mapped to the $typeName GraphQL type. + * + * @param string $typeName + * @param MutableObjectType $type + * @param RecursiveTypeMapperInterface $recursiveTypeMapper + * @throws CannotMapTypeExceptionInterface + */ + public function extendTypeForName(string $typeName, MutableObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): void + { + $extendTypeClassNames = $this->getExtendTypesFromCacheByGraphQLTypeName($typeName); + if ($extendTypeClassNames === null) { + /*$factory = $this->getFactoryFromCacheByGraphQLInputTypeName($typeName); + if ($factory === null) {*/ + $this->getExtendMaps($recursiveTypeMapper); + //} + } + + if (!isset($this->mapNameToExtendType[$typeName])) { + throw CannotMapTypeException::createForExtendName($typeName, $type); + } + + foreach ($this->mapNameToExtendType[$typeName] as $extendedTypeClass) { + $this->typeGenerator->extendAnnotatedObject($this->container->get($extendedTypeClass), $type, $recursiveTypeMapper); + } + + /*if (isset($this->mapInputNameToFactory[$typeName])) { + $factory = $this->mapInputNameToFactory[$typeName]; + return $this->inputTypeGenerator->mapFactoryMethod($this->container->get($factory[0]), $factory[1], $recursiveTypeMapper); + }*/ + } } diff --git a/src/Mappers/PorpaginasTypeMapper.php b/src/Mappers/PorpaginasTypeMapper.php index db39048..d96197b 100644 --- a/src/Mappers/PorpaginasTypeMapper.php +++ b/src/Mappers/PorpaginasTypeMapper.php @@ -15,11 +15,12 @@ use RuntimeException; use function strpos; use function substr; +use TheCodingMachine\GraphQL\Controllers\Types\MutableObjectType; class PorpaginasTypeMapper implements TypeMapperInterface { /** - * @var array + * @var array */ private $cache = []; @@ -40,10 +41,10 @@ public function canMapClassToType(string $className): bool * @param string $className The exact class name to look for (this function does not look into parent classes). * @param OutputType|null $subType An optional sub-type if the main class is an iterator that needs to be typed. * @param RecursiveTypeMapperInterface $recursiveTypeMapper - * @return ObjectType + * @return MutableObjectType * @throws CannotMapTypeExceptionInterface */ - public function mapClassToType(string $className, ?OutputType $subType, RecursiveTypeMapperInterface $recursiveTypeMapper): ObjectType + public function mapClassToType(string $className, ?OutputType $subType, RecursiveTypeMapperInterface $recursiveTypeMapper): MutableObjectType { if (!$this->canMapClassToType($className)) { throw CannotMapTypeException::createForType($className); @@ -55,7 +56,7 @@ public function mapClassToType(string $className, ?OutputType $subType, Recursiv return $this->getObjectType($subType); } - private function getObjectType(OutputType $subType): ObjectType + private function getObjectType(OutputType $subType): MutableObjectType { if (!isset($subType->name)) { throw new RuntimeException('Cannot get name property from sub type '.get_class($subType)); @@ -66,7 +67,7 @@ private function getObjectType(OutputType $subType): ObjectType $typeName = 'PorpaginasResult_'.$name; if (!isset($this->cache[$typeName])) { - $this->cache[$typeName] = new ObjectType([ + $this->cache[$typeName] = new MutableObjectType([ 'name' => $typeName, 'fields' => function() use ($subType) { return [ @@ -172,4 +173,56 @@ public function mapClassToInputType(string $className, RecursiveTypeMapperInterf { throw CannotMapTypeException::createForInputType($className); } + + /** + * Returns true if this type mapper can extend an existing type for the $className FQCN + * + * @param string $className + * @param MutableObjectType $type + * @param RecursiveTypeMapperInterface $recursiveTypeMapper + * @return bool + */ + public function canExtendTypeForClass(string $className, MutableObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): bool + { + return false; + } + + /** + * Extends the existing GraphQL type that is mapped to $className. + * + * @param string $className + * @param MutableObjectType $type + * @param RecursiveTypeMapperInterface $recursiveTypeMapper + * @throws CannotMapTypeExceptionInterface + */ + public function extendTypeForClass(string $className, MutableObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): void + { + throw CannotMapTypeException::createForExtendType($className, $type); + } + + /** + * Returns true if this type mapper can extend an existing type for the $typeName GraphQL type + * + * @param string $typeName + * @param MutableObjectType $type + * @param RecursiveTypeMapperInterface $recursiveTypeMapper + * @return bool + */ + public function canExtendTypeForName(string $typeName, MutableObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): bool + { + return false; + } + + /** + * Extends the existing GraphQL type that is mapped to the $typeName GraphQL type. + * + * @param string $typeName + * @param MutableObjectType $type + * @param RecursiveTypeMapperInterface $recursiveTypeMapper + * @throws CannotMapTypeExceptionInterface + */ + public function extendTypeForName(string $typeName, MutableObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): void + { + throw CannotMapTypeException::createForExtendName($typeName, $type); + } } diff --git a/src/Mappers/RecursiveTypeMapper.php b/src/Mappers/RecursiveTypeMapper.php index a9f72d4..0ce0989 100644 --- a/src/Mappers/RecursiveTypeMapper.php +++ b/src/Mappers/RecursiveTypeMapper.php @@ -5,6 +5,7 @@ use function array_flip; +use function array_reverse; use function get_parent_class; use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\InputType; @@ -14,7 +15,9 @@ use GraphQL\Type\Definition\Type; use Psr\SimpleCache\CacheInterface; use TheCodingMachine\GraphQL\Controllers\NamingStrategyInterface; +use TheCodingMachine\GraphQL\Controllers\TypeRegistry; use TheCodingMachine\GraphQL\Controllers\Types\InterfaceFromObjectType; +use TheCodingMachine\GraphQL\Controllers\Types\MutableObjectType; /** * This class wraps a TypeMapperInterface into a RecursiveTypeMapperInterface. @@ -62,12 +65,19 @@ class RecursiveTypeMapper implements RecursiveTypeMapperInterface */ private $interfaceToClassNameMap; - public function __construct(TypeMapperInterface $typeMapper, NamingStrategyInterface $namingStrategy, CacheInterface $cache, ?int $ttl = null) + /** + * @var TypeRegistry + */ + private $typeRegistry; + + + public function __construct(TypeMapperInterface $typeMapper, NamingStrategyInterface $namingStrategy, CacheInterface $cache, TypeRegistry $typeRegistry, ?int $ttl = null) { $this->typeMapper = $typeMapper; $this->namingStrategy = $namingStrategy; $this->cache = $cache; $this->ttl = $ttl; + $this->typeRegistry = $typeRegistry; } /** @@ -85,17 +95,36 @@ public function canMapClassToType(string $className): bool * Maps a PHP fully qualified class name to a GraphQL type. * * @param string $className The class name to look for (this function looks into parent classes if the class does not match a type) - * @param ObjectType|null $subType An optional sub-type if the main class is an iterator that needs to be typed. - * @return ObjectType + * @param (OutputType&MutableObjectType)|(OutputType&InterfaceType)|null $subType An optional sub-type if the main class is an iterator that needs to be typed. + * @return MutableObjectType * @throws CannotMapTypeExceptionInterface */ - public function mapClassToType(string $className, ?ObjectType $subType): ObjectType + public function mapClassToType(string $className, ?OutputType $subType): MutableObjectType { $closestClassName = $this->findClosestMatchingParent($className); if ($closestClassName === null) { throw CannotMapTypeException::createForType($className); } - return $this->typeMapper->mapClassToType($closestClassName, $subType, $this); + $type = $this->typeMapper->mapClassToType($closestClassName, $subType, $this); + + // In the event this type was already part of cache, let's not extend it. + if ($this->typeRegistry->hasType($type->name)) { + $cachedType = $this->typeRegistry->getType($type->name); + if ($cachedType !== $type) { + throw new \RuntimeException('Cached type in registry is not the type returned by type mapper.'); + } + //if ($cachedType->getStatus() === MutableObjectType::STATUS_FROZEN) { + return $type; + //} + } + + $this->typeRegistry->registerType($type); + + $this->extendType($className, $type); + + $type->freeze(); + + return $type; } /** @@ -114,12 +143,35 @@ private function findClosestMatchingParent(string $className): ?string return null; } + /** + * Extends a type using available type extenders. + * + * @param string $className + * @param MutableObjectType $type + * @throws CannotMapTypeExceptionInterface + */ + private function extendType(string $className, MutableObjectType $type): void + { + $classes = []; + do { + if ($this->typeMapper->canExtendTypeForClass($className, $type, $this)) { + $classes[] = $className; + } + } while ($className = get_parent_class($className)); + + // Let's apply extenders from the most basic type. + $classes = array_reverse($classes); + foreach ($classes as $class) { + $this->typeMapper->extendTypeForClass($class, $type, $this); + } + } + /** * Maps a PHP fully qualified class name to a GraphQL type. Returns an interface if possible (if the class * has children) or returns an output type otherwise. * * @param string $className The exact class name to look for (this function does not look into parent classes). - * @param OutputType|null $subType A subtype (if the main className is an iterator) + * @param (OutputType&ObjectType)|(OutputType&InterfaceType)|null $subType A subtype (if the main className is an iterator) * @return OutputType&Type * @throws CannotMapTypeExceptionInterface */ @@ -130,12 +182,13 @@ public function mapClassToInterfaceOrType(string $className, ?OutputType $subTyp throw CannotMapTypeException::createForType($className); } if (!isset($this->interfaces[$closestClassName])) { - $objectType = $this->typeMapper->mapClassToType($closestClassName, $subType, $this); + $objectType = $this->mapClassToType($className, $subType); $supportedClasses = $this->getClassTree(); if (isset($supportedClasses[$closestClassName]) && !empty($supportedClasses[$closestClassName]->getChildren())) { // Cast as an interface $this->interfaces[$closestClassName] = new InterfaceFromObjectType($this->namingStrategy->getInterfaceNameFromConcreteName($objectType->name), $objectType, $subType, $this); + $this->typeRegistry->registerType($this->interfaces[$closestClassName]); } else { $this->interfaces[$closestClassName] = $objectType; } @@ -154,7 +207,7 @@ private function buildInterfaceToClassNameMap(): array $supportedClasses = $this->getClassTree(); foreach ($supportedClasses as $className => $mappedClass) { if (!empty($mappedClass->getChildren())) { - $objectType = $this->typeMapper->mapClassToType($className, null, $this); + $objectType = $this->mapClassToType($className, null); $interfaceName = $this->namingStrategy->getInterfaceNameFromConcreteName($objectType->name); $map[$interfaceName] = $className; } @@ -275,7 +328,7 @@ public function getOutputTypes(): array { $types = []; foreach ($this->typeMapper->getSupportedClasses() as $supportedClass) { - $types[$supportedClass] = $this->typeMapper->mapClassToType($supportedClass, null, $this); + $types[$supportedClass] = $this->mapClassToType($supportedClass, null); } return $types; } @@ -310,8 +363,32 @@ public function canMapNameToType(string $typeName): bool */ public function mapNameToType(string $typeName): Type { + if ($this->typeRegistry->hasType($typeName)) { + return $this->typeRegistry->getType($typeName); + } if ($this->typeMapper->canMapNameToType($typeName)) { - return $this->typeMapper->mapNameToType($typeName, $this); + $type = $this->typeMapper->mapNameToType($typeName, $this); + + if ($this->typeRegistry->hasType($typeName)) { + $cachedType = $this->typeRegistry->getType($typeName); + if ($cachedType !== $type) { + throw new \RuntimeException('Cached type in registry is not the type returned by type mapper.'); + } + if ($cachedType instanceof MutableObjectType && $cachedType->getStatus() === MutableObjectType::STATUS_FROZEN) { + return $type; + } + } + + if (!$this->typeRegistry->hasType($typeName)) { + $this->typeRegistry->registerType($type); + } + if ($type instanceof MutableObjectType) { + if ($this->typeMapper->canExtendTypeForName($typeName, $type, $this)) { + $this->typeMapper->extendTypeForName($typeName, $type, $this); + } + $type->freeze(); + } + return $type; } // Maybe the type is an interface? diff --git a/src/Mappers/RecursiveTypeMapperInterface.php b/src/Mappers/RecursiveTypeMapperInterface.php index 313d89c..98a54c2 100644 --- a/src/Mappers/RecursiveTypeMapperInterface.php +++ b/src/Mappers/RecursiveTypeMapperInterface.php @@ -9,6 +9,7 @@ use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\OutputType; use GraphQL\Type\Definition\Type; +use TheCodingMachine\GraphQL\Controllers\Types\MutableObjectType; /** * Maps a PHP class to a GraphQL type. @@ -29,17 +30,17 @@ public function canMapClassToType(string $className): bool; * Maps a PHP fully qualified class name to a GraphQL type. * * @param string $className The class name to look for (this function looks into parent classes if the class does not match a type). - * @param ObjectType|null $subType An optional sub-type if the main class is an iterator that needs to be typed. - * @return ObjectType + * @param (OutputType&MutableObjectType)|(OutputType&InterfaceType)|null $subType An optional sub-type if the main class is an iterator that needs to be typed. + * @return MutableObjectType * @throws CannotMapTypeExceptionInterface */ - public function mapClassToType(string $className, ?ObjectType $subType): ObjectType; + public function mapClassToType(string $className, ?OutputType $subType): MutableObjectType; /** * Maps a PHP fully qualified class name to a GraphQL interface (or returns null if no interface is found). * * @param string $className The exact class name to look for (this function does not look into parent classes). - * @param OutputType|null $subType A subtype (if the main className is an iterator) + * @param (OutputType&ObjectType)|(OutputType&InterfaceType)|null $subType A subtype (if the main className is an iterator) * @return OutputType&Type * @throws CannotMapTypeExceptionInterface */ diff --git a/src/Mappers/StaticTypeMapper.php b/src/Mappers/StaticTypeMapper.php index c0a0265..3e951bd 100644 --- a/src/Mappers/StaticTypeMapper.php +++ b/src/Mappers/StaticTypeMapper.php @@ -9,6 +9,7 @@ use GraphQL\Type\Definition\OutputType; use GraphQL\Type\Definition\Type; use TheCodingMachine\GraphQL\Controllers\Mappers\Interfaces\InterfacesResolverInterface; +use TheCodingMachine\GraphQL\Controllers\Types\MutableObjectType; /** * A simple implementation of the TypeMapperInterface that expects mapping to be passed in a setter. @@ -18,14 +19,14 @@ final class StaticTypeMapper implements TypeMapperInterface { /** - * @var array + * @var array */ private $types = []; /** * An array mapping a fully qualified class name to the matching TypeInterface * - * @param array $types + * @param array $types */ public function setTypes(array $types): void { @@ -48,7 +49,7 @@ public function setInputTypes(array $inputTypes): void } /** - * @var array + * @var array */ private $notMappedTypes = []; @@ -83,10 +84,10 @@ public function canMapClassToType(string $className): bool * @param string $className * @param OutputType|null $subType * @param RecursiveTypeMapperInterface $recursiveTypeMapper - * @return ObjectType + * @return MutableObjectType * @throws CannotMapTypeExceptionInterface */ - public function mapClassToType(string $className, ?OutputType $subType, RecursiveTypeMapperInterface $recursiveTypeMapper): ObjectType + public function mapClassToType(string $className, ?OutputType $subType, RecursiveTypeMapperInterface $recursiveTypeMapper): MutableObjectType { // TODO: add support for $subType if ($subType !== null) { @@ -182,4 +183,56 @@ public function canMapNameToType(string $typeName): bool } return isset($this->notMappedTypes[$typeName]); } + + /** + * Returns true if this type mapper can extend an existing type for the $className FQCN + * + * @param string $className + * @param MutableObjectType $type + * @param RecursiveTypeMapperInterface $recursiveTypeMapper + * @return bool + */ + public function canExtendTypeForClass(string $className, MutableObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): bool + { + return false; + } + + /** + * Extends the existing GraphQL type that is mapped to $className. + * + * @param string $className + * @param MutableObjectType $type + * @param RecursiveTypeMapperInterface $recursiveTypeMapper + * @throws CannotMapTypeExceptionInterface + */ + public function extendTypeForClass(string $className, MutableObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): void + { + throw CannotMapTypeException::createForExtendType($className, $type); + } + + /** + * Returns true if this type mapper can extend an existing type for the $typeName GraphQL type + * + * @param string $typeName + * @param MutableObjectType $type + * @param RecursiveTypeMapperInterface $recursiveTypeMapper + * @return bool + */ + public function canExtendTypeForName(string $typeName, MutableObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): bool + { + return false; + } + + /** + * Extends the existing GraphQL type that is mapped to the $typeName GraphQL type. + * + * @param string $typeName + * @param MutableObjectType $type + * @param RecursiveTypeMapperInterface $recursiveTypeMapper + * @throws CannotMapTypeExceptionInterface + */ + public function extendTypeForName(string $typeName, MutableObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): void + { + throw CannotMapTypeException::createForExtendName($typeName, $type); + } } diff --git a/src/Mappers/TypeMapperInterface.php b/src/Mappers/TypeMapperInterface.php index 5ca3a68..959df47 100644 --- a/src/Mappers/TypeMapperInterface.php +++ b/src/Mappers/TypeMapperInterface.php @@ -9,6 +9,7 @@ use GraphQL\Type\Definition\OutputType; use GraphQL\Type\Definition\Type; use TheCodingMachine\GraphQL\Controllers\Mappers\Interfaces\InterfacesResolverInterface; +use TheCodingMachine\GraphQL\Controllers\Types\MutableObjectType; /** * Maps a PHP class to a GraphQL type @@ -29,10 +30,10 @@ public function canMapClassToType(string $className): bool; * @param string $className The exact class name to look for (this function does not look into parent classes). * @param OutputType|null $subType An optional sub-type if the main class is an iterator that needs to be typed. * @param RecursiveTypeMapperInterface $recursiveTypeMapper - * @return ObjectType + * @return MutableObjectType * @throws CannotMapTypeExceptionInterface */ - public function mapClassToType(string $className, ?OutputType $subType, RecursiveTypeMapperInterface $recursiveTypeMapper): ObjectType; + public function mapClassToType(string $className, ?OutputType $subType, RecursiveTypeMapperInterface $recursiveTypeMapper): MutableObjectType; /** * Returns true if this type mapper can map the $typeName GraphQL name to a GraphQL type. @@ -74,4 +75,44 @@ public function canMapClassToInputType(string $className): bool; * @return InputObjectType */ public function mapClassToInputType(string $className, RecursiveTypeMapperInterface $recursiveTypeMapper): InputObjectType; + + /** + * Returns true if this type mapper can extend an existing type for the $className FQCN + * + * @param string $className + * @param MutableObjectType $type + * @param RecursiveTypeMapperInterface $recursiveTypeMapper + * @return bool + */ + public function canExtendTypeForClass(string $className, MutableObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): bool; + + /** + * Extends the existing GraphQL type that is mapped to $className. + * + * @param string $className + * @param MutableObjectType $type + * @param RecursiveTypeMapperInterface $recursiveTypeMapper + * @throws CannotMapTypeExceptionInterface + */ + public function extendTypeForClass(string $className, MutableObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): void; + + /** + * Returns true if this type mapper can extend an existing type for the $typeName GraphQL type + * + * @param string $typeName + * @param MutableObjectType $type + * @param RecursiveTypeMapperInterface $recursiveTypeMapper + * @return bool + */ + public function canExtendTypeForName(string $typeName, MutableObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): bool; + + /** + * Extends the existing GraphQL type that is mapped to the $typeName GraphQL type. + * + * @param string $typeName + * @param MutableObjectType $type + * @param RecursiveTypeMapperInterface $recursiveTypeMapper + * @throws CannotMapTypeExceptionInterface + */ + public function extendTypeForName(string $typeName, MutableObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): void; } diff --git a/src/MissingAnnotationException.php b/src/MissingAnnotationException.php index e17c3de..73e36f2 100644 --- a/src/MissingAnnotationException.php +++ b/src/MissingAnnotationException.php @@ -15,4 +15,9 @@ public static function missingTypeException(): self { return new self('GraphQL type classes must provide a @Type annotation.'); } + + public static function missingExtendTypeException(): self + { + return new self('Expected a @ExtendType annotation.'); + } } diff --git a/src/Schema/DescriptorInterface.php b/src/Schema/DescriptorInterface.php new file mode 100644 index 0000000..4628288 --- /dev/null +++ b/src/Schema/DescriptorInterface.php @@ -0,0 +1,17 @@ + + */ + private $fields; + /** + * @var array + */ + private $interfaces; + + /** + * @param string $name + * @param string $mappedClass + */ + public function __construct(string $name, string $mappedClass, callable $fieldsBuilder, callable $interfacesBuilder) + { + $this->name = $name; + $this->mappedClass = $mappedClass; + $this->fieldsBuilder = $fieldsBuilder; + $this->interfacesBuilder = $interfacesBuilder; + } + + + /** + * Returns the GraphQL name of this type. + * + * @return string + */ + public function getName(): string + { + return $this->name; + } + + /** + * Returns the fully qualified PHP class name we are mapping to. + * + * @return string + */ + public function getClass(): string + { + return $this->mappedClass; + } + + /** + * Returns an array of interface descriptors, mapped by name. + * + * @return array + */ + public function getInterfaces(): array + { + if ($this->interfaces === null) { + $InterfaceBuilder = $this->interfacesBuilder; + $this->interfaces = $InterfaceBuilder(); + } + return $this->interfaces; + } + + /** + * Returns an array of field descriptors, mapped by name. + * + * @return array + */ + public function getFields(): array + { + if ($this->fields === null) { + $fieldsBuilder = $this->fieldsBuilder; + $this->fields = $fieldsBuilder(); + } + return $this->fields; + } + + /** + * @param callable $fieldsBuilder + */ + public function addFields(callable $fieldsBuilder) + { + $oldFieldsBuilder = $this->fieldsBuilder; + $this->fieldsBuilder = function() use ($oldFieldsBuilder, $fieldsBuilder) { + return $oldFieldsBuilder() + $fieldsBuilder(); + }; + } + + /** + * Returns the name of the factory capable of building this descriptor. + * + * @return string + */ + public function getFactory(): string + { + return 'object_type'; + } +} diff --git a/src/Schema/ObjectType/ObjectTypeDescriptorInterface.php b/src/Schema/ObjectType/ObjectTypeDescriptorInterface.php new file mode 100644 index 0000000..2004abf --- /dev/null +++ b/src/Schema/ObjectType/ObjectTypeDescriptorInterface.php @@ -0,0 +1,38 @@ + + */ + public function getInterfaces(): array; + + /** + * Returns an array of field descriptors, mapped by name. + * + * @return array + */ + public function getFields(): array; +} diff --git a/src/TypeGenerator.php b/src/TypeGenerator.php index 1f8cdb6..ad5bcb9 100644 --- a/src/TypeGenerator.php +++ b/src/TypeGenerator.php @@ -8,6 +8,8 @@ use ReflectionClass; use TheCodingMachine\GraphQL\Controllers\Annotations\Type; use TheCodingMachine\GraphQL\Controllers\Mappers\RecursiveTypeMapperInterface; +use TheCodingMachine\GraphQL\Controllers\Types\MutableObjectType; +use TheCodingMachine\GraphQL\Controllers\Types\TypeAnnotatedObjectType; /** * This class is in charge of creating Webonix GraphQL types from annotated objects that do not extend the @@ -22,32 +24,34 @@ class TypeGenerator /** * @var FieldsBuilderFactory */ - private $controllerQueryProviderFactory; - /** - * @var array - */ - private $cache = []; + private $fieldsBuilderFactory; /** * @var NamingStrategyInterface */ private $namingStrategy; + /** + * @var TypeRegistry + */ + private $typeRegistry; public function __construct(AnnotationReader $annotationReader, - FieldsBuilderFactory $controllerQueryProviderFactory, - NamingStrategyInterface $namingStrategy) + FieldsBuilderFactory $fieldsBuilderFactory, + NamingStrategyInterface $namingStrategy, + TypeRegistry $typeRegistry) { $this->annotationReader = $annotationReader; - $this->controllerQueryProviderFactory = $controllerQueryProviderFactory; + $this->fieldsBuilderFactory = $fieldsBuilderFactory; $this->namingStrategy = $namingStrategy; + $this->typeRegistry = $typeRegistry; } /** * @param object $annotatedObject An object with a Type annotation. * @param RecursiveTypeMapperInterface $recursiveTypeMapper - * @return ObjectType + * @return MutableObjectType * @throws \ReflectionException */ - public function mapAnnotatedObject($annotatedObject, RecursiveTypeMapperInterface $recursiveTypeMapper): ObjectType + public function mapAnnotatedObject($annotatedObject, RecursiveTypeMapperInterface $recursiveTypeMapper): MutableObjectType { $refTypeClass = new \ReflectionClass($annotatedObject); @@ -59,31 +63,103 @@ public function mapAnnotatedObject($annotatedObject, RecursiveTypeMapperInterfac $typeName = $this->namingStrategy->getOutputTypeName($refTypeClass->getName(), $typeField); - if (!isset($this->cache[$typeName])) { - $this->cache[$typeName] = new ObjectType([ - 'name' => $typeName, - 'fields' => function() use ($annotatedObject, $recursiveTypeMapper, $typeField) { - $parentClass = get_parent_class($typeField->getClass()); - $parentType = null; - if ($parentClass !== false) { - if ($recursiveTypeMapper->canMapClassToType($parentClass)) { - $parentType = $recursiveTypeMapper->mapClassToType($parentClass, null); - } - } + if ($this->typeRegistry->hasType($typeName)) { + return $this->typeRegistry->getMutableObjectType($typeName); + } + + return TypeAnnotatedObjectType::createFromAnnotatedClass($typeName, $typeField->getClass(), $annotatedObject, $this->fieldsBuilderFactory, $recursiveTypeMapper); - $fieldProvider = $this->controllerQueryProviderFactory->buildFieldsBuilder($recursiveTypeMapper); - $fields = $fieldProvider->getFields($annotatedObject); - if ($parentType !== null) { - $fields = $parentType->getFields() + $fields; + /*return new ObjectType([ + 'name' => $typeName, + 'fields' => function() use ($annotatedObject, $recursiveTypeMapper, $typeField) { + $parentClass = get_parent_class($typeField->getClass()); + $parentType = null; + if ($parentClass !== false) { + if ($recursiveTypeMapper->canMapClassToType($parentClass)) { + $parentType = $recursiveTypeMapper->mapClassToType($parentClass, null); } - return $fields; - }, - 'interfaces' => function() use ($typeField, $recursiveTypeMapper) { - return $recursiveTypeMapper->findInterfaces($typeField->getClass()); } - ]); + + $fieldProvider = $this->controllerQueryProviderFactory->buildFieldsBuilder($recursiveTypeMapper); + $fields = $fieldProvider->getFields($annotatedObject); + if ($parentType !== null) { + $fields = $parentType->getFields() + $fields; + } + return $fields; + }, + 'interfaces' => function() use ($typeField, $recursiveTypeMapper) { + return $recursiveTypeMapper->findInterfaces($typeField->getClass()); + } + ]);*/ + } + + /** + * @param object $annotatedObject An object with a ExtendType annotation. + * @param MutableObjectType $type + * @param RecursiveTypeMapperInterface $recursiveTypeMapper + */ + public function extendAnnotatedObject($annotatedObject, MutableObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper) + { + $refTypeClass = new \ReflectionClass($annotatedObject); + + $extendTypeAnnotation = $this->annotationReader->getExtendTypeAnnotation($refTypeClass); + + if ($extendTypeAnnotation === null) { + throw MissingAnnotationException::missingExtendTypeException(); } - return $this->cache[$typeName]; + //$typeName = $this->namingStrategy->getOutputTypeName($refTypeClass->getName(), $extendTypeAnnotation); + $typeName = $type->name; + + /*if ($this->typeRegistry->hasType($typeName)) { + throw new GraphQLException(sprintf('Tried to extend GraphQL type "%s" that is already stored in the type registry.', $typeName)); + } + + if (!$type instanceof MutableObjectType) { + throw new \RuntimeException('TEMP EXCEPTION'); + }*/ + + $type->addFields(function() use ($annotatedObject, $recursiveTypeMapper) { + /*$parentClass = get_parent_class($extendTypeAnnotation->getClass()); + $parentType = null; + if ($parentClass !== false) { + if ($recursiveTypeMapper->canMapClassToType($parentClass)) { + $parentType = $recursiveTypeMapper->mapClassToType($parentClass, null); + } + }*/ + + $fieldProvider = $this->fieldsBuilderFactory->buildFieldsBuilder($recursiveTypeMapper); + return $fieldProvider->getFields($annotatedObject); + /*if ($parentType !== null) { + $fields = $parentType->getFields() + $fields; + }*/ + }); + + +// return new ObjectType([ +// 'name' => $typeName, +// 'fields' => function() use ($annotatedObject, $recursiveTypeMapper, $type) { +// /*$parentClass = get_parent_class($extendTypeAnnotation->getClass()); +// $parentType = null; +// if ($parentClass !== false) { +// if ($recursiveTypeMapper->canMapClassToType($parentClass)) { +// $parentType = $recursiveTypeMapper->mapClassToType($parentClass, null); +// } +// }*/ +// +// $fieldProvider = $this->fieldsBuilderFactory->buildFieldsBuilder($recursiveTypeMapper); +// $fields = $fieldProvider->getFields($annotatedObject); +// /*if ($parentType !== null) { +// $fields = $parentType->getFields() + $fields; +// }*/ +// +// $fields = $type->getFields() + $fields; +// +// return $fields; +// }, +// 'interfaces' => function() use ($type) { +// return $type->getInterfaces(); +// } +// ]); } } diff --git a/src/TypeRegistry.php b/src/TypeRegistry.php new file mode 100644 index 0000000..5012813 --- /dev/null +++ b/src/TypeRegistry.php @@ -0,0 +1,63 @@ + + */ + private $outputTypes = []; + + /** + * Registers a type. + * IMPORTANT: the type MUST be fully computed (so ExtendType annotations must have ALREADY been applied to the tag) + * ONLY THE RecursiveTypeMapper IS ALLOWED TO CALL THIS METHOD. + * + * @param NamedType&Type&(ObjectType|InterfaceType) $type + */ + public function registerType(NamedType $type): void + { + if (isset($this->outputTypes[$type->name])) { + throw new GraphQLException('Type "'.$type->name.'" is already registered'); + } + $this->outputTypes[$type->name] = $type; + } + + public function hasType(string $typeName): bool + { + return isset($this->outputTypes[$typeName]); + } + + /** + * @param string $typeName + * @return NamedType&Type&(ObjectType|InterfaceType) + */ + public function getType(string $typeName): NamedType + { + if (!isset($this->outputTypes[$typeName])) { + throw new GraphQLException('Could not find type "'.$typeName.'" in registry'); + } + return $this->outputTypes[$typeName]; + } + + public function getMutableObjectType(string $typeName): MutableObjectType + { + $type = $this->getType($typeName); + if (!$type instanceof MutableObjectType) { + throw new GraphQLException('Expected GraphQL type "'.$typeName.'" to be an MutableObjectType. Got a '.get_class($type)); + } + return $type; + } +} diff --git a/src/Types/MutableObjectType.php b/src/Types/MutableObjectType.php new file mode 100644 index 0000000..89a014c --- /dev/null +++ b/src/Types/MutableObjectType.php @@ -0,0 +1,110 @@ + + */ + private $fieldsCallables = []; + + /** + * @var FieldDefinition[]|null + */ + private $finalFields; + + public function __construct(array $config) + { + $this->status = self::STATUS_PENDING; + + parent::__construct($config); + } + + public function freeze(): void + { + $this->status = self::STATUS_FROZEN; + } + + public function getStatus(): string + { + return $this->status; + } + + public function addFields(callable $fields): void + { + if ($this->status !== self::STATUS_PENDING) { + throw new \RuntimeException('Tried to add fields to a frozen MutableObjectType.'); + } + $this->fieldsCallables[] = $fields; + } + + /** + * @param string $name + * + * @return FieldDefinition + * + * @throws Exception + */ + public function getField($name): FieldDefinition + { + if ($this->status === self::STATUS_PENDING) { + throw new \RuntimeException('You must freeze() a MutableObjectType before fetching its fields.'); + } + return parent::getField($name); + } + + /** + * @param string $name + * + * @return bool + */ + public function hasField($name): bool + { + if ($this->status === self::STATUS_PENDING) { + throw new \RuntimeException('You must freeze() a MutableObjectType before fetching its fields.'); + } + return parent::hasField($name); + } + + /** + * @return FieldDefinition[] + * + * @throws InvariantViolation + */ + public function getFields(): array + { + if ($this->finalFields === null) { + if ($this->status === self::STATUS_PENDING) { + throw new \RuntimeException('You must freeze() a MutableObjectType before fetching its fields.'); + } + + $this->finalFields = parent::getFields(); + foreach ($this->fieldsCallables as $fieldsCallable) { + $this->finalFields += $fieldsCallable(); + } + } + + return $this->finalFields; + } +} diff --git a/src/Types/TypeAnnotatedObjectType.php b/src/Types/TypeAnnotatedObjectType.php new file mode 100644 index 0000000..9484583 --- /dev/null +++ b/src/Types/TypeAnnotatedObjectType.php @@ -0,0 +1,58 @@ +className = $className; + + parent::__construct($config); + } + + public static function createFromAnnotatedClass(string $typeName, string $className, $annotatedObject, FieldsBuilderFactory $fieldsBuilderFactory, RecursiveTypeMapperInterface $recursiveTypeMapper): self + { + return new self($className, [ + 'name' => $typeName, + 'fields' => function() use ($annotatedObject, $recursiveTypeMapper, $className, $fieldsBuilderFactory) { + $parentClass = get_parent_class($className); + $parentType = null; + if ($parentClass !== false) { + if ($recursiveTypeMapper->canMapClassToType($parentClass)) { + $parentType = $recursiveTypeMapper->mapClassToType($parentClass, null); + } + } + + $fieldProvider = $fieldsBuilderFactory->buildFieldsBuilder($recursiveTypeMapper); + $fields = $fieldProvider->getFields($annotatedObject); + if ($parentType !== null) { + $fields = $parentType->getFields() + $fields; + } + return $fields; + }, + 'interfaces' => function() use ($className, $recursiveTypeMapper) { + return $recursiveTypeMapper->findInterfaces($className); + } + ]); + } + + public function getMappedClassName(): string + { + return $this->className; + } +} diff --git a/tests/AbstractQueryProviderTest.php b/tests/AbstractQueryProviderTest.php index 572e830..90a38ee 100644 --- a/tests/AbstractQueryProviderTest.php +++ b/tests/AbstractQueryProviderTest.php @@ -21,6 +21,7 @@ use TheCodingMachine\GraphQL\Controllers\Fixtures\Types\TestFactory; use TheCodingMachine\GraphQL\Controllers\Hydrators\HydratorInterface; use TheCodingMachine\GraphQL\Controllers\Mappers\CannotMapTypeException; +use TheCodingMachine\GraphQL\Controllers\Mappers\CannotMapTypeExceptionInterface; use TheCodingMachine\GraphQL\Controllers\Mappers\RecursiveTypeMapper; use TheCodingMachine\GraphQL\Controllers\Mappers\RecursiveTypeMapperInterface; use TheCodingMachine\GraphQL\Controllers\Mappers\TypeMapperInterface; @@ -29,6 +30,7 @@ use TheCodingMachine\GraphQL\Controllers\Reflection\CachedDocBlockFactory; use TheCodingMachine\GraphQL\Controllers\Security\VoidAuthenticationService; use TheCodingMachine\GraphQL\Controllers\Security\VoidAuthorizationService; +use TheCodingMachine\GraphQL\Controllers\Types\MutableObjectType; use TheCodingMachine\GraphQL\Controllers\Types\ResolvableInputObjectType; use TheCodingMachine\GraphQL\Controllers\Types\TypeResolver; @@ -47,11 +49,12 @@ abstract class AbstractQueryProviderTest extends TestCase private $controllerQueryProviderFactory; private $annotationReader; private $typeResolver; + private $typeRegistry; - protected function getTestObjectType() + protected function getTestObjectType(): MutableObjectType { if ($this->testObjectType === null) { - $this->testObjectType = new ObjectType([ + $this->testObjectType = new MutableObjectType([ 'name' => 'TestObject', 'fields' => [ 'test' => Type::string(), @@ -61,10 +64,10 @@ protected function getTestObjectType() return $this->testObjectType; } - protected function getTestObjectType2() + protected function getTestObjectType2(): MutableObjectType { if ($this->testObjectType2 === null) { - $this->testObjectType2 = new ObjectType([ + $this->testObjectType2 = new MutableObjectType([ 'name' => 'TestObject2', 'fields' => [ 'test' => Type::string(), @@ -74,7 +77,7 @@ protected function getTestObjectType2() return $this->testObjectType2; } - protected function getInputTestObjectType() + protected function getInputTestObjectType(): InputObjectType { if ($this->inputTestObjectType === null) { $this->inputTestObjectType = new InputObjectType([ @@ -124,7 +127,7 @@ public function __construct(ObjectType $testObjectType, ObjectType $testObjectTy //$this->inputTestObjectType2 = $inputTestObjectType2; } - public function mapClassToType(string $className, ?OutputType $subType, RecursiveTypeMapperInterface $recursiveTypeMapper): ObjectType + public function mapClassToType(string $className, ?OutputType $subType, RecursiveTypeMapperInterface $recursiveTypeMapper): MutableObjectType { if ($className === TestObject::class) { return $this->testObjectType; @@ -151,34 +154,16 @@ public function canMapClassToType(string $className): bool return $className === TestObject::class || $className === TestObject2::class; } - /** - * Returns true if this type mapper can map the $className FQCN to a GraphQL input type. - * - * @param string $className - * @return bool - */ public function canMapClassToInputType(string $className): bool { return $className === TestObject::class || $className === TestObject2::class; } - /** - * Returns the list of classes that have matching input GraphQL types. - * - * @return string[] - */ public function getSupportedClasses(): array { return [TestObject::class, TestObject2::class]; } - /** - * Returns a GraphQL type by name (can be either an input or output type) - * - * @param string $typeName The name of the GraphQL type - * @return Type&(InputType|OutputType) - * @throws CannotMapTypeException - */ public function mapNameToType(string $typeName, RecursiveTypeMapperInterface $recursiveTypeMapper): Type { switch ($typeName) { @@ -193,17 +178,31 @@ public function mapNameToType(string $typeName, RecursiveTypeMapperInterface $re } } - /** - * Returns true if this type mapper can map the $typeName GraphQL name to a GraphQL type. - * - * @param string $typeName The name of the GraphQL type - * @return bool - */ public function canMapNameToType(string $typeName): bool { return $typeName === 'TestObject' || $typeName === 'TestObject2' || $typeName === 'TestObjectInput'; } - }, new NamingStrategy(), new ArrayCache()); + + public function canExtendTypeForClass(string $className, MutableObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): bool + { + return false; + } + + public function extendTypeForClass(string $className, MutableObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): void + { + throw CannotMapTypeException::createForExtendType($className, $type); + } + + public function canExtendTypeForName(string $typeName, MutableObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): bool + { + return false; + } + + public function extendTypeForName(string $typeName, MutableObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): void + { + throw CannotMapTypeException::createForExtendName($typeName, $type); + } + }, new NamingStrategy(), new ArrayCache(), $this->getTypeRegistry()); } return $this->typeMapper; } @@ -267,7 +266,7 @@ protected function buildFieldsBuilder(): FieldsBuilder protected function getTypeGenerator(): TypeGenerator { if ($this->typeGenerator === null) { - $this->typeGenerator = new TypeGenerator($this->getAnnotationReader(), $this->getControllerQueryProviderFactory(), new NamingStrategy()); + $this->typeGenerator = new TypeGenerator($this->getAnnotationReader(), $this->getControllerQueryProviderFactory(), new NamingStrategy(), $this->getTypeRegistry()); } return $this->typeGenerator; } @@ -309,4 +308,12 @@ protected function getControllerQueryProviderFactory(): FieldsBuilderFactory } return $this->controllerQueryProviderFactory; } + + protected function getTypeRegistry(): TypeRegistry + { + if ($this->typeRegistry === null) { + $this->typeRegistry = new TypeRegistry(); + } + return $this->typeRegistry; + } } diff --git a/tests/Cache/FileModificationTimeCacheValidatorTraitTest.php b/tests/Cache/FileModificationTimeCacheValidatorTraitTest.php new file mode 100644 index 0000000..8751d9f --- /dev/null +++ b/tests/Cache/FileModificationTimeCacheValidatorTraitTest.php @@ -0,0 +1,34 @@ +addTrackedFile($file); + + $this->assertTrue($cacheItem->isValid()); + + touch($file, strtotime('2019-01-02')); + clearstatcache($file); + $this->assertFalse($cacheItem->isValid()); + unlink($file); + } +} diff --git a/tests/Fixtures/Integration/Types/ExtendedContactType.php b/tests/Fixtures/Integration/Types/ExtendedContactType.php new file mode 100644 index 0000000..8446b5d --- /dev/null +++ b/tests/Fixtures/Integration/Types/ExtendedContactType.php @@ -0,0 +1,25 @@ +getName()); + } +} \ No newline at end of file diff --git a/tests/Fixtures/Types/FooExtendType.php b/tests/Fixtures/Types/FooExtendType.php new file mode 100644 index 0000000..f7cc434 --- /dev/null +++ b/tests/Fixtures/Types/FooExtendType.php @@ -0,0 +1,25 @@ +getTest()); + } +} diff --git a/tests/Integration/EndToEndTest.php b/tests/Integration/EndToEndTest.php index b87c4a6..820f7a5 100644 --- a/tests/Integration/EndToEndTest.php +++ b/tests/Integration/EndToEndTest.php @@ -42,6 +42,7 @@ use TheCodingMachine\GraphQL\Controllers\Security\VoidAuthenticationService; use TheCodingMachine\GraphQL\Controllers\Security\VoidAuthorizationService; use TheCodingMachine\GraphQL\Controllers\TypeGenerator; +use TheCodingMachine\GraphQL\Controllers\TypeRegistry; use TheCodingMachine\GraphQL\Controllers\Types\TypeResolver; use function var_export; @@ -85,7 +86,12 @@ public function setUp() return new VoidAuthenticationService(); }, RecursiveTypeMapperInterface::class => function(ContainerInterface $container) { - return new RecursiveTypeMapper($container->get(TypeMapperInterface::class), $container->get(NamingStrategyInterface::class), new ArrayCache()); + return new RecursiveTypeMapper( + $container->get(TypeMapperInterface::class), + $container->get(NamingStrategyInterface::class), + new ArrayCache(), + $container->get(TypeRegistry::class) + ); }, TypeMapperInterface::class => function(ContainerInterface $container) { return new CompositeTypeMapper([ @@ -111,9 +117,13 @@ public function setUp() return new TypeGenerator( $container->get(AnnotationReader::class), $container->get(FieldsBuilderFactory::class), - $container->get(NamingStrategyInterface::class) + $container->get(NamingStrategyInterface::class), + $container->get(TypeRegistry::class) ); }, + TypeRegistry::class => function() { + return new TypeRegistry(); + }, InputTypeGenerator::class => function(ContainerInterface $container) { return new InputTypeGenerator( $container->get(InputTypeUtils::class), @@ -157,6 +167,7 @@ public function testEndToEnd() query { getContacts { name + uppercaseName ... on User { email } @@ -172,10 +183,12 @@ public function testEndToEnd() $this->assertSame([ 'getContacts' => [ [ - 'name' => 'Joe' + 'name' => 'Joe', + 'uppercaseName' => 'JOE' ], [ 'name' => 'Bill', + 'uppercaseName' => 'BILL', 'email' => 'bill@example.com' ] @@ -191,10 +204,12 @@ public function testEndToEnd() $this->assertSame([ 'getContacts' => [ [ - 'name' => 'Joe' + 'name' => 'Joe', + 'uppercaseName' => 'JOE' ], [ 'name' => 'Bill', + 'uppercaseName' => 'BILL', 'email' => 'bill@example.com' ] @@ -258,13 +273,12 @@ public function testEndToEndPorpaginas() */ $schema = $this->mainContainer->get(Schema::class); - $schema->assertValid(); - $queryString = ' query { getContactsIterator { items(limit: 1, offset: 1) { name + uppercaseName ... on User { email } @@ -284,6 +298,7 @@ public function testEndToEndPorpaginas() 'items' => [ [ 'name' => 'Bill', + 'uppercaseName' => 'BILL', 'email' => 'bill@example.com' ] ], @@ -302,6 +317,7 @@ public function testEndToEndPorpaginas() 'items' => [ [ 'name' => 'Bill', + 'uppercaseName' => 'BILL', 'email' => 'bill@example.com' ] ], @@ -356,7 +372,7 @@ public function testEndToEndPorpaginas() 'getContactsIterator' => [ 'items' => [ [ - 'name' => 'Joe' + 'name' => 'Joe', ], [ 'name' => 'Bill', diff --git a/tests/Mappers/CompositeTypeMapperTest.php b/tests/Mappers/CompositeTypeMapperTest.php index d8f8c3b..e77739b 100644 --- a/tests/Mappers/CompositeTypeMapperTest.php +++ b/tests/Mappers/CompositeTypeMapperTest.php @@ -11,6 +11,7 @@ use TheCodingMachine\GraphQL\Controllers\TypeMappingException; use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\ObjectType; +use TheCodingMachine\GraphQL\Controllers\Types\MutableObjectType; class CompositeTypeMapperTest extends AbstractQueryProviderTest { @@ -22,10 +23,10 @@ class CompositeTypeMapperTest extends AbstractQueryProviderTest public function setUp() { $typeMapper1 = new class() implements TypeMapperInterface { - public function mapClassToType(string $className, ?OutputType $subType, RecursiveTypeMapperInterface $recursiveTypeMapper): ObjectType + public function mapClassToType(string $className, ?OutputType $subType, RecursiveTypeMapperInterface $recursiveTypeMapper): MutableObjectType { if ($className === TestObject::class) { - return new ObjectType([ + return new MutableObjectType([ 'name' => 'TestObject', 'fields' => [ 'test' => Type::string(), @@ -102,6 +103,26 @@ public function canMapNameToType(string $typeName): bool { return $typeName === 'TestObject'; } + + public function canExtendTypeForClass(string $className, MutableObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): bool + { + return false; + } + + public function extendTypeForClass(string $className, MutableObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): void + { + throw CannotMapTypeException::createForExtendType($className, $type); + } + + public function canExtendTypeForName(string $typeName, MutableObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): bool + { + return false; + } + + public function extendTypeForName(string $typeName, MutableObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): void + { + throw CannotMapTypeException::createForExtendName($typeName, $type); + } }; $this->composite = new CompositeTypeMapper([$typeMapper1]); diff --git a/tests/Mappers/GlobTypeMapperTest.php b/tests/Mappers/GlobTypeMapperTest.php index 340f9f3..5478c00 100644 --- a/tests/Mappers/GlobTypeMapperTest.php +++ b/tests/Mappers/GlobTypeMapperTest.php @@ -11,6 +11,7 @@ use TheCodingMachine\GraphQL\Controllers\Annotations\Exceptions\ClassNotFoundException; use TheCodingMachine\GraphQL\Controllers\Fixtures\TestObject; use TheCodingMachine\GraphQL\Controllers\Fixtures\TestType; +use TheCodingMachine\GraphQL\Controllers\Fixtures\Types\FooExtendType; use TheCodingMachine\GraphQL\Controllers\Fixtures\Types\FooType; use TheCodingMachine\GraphQL\Controllers\Fixtures\Types\TestFactory; use TheCodingMachine\GraphQL\Controllers\NamingStrategy; @@ -149,4 +150,39 @@ public function testGlobTypeMapperInputType() $this->expectException(CannotMapTypeException::class); $mapper->mapClassToInputType(TestType::class, $this->getTypeMapper()); } + + public function testGlobTypeMapperExtend() + { + $container = new Picotainer([ + FooType::class => function() { + return new FooType(); + }, + FooExtendType::class => function() { + return new FooExtendType(); + } + ]); + + $typeGenerator = $this->getTypeGenerator(); + $inputTypeGenerator = $this->getInputTypeGenerator(); + + $cache = new ArrayCache(); + + $mapper = new GlobTypeMapper('TheCodingMachine\GraphQL\Controllers\Fixtures\Types', $typeGenerator, $inputTypeGenerator, $this->getInputTypeUtils(), $container, new \TheCodingMachine\GraphQL\Controllers\AnnotationReader(new AnnotationReader()), new NamingStrategy(), $cache); + + $type = $mapper->mapClassToType(TestObject::class, null, $this->getTypeMapper()); + + $this->assertTrue($mapper->canExtendTypeForClass(TestObject::class, $type, $this->getTypeMapper())); + $mapper->extendTypeForClass(TestObject::class, $type, $this->getTypeMapper()); + $mapper->extendTypeForName('TestObject', $type, $this->getTypeMapper()); + $this->assertTrue($mapper->canExtendTypeForName('TestObject', $type, $this->getTypeMapper())); + $this->assertFalse($mapper->canExtendTypeForName('NotExists', $type, $this->getTypeMapper())); + + // Again to test cache + $anotherMapperSameCache = new GlobTypeMapper('TheCodingMachine\GraphQL\Controllers\Fixtures\Types', $typeGenerator, $this->getInputTypeGenerator(), $this->getInputTypeUtils(), $container, new \TheCodingMachine\GraphQL\Controllers\AnnotationReader(new AnnotationReader()), new NamingStrategy(), $cache); + $this->assertTrue($anotherMapperSameCache->canExtendTypeForClass(TestObject::class, $type, $this->getTypeMapper())); + $this->assertTrue($anotherMapperSameCache->canExtendTypeForName('TestObject', $type, $this->getTypeMapper())); + + $this->expectException(CannotMapTypeException::class); + $mapper->extendTypeForClass(\stdClass::class, $type, $this->getTypeMapper()); + } } diff --git a/tests/Mappers/RecursiveTypeMapperTest.php b/tests/Mappers/RecursiveTypeMapperTest.php index 3126f0d..22a8259 100644 --- a/tests/Mappers/RecursiveTypeMapperTest.php +++ b/tests/Mappers/RecursiveTypeMapperTest.php @@ -18,13 +18,14 @@ use TheCodingMachine\GraphQL\Controllers\Fixtures\TestObject; use TheCodingMachine\GraphQL\Controllers\NamingStrategy; use TheCodingMachine\GraphQL\Controllers\TypeGenerator; +use TheCodingMachine\GraphQL\Controllers\Types\MutableObjectType; class RecursiveTypeMapperTest extends AbstractQueryProviderTest { public function testMapClassToType() { - $objectType = new ObjectType([ + $objectType = new MutableObjectType([ 'name' => 'Foobar' ]); @@ -33,7 +34,7 @@ public function testMapClassToType() ClassB::class => $objectType ]); - $recursiveTypeMapper = new RecursiveTypeMapper($typeMapper, new NamingStrategy(), new ArrayCache()); + $recursiveTypeMapper = new RecursiveTypeMapper($typeMapper, new NamingStrategy(), new ArrayCache(), $this->getTypeRegistry()); $this->assertFalse($typeMapper->canMapClassToType(ClassC::class)); $this->assertTrue($recursiveTypeMapper->canMapClassToType(ClassC::class)); @@ -46,7 +47,7 @@ public function testMapClassToType() public function testMapNameToType() { - $objectType = new ObjectType([ + $objectType = new MutableObjectType([ 'name' => 'Foobar' ]); @@ -55,7 +56,7 @@ public function testMapNameToType() ClassB::class => $objectType ]); - $recursiveTypeMapper = new RecursiveTypeMapper($typeMapper, new NamingStrategy(), new ArrayCache()); + $recursiveTypeMapper = new RecursiveTypeMapper($typeMapper, new NamingStrategy(), new ArrayCache(), $this->getTypeRegistry()); $this->assertTrue($recursiveTypeMapper->canMapNameToType('Foobar')); $this->assertSame($objectType, $recursiveTypeMapper->mapNameToType('Foobar')); @@ -88,7 +89,7 @@ public function testMapClassToInputType() ClassB::class => $inputObjectType ]); - $recursiveTypeMapper = new RecursiveTypeMapper($typeMapper, new NamingStrategy(), new ArrayCache()); + $recursiveTypeMapper = new RecursiveTypeMapper($typeMapper, new NamingStrategy(), new ArrayCache(), $this->getTypeRegistry()); $this->assertFalse($recursiveTypeMapper->canMapClassToInputType(ClassC::class)); @@ -109,11 +110,11 @@ protected function getTypeMapper() $namingStrategy = new NamingStrategy(); - $typeGenerator = new TypeGenerator($this->getAnnotationReader(), $this->getControllerQueryProviderFactory(), $namingStrategy); + $typeGenerator = new TypeGenerator($this->getAnnotationReader(), $this->getControllerQueryProviderFactory(), $namingStrategy, $this->getTypeRegistry()); $mapper = new GlobTypeMapper('TheCodingMachine\GraphQL\Controllers\Fixtures\Interfaces\Types', $typeGenerator, $this->getInputTypeGenerator(), $this->getInputTypeUtils(), $container, new \TheCodingMachine\GraphQL\Controllers\AnnotationReader(new AnnotationReader()), $namingStrategy, new NullCache()); - return new RecursiveTypeMapper($mapper, new NamingStrategy(), new ArrayCache()); + return new RecursiveTypeMapper($mapper, new NamingStrategy(), new ArrayCache(), $this->getTypeRegistry()); } public function testMapClassToInterfaceOrType() diff --git a/tests/Mappers/StaticTypeMapperTest.php b/tests/Mappers/StaticTypeMapperTest.php index b97a109..e86ae11 100644 --- a/tests/Mappers/StaticTypeMapperTest.php +++ b/tests/Mappers/StaticTypeMapperTest.php @@ -10,6 +10,7 @@ use TheCodingMachine\GraphQL\Controllers\Fixtures\TestObject; use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\ObjectType; +use TheCodingMachine\GraphQL\Controllers\Types\MutableObjectType; class StaticTypeMapperTest extends AbstractQueryProviderTest { @@ -22,7 +23,7 @@ public function setUp(): void { $this->typeMapper = new StaticTypeMapper(); $this->typeMapper->setTypes([ - TestObject::class => new ObjectType([ + TestObject::class => new MutableObjectType([ 'name' => 'TestObject', 'fields' => [ 'test' => Type::string(), diff --git a/tests/TypeGeneratorTest.php b/tests/TypeGeneratorTest.php index 649de99..b2cbc51 100644 --- a/tests/TypeGeneratorTest.php +++ b/tests/TypeGeneratorTest.php @@ -14,6 +14,7 @@ public function testNameAndFields() $type = $typeGenerator->mapAnnotatedObject(new TypeFoo(), $this->getTypeMapper()); $this->assertSame('TestObject', $type->name); + $type->freeze(); $this->assertCount(1, $type->getFields()); } diff --git a/tests/TypeRegistryTest.php b/tests/TypeRegistryTest.php new file mode 100644 index 0000000..0883bcc --- /dev/null +++ b/tests/TypeRegistryTest.php @@ -0,0 +1,55 @@ + 'Foo', + 'fields' => function() {return [];} + ]); + + $registry = new TypeRegistry(); + $registry->registerType($type); + + $this->expectException(GraphQLException::class); + $registry->registerType($type); + } + + public function testGetType() + { + $type = new ObjectType([ + 'name' => 'Foo', + 'fields' => function() {return [];} + ]); + + $registry = new TypeRegistry(); + $registry->registerType($type); + + $this->assertSame($type, $registry->getType('Foo')); + + $this->expectException(GraphQLException::class); + $registry->getType('Bar'); + } + + public function testHasType() + { + $type = new ObjectType([ + 'name' => 'Foo', + 'fields' => function() {return [];} + ]); + + $registry = new TypeRegistry(); + $registry->registerType($type); + + $this->assertTrue($registry->hasType('Foo')); + $this->assertFalse($registry->hasType('Bar')); + + } +}