From 95c4c758259ffb6737487736064ad6f90837048e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Fri, 11 Jan 2019 15:57:43 +0100 Subject: [PATCH 1/6] Adding support for ExtendType annotation --- src/AnnotationReader.php | 19 +- src/Annotations/ExtendType.php | 51 +++++ src/Mappers/CannotMapTypeException.php | 11 + src/Mappers/CompositeTypeMapper.php | 72 +++++++ src/Mappers/GlobTypeMapper.php | 199 ++++++++++++++++++ src/Mappers/PorpaginasTypeMapper.php | 52 +++++ src/Mappers/RecursiveTypeMapper.php | 76 ++++++- src/Mappers/RecursiveTypeMapperInterface.php | 6 +- src/Mappers/StaticTypeMapper.php | 52 +++++ src/Mappers/TypeMapperInterface.php | 40 ++++ src/MissingAnnotationException.php | 5 + src/TypeGenerator.php | 103 ++++++--- src/TypeRegistry.php | 62 ++++++ tests/AbstractQueryProviderTest.php | 58 ++--- .../Integration/Types/ExtendedContactType.php | 25 +++ tests/Integration/EndToEndTest.php | 25 ++- tests/Mappers/CompositeTypeMapperTest.php | 20 ++ tests/Mappers/RecursiveTypeMapperTest.php | 10 +- tests/TypeRegistryTest.php | 55 +++++ 19 files changed, 862 insertions(+), 79 deletions(-) create mode 100644 src/Annotations/ExtendType.php create mode 100644 src/TypeRegistry.php create mode 100644 tests/Fixtures/Integration/Types/ExtendedContactType.php create mode 100644 tests/TypeRegistryTest.php 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/Mappers/CannotMapTypeException.php b/src/Mappers/CannotMapTypeException.php index 83936c4..3d81618 100644 --- a/src/Mappers/CannotMapTypeException.php +++ b/src/Mappers/CannotMapTypeException.php @@ -4,6 +4,7 @@ namespace TheCodingMachine\GraphQL\Controllers\Mappers; +use GraphQL\Type\Definition\ObjectType; use ReflectionMethod; class CannotMapTypeException extends \Exception implements CannotMapTypeExceptionInterface @@ -48,4 +49,14 @@ public static function mustBeOutputType($subTypeName): self { return new self('type "'.$subTypeName.'" must be an output type.'); } + + public static function createForExtendType(string $className, ObjectType $type): self + { + return new self('cannot extend GraphQL type "'.$type->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..130d87f 100644 --- a/src/Mappers/CompositeTypeMapper.php +++ b/src/Mappers/CompositeTypeMapper.php @@ -152,4 +152,76 @@ 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 ObjectType $type + * @return bool + */ + public function canExtendTypeForClass(string $className, ObjectType $type): bool + { + foreach ($this->typeMappers as $typeMapper) { + if ($typeMapper->canExtendTypeForClass($className, $type)) { + return true; + } + } + return false; + } + + /** + * Extends the existing GraphQL type that is mapped to $className. + * + * @param string $className + * @param ObjectType $type + * @param RecursiveTypeMapperInterface $recursiveTypeMapper + * @return ObjectType + * @throws CannotMapTypeExceptionInterface + */ + public function extendTypeForClass(string $className, ObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): ObjectType + { + foreach ($this->typeMappers as $typeMapper) { + if ($typeMapper->canExtendTypeForClass($className, $type)) { + $type = $typeMapper->extendTypeForClass($className, $type, $recursiveTypeMapper); + } + } + return $type; + } + + /** + * Returns true if this type mapper can extend an existing type for the $typeName GraphQL type + * + * @param string $typeName + * @param ObjectType $type + * @return bool + */ + public function canExtendTypeForName(string $typeName, ObjectType $type): bool + { + foreach ($this->typeMappers as $typeMapper) { + if ($typeMapper->canExtendTypeForName($typeName, $type)) { + return true; + } + } + return false; + } + + /** + * Extends the existing GraphQL type that is mapped to the $typeName GraphQL type. + * + * @param string $typeName + * @param ObjectType $type + * @param RecursiveTypeMapperInterface $recursiveTypeMapper + * @return ObjectType + * @throws CannotMapTypeExceptionInterface + */ + public function extendTypeForName(string $typeName, ObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): ObjectType + { + foreach ($this->typeMappers as $typeMapper) { + if ($typeMapper->canExtendTypeForName($typeName, $type)) { + $type = $typeMapper->extendTypeForName($typeName, $type, $recursiveTypeMapper); + } + } + return $type; + } } diff --git a/src/Mappers/GlobTypeMapper.php b/src/Mappers/GlobTypeMapper.php index 3e1a0e8..5b23192 100644 --- a/src/Mappers/GlobTypeMapper.php +++ b/src/Mappers/GlobTypeMapper.php @@ -15,6 +15,7 @@ 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; @@ -50,10 +51,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] */ @@ -164,6 +173,12 @@ private function buildMap(): void $this->storeTypeInCache($className, $type, $refClass->getFileName()); } + $extendType = $this->annotationReader->getExtendTypeAnnotation($refClass); + + if ($extendType !== null) { + $this->storeExtendTypeInCache($className, $extendType, $refClass->getFileName()); + } + foreach ($refClass->getMethods() as $method) { $factory = $this->annotationReader->getFactoryAnnotation($method); if ($factory !== null) { @@ -221,6 +236,29 @@ 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): 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. + $type = new Type(['class'=>$extendType->getClass()]); + $typeName = $this->namingStrategy->getOutputTypeName($extendTypeClassName, $type); + $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 +338,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 */ @@ -469,4 +565,107 @@ public function canMapNameToType(string $typeName): bool 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 ObjectType $type + * @return bool + */ + public function canExtendTypeForClass(string $className, ObjectType $type): bool + { + $extendTypeClassName = $this->getExtendTypesFromCacheByObjectClass($className); + + if ($extendTypeClassName === null) { + $this->getMap(); + } + + return isset($this->mapClassToExtendTypeArray[$className]); + } + + /** + * Extends the existing GraphQL type that is mapped to $className. + * + * @param string $className + * @param ObjectType $type + * @param RecursiveTypeMapperInterface $recursiveTypeMapper + * @return ObjectType + * @throws CannotMapTypeExceptionInterface + */ + public function extendTypeForClass(string $className, ObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): ObjectType + { + $extendTypeClassNames = $this->getExtendTypesFromCacheByObjectClass($className); + + if ($extendTypeClassNames === null) { + $this->getMap(); + } + + if (!isset($this->mapClassToExtendTypeArray[$className])) { + throw CannotMapTypeException::createForExtendType($className, $type); + } + + foreach ($this->mapClassToExtendTypeArray[$className] as $extendedTypeClass) { + $type = $this->typeGenerator->extendAnnotatedObject($this->container->get($extendedTypeClass), $type, $recursiveTypeMapper); + } + return $type; + } + + /** + * Returns true if this type mapper can extend an existing type for the $typeName GraphQL type + * + * @param string $typeName + * @param ObjectType $type + * @return bool + */ + public function canExtendTypeForName(string $typeName, ObjectType $type): bool + { + $typeClassNames = $this->getExtendTypesFromCacheByGraphQLTypeName($typeName); + + if ($typeClassNames !== null) { + return true; + } + + /*$factory = $this->getFactoryFromCacheByGraphQLInputTypeName($typeName); + if ($factory !== null) { + return true; + }*/ + + $this->getMap(); + + 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 ObjectType $type + * @param RecursiveTypeMapperInterface $recursiveTypeMapper + * @return ObjectType + * @throws CannotMapTypeExceptionInterface + */ + public function extendTypeForName(string $typeName, ObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): ObjectType + { + $extendTypeClassNames = $this->getExtendTypesFromCacheByGraphQLTypeName($typeName); + if ($extendTypeClassNames === null) { + /*$factory = $this->getFactoryFromCacheByGraphQLInputTypeName($typeName); + if ($factory === null) {*/ + $this->getMap(); + //} + } + + if (isset($this->mapNameToExtendType[$typeName])) { + foreach ($this->mapNameToExtendType[$typeName] as $extendedTypeClass) { + $type = $this->typeGenerator->extendAnnotatedObject($this->container->get($extendedTypeClass), $type, $recursiveTypeMapper); + } + return $type; + } + /*if (isset($this->mapInputNameToFactory[$typeName])) { + $factory = $this->mapInputNameToFactory[$typeName]; + return $this->inputTypeGenerator->mapFactoryMethod($this->container->get($factory[0]), $factory[1], $recursiveTypeMapper); + }*/ + + throw CannotMapTypeException::createForExtendName($typeName, $type); + } } diff --git a/src/Mappers/PorpaginasTypeMapper.php b/src/Mappers/PorpaginasTypeMapper.php index db39048..6ef7263 100644 --- a/src/Mappers/PorpaginasTypeMapper.php +++ b/src/Mappers/PorpaginasTypeMapper.php @@ -172,4 +172,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 ObjectType $type + * @return bool + */ + public function canExtendTypeForClass(string $className, ObjectType $type): bool + { + return false; + } + + /** + * Extends the existing GraphQL type that is mapped to $className. + * + * @param string $className + * @param ObjectType $type + * @param RecursiveTypeMapperInterface $recursiveTypeMapper + * @return ObjectType + * @throws CannotMapTypeExceptionInterface + */ + public function extendTypeForClass(string $className, ObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): ObjectType + { + 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 ObjectType $type + * @return bool + */ + public function canExtendTypeForName(string $typeName, ObjectType $type): bool + { + return false; + } + + /** + * Extends the existing GraphQL type that is mapped to the $typeName GraphQL type. + * + * @param string $typeName + * @param ObjectType $type + * @param RecursiveTypeMapperInterface $recursiveTypeMapper + * @return ObjectType + * @throws CannotMapTypeExceptionInterface + */ + public function extendTypeForName(string $typeName, ObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): ObjectType + { + throw CannotMapTypeException::createForExtendName($typeName, $type); + } } diff --git a/src/Mappers/RecursiveTypeMapper.php b/src/Mappers/RecursiveTypeMapper.php index a9f72d4..b76e053 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,6 +15,7 @@ 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; /** @@ -62,12 +64,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 +94,27 @@ 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. + * @param (OutputType&ObjectType)|(OutputType&InterfaceType)|null $subType An optional sub-type if the main class is an iterator that needs to be typed. * @return ObjectType * @throws CannotMapTypeExceptionInterface */ - public function mapClassToType(string $className, ?ObjectType $subType): ObjectType + public function mapClassToType(string $className, ?OutputType $subType): ObjectType { $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)) { + return $type; + } + + $type = $this->extendType($className, $type); + $this->typeRegistry->registerType($type); + + return $type; } /** @@ -114,12 +133,38 @@ private function findClosestMatchingParent(string $className): ?string return null; } + /** + * Extends a type using available type extenders. + * + * @param string $className + * @param ObjectType $type + * @return ObjectType + * @throws CannotMapTypeExceptionInterface + */ + private function extendType(string $className, ObjectType $type): ObjectType + { + $classes = []; + do { + if ($this->typeMapper->canExtendTypeForClass($className, $type)) { + $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) { + $type = $this->typeMapper->extendTypeForClass($class, $type, $this); + } + + return $type; + } + /** * 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 +175,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 +200,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 +321,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 +356,20 @@ 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)) { + if ($type instanceof ObjectType) { + if ($this->typeMapper->canExtendTypeForName($typeName, $type)) { + $type = $this->typeMapper->extendTypeForName($typeName, $type, $this); + } + $this->typeRegistry->registerType($type); + } + } + return $type; } // Maybe the type is an interface? diff --git a/src/Mappers/RecursiveTypeMapperInterface.php b/src/Mappers/RecursiveTypeMapperInterface.php index 313d89c..18fd47e 100644 --- a/src/Mappers/RecursiveTypeMapperInterface.php +++ b/src/Mappers/RecursiveTypeMapperInterface.php @@ -29,17 +29,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. + * @param (OutputType&ObjectType)|(OutputType&InterfaceType)|null $subType An optional sub-type if the main class is an iterator that needs to be typed. * @return ObjectType * @throws CannotMapTypeExceptionInterface */ - public function mapClassToType(string $className, ?ObjectType $subType): ObjectType; + public function mapClassToType(string $className, ?OutputType $subType): ObjectType; /** * 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..a0dbbb9 100644 --- a/src/Mappers/StaticTypeMapper.php +++ b/src/Mappers/StaticTypeMapper.php @@ -182,4 +182,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 ObjectType $type + * @return bool + */ + public function canExtendTypeForClass(string $className, ObjectType $type): bool + { + return false; + } + + /** + * Extends the existing GraphQL type that is mapped to $className. + * + * @param string $className + * @param ObjectType $type + * @param RecursiveTypeMapperInterface $recursiveTypeMapper + * @return ObjectType + * @throws CannotMapTypeExceptionInterface + */ + public function extendTypeForClass(string $className, ObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): ObjectType + { + 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 ObjectType $type + * @return bool + */ + public function canExtendTypeForName(string $typeName, ObjectType $type): bool + { + return false; + } + + /** + * Extends the existing GraphQL type that is mapped to the $typeName GraphQL type. + * + * @param string $typeName + * @param ObjectType $type + * @param RecursiveTypeMapperInterface $recursiveTypeMapper + * @return ObjectType + * @throws CannotMapTypeExceptionInterface + */ + public function extendTypeForName(string $typeName, ObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): ObjectType + { + throw CannotMapTypeException::createForExtendName($typeName, $type); + } } diff --git a/src/Mappers/TypeMapperInterface.php b/src/Mappers/TypeMapperInterface.php index 5ca3a68..36b722f 100644 --- a/src/Mappers/TypeMapperInterface.php +++ b/src/Mappers/TypeMapperInterface.php @@ -74,4 +74,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 ObjectType $type + * @return bool + */ + public function canExtendTypeForClass(string $className, ObjectType $type): bool; + + /** + * Extends the existing GraphQL type that is mapped to $className. + * + * @param string $className + * @param ObjectType $type + * @param RecursiveTypeMapperInterface $recursiveTypeMapper + * @return ObjectType + * @throws CannotMapTypeExceptionInterface + */ + public function extendTypeForClass(string $className, ObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): ObjectType; + + /** + * Returns true if this type mapper can extend an existing type for the $typeName GraphQL type + * + * @param string $typeName + * @param ObjectType $type + * @return bool + */ + public function canExtendTypeForName(string $typeName, ObjectType $type): bool; + + /** + * Extends the existing GraphQL type that is mapped to the $typeName GraphQL type. + * + * @param string $typeName + * @param ObjectType $type + * @param RecursiveTypeMapperInterface $recursiveTypeMapper + * @return ObjectType + * @throws CannotMapTypeExceptionInterface + */ + public function extendTypeForName(string $typeName, ObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): ObjectType; } 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/TypeGenerator.php b/src/TypeGenerator.php index 1f8cdb6..72f4204 100644 --- a/src/TypeGenerator.php +++ b/src/TypeGenerator.php @@ -23,22 +23,24 @@ class TypeGenerator * @var FieldsBuilderFactory */ private $controllerQueryProviderFactory; - /** - * @var array - */ - private $cache = []; /** * @var NamingStrategyInterface */ private $namingStrategy; + /** + * @var TypeRegistry + */ + private $typeRegistry; public function __construct(AnnotationReader $annotationReader, FieldsBuilderFactory $controllerQueryProviderFactory, - NamingStrategyInterface $namingStrategy) + NamingStrategyInterface $namingStrategy, + TypeRegistry $typeRegistry) { $this->annotationReader = $annotationReader; $this->controllerQueryProviderFactory = $controllerQueryProviderFactory; $this->namingStrategy = $namingStrategy; + $this->typeRegistry = $typeRegistry; } /** @@ -59,31 +61,80 @@ 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->getObjectType($typeName); + } - $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 ObjectType $type + * @param RecursiveTypeMapperInterface $recursiveTypeMapper + */ + public function extendAnnotatedObject($annotatedObject, ObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper) + { + $refTypeClass = new \ReflectionClass($annotatedObject); + + $extendTypeAnnotation = $this->annotationReader->getExtendTypeAnnotation($refTypeClass); + + if ($extendTypeAnnotation === null) { + throw MissingAnnotationException::missingExtendTypeException(); + } + + //$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)); } - return $this->cache[$typeName]; + 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->controllerQueryProviderFactory->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..9325369 --- /dev/null +++ b/src/TypeRegistry.php @@ -0,0 +1,62 @@ + + */ + 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 getObjectType(string $typeName): ObjectType + { + $type = $this->getType($typeName); + if (!$type instanceof ObjectType) { + throw new GraphQLException('Expected GraphQL type "'.$typeName.'" to be an ObjectType. Got a '.get_class($type)); + } + return $type; + } +} diff --git a/tests/AbstractQueryProviderTest.php b/tests/AbstractQueryProviderTest.php index 572e830..8b08bb7 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; @@ -47,6 +48,7 @@ abstract class AbstractQueryProviderTest extends TestCase private $controllerQueryProviderFactory; private $annotationReader; private $typeResolver; + private $typeRegistry; protected function getTestObjectType() { @@ -151,34 +153,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 +177,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, ObjectType $type): bool + { + return false; + } + + public function extendTypeForClass(string $className, ObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): ObjectType + { + throw CannotMapTypeException::createForExtendType($className, $type); + } + + public function canExtendTypeForName(string $typeName, ObjectType $type): bool + { + return false; + } + + public function extendTypeForName(string $typeName, ObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): ObjectType + { + throw CannotMapTypeException::createForExtendName($typeName, $type); + } + }, new NamingStrategy(), new ArrayCache(), $this->getTypeRegistry()); } return $this->typeMapper; } @@ -267,7 +265,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 +307,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/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/Integration/EndToEndTest.php b/tests/Integration/EndToEndTest.php index b87c4a6..44292f8 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,8 +273,6 @@ public function testEndToEndPorpaginas() */ $schema = $this->mainContainer->get(Schema::class); - $schema->assertValid(); - $queryString = ' query { getContactsIterator { diff --git a/tests/Mappers/CompositeTypeMapperTest.php b/tests/Mappers/CompositeTypeMapperTest.php index d8f8c3b..f95cbdf 100644 --- a/tests/Mappers/CompositeTypeMapperTest.php +++ b/tests/Mappers/CompositeTypeMapperTest.php @@ -102,6 +102,26 @@ public function canMapNameToType(string $typeName): bool { return $typeName === 'TestObject'; } + + public function canExtendTypeForClass(string $className, ObjectType $type): bool + { + return false; + } + + public function extendTypeForClass(string $className, ObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): ObjectType + { + throw CannotMapTypeException::createForExtendType($className, $type); + } + + public function canExtendTypeForName(string $typeName, ObjectType $type): bool + { + return false; + } + + public function extendTypeForName(string $typeName, ObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): ObjectType + { + throw CannotMapTypeException::createForExtendName($typeName, $type); + } }; $this->composite = new CompositeTypeMapper([$typeMapper1]); diff --git a/tests/Mappers/RecursiveTypeMapperTest.php b/tests/Mappers/RecursiveTypeMapperTest.php index 3126f0d..f77f23b 100644 --- a/tests/Mappers/RecursiveTypeMapperTest.php +++ b/tests/Mappers/RecursiveTypeMapperTest.php @@ -33,7 +33,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)); @@ -55,7 +55,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 +88,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 +109,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/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')); + + } +} From cce8eb688eb259a4300e49ed53d42ddadba1be38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Mon, 14 Jan 2019 14:04:09 +0100 Subject: [PATCH 2/6] Hunting ExtendType bug --- .travis.yml | 15 +- src/Mappers/GlobTypeMapper.php | 188 ++++++++++++++++++++----- tests/Fixtures/Types/FooExtendType.php | 25 ++++ tests/Integration/EndToEndTest.php | 4 +- tests/Mappers/GlobTypeMapperTest.php | 36 +++++ 5 files changed, 229 insertions(+), 39 deletions(-) create mode 100644 tests/Fixtures/Types/FooExtendType.php diff --git a/.travis.yml b/.travis.yml index a92655d..94b6088 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="" + - 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/Mappers/GlobTypeMapper.php b/src/Mappers/GlobTypeMapper.php index 5b23192..9ed49b9 100644 --- a/src/Mappers/GlobTypeMapper.php +++ b/src/Mappers/GlobTypeMapper.php @@ -12,6 +12,7 @@ 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; @@ -87,6 +88,10 @@ final class GlobTypeMapper implements TypeMapperInterface * @var bool */ private $fullMapComputed = false; + /** + * @var bool + */ + private $fullExtendMapComputed = false; /** * @var NamingStrategy */ @@ -99,6 +104,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) @@ -120,9 +133,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); @@ -134,7 +147,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. @@ -143,23 +160,106 @@ private function getMap(): array $this->cache->set($keyInputClassCache, $this->mapClassToFactory, $this->globTtl); $this->cache->set($keyInputNameCache, $this->mapInputNameToFactory, $this->globTtl); } - } - return $this->mapClassToTypeArray; + $this->fullMapComputed = true; + } + return [ + 'mapClassToTypeArray' => $this->mapClassToTypeArray, + 'mapNameToType' => $this->mapNameToType, + 'mapClassToFactory' => $this->mapClassToFactory, + 'mapInputNameToFactory' => $this->mapInputNameToFactory, + ]; } - private function buildMap(): void + private function getMapClassToType(): array + { + return $this->getMaps()['mapClassToTypeArray']; + } + + private function getMapNameToType(): array + { + return $this->getMaps()['mapNameToType']; + } + + private function getMapClassToFactory(): array + { + return $this->getMaps()['mapClassToFactory']; + } + + private function getMapInputNameToFactory(): 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()['mapInputNameToFactory']; + } + + /** + * Returns an array of fully qualified class names. + * + * @return array> + */ + private function getExtendMaps(): 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(); + // 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(): array + { + return $this->getExtendMaps()['mapClassToExtendTypeArray']; + } + + private function getMapNameToExtendType(): array + { + return $this->getExtendMaps()['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) { @@ -173,12 +273,6 @@ private function buildMap(): void $this->storeTypeInCache($className, $type, $refClass->getFileName()); } - $extendType = $this->annotationReader->getExtendTypeAnnotation($refClass); - - if ($extendType !== null) { - $this->storeExtendTypeInCache($className, $extendType, $refClass->getFileName()); - } - foreach ($refClass->getMethods() as $method) { $factory = $this->annotationReader->getFactoryAnnotation($method); if ($factory !== null) { @@ -192,7 +286,18 @@ private function buildMap(): void } } - $this->fullMapComputed = true; + } + + private function buildExtendMap(): void + { + $classes = $this->getClassList(); + foreach ($classes as $className => $refClass) { + $extendType = $this->annotationReader->getExtendTypeAnnotation($refClass); + + if ($extendType !== null) { + $this->storeExtendTypeInCache($className, $extendType, $refClass->getFileName()); + } + } } /** @@ -250,6 +355,25 @@ private function storeExtendTypeInCache(string $extendTypeClassName, ExtendType ], $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. + $type = new Type(['class'=>$extendType->getClass()]); $typeName = $this->namingStrategy->getOutputTypeName($extendTypeClassName, $type); $this->mapNameToExtendType[$typeName][$extendTypeClassName] = $extendTypeClassName; @@ -435,7 +559,7 @@ public function canMapClassToType(string $className): bool $typeClassName = $this->getTypeFromCacheByObjectClass($className); if ($typeClassName === null) { - $this->getMap(); + $this->getMaps(); } return isset($this->mapClassToTypeArray[$className]); @@ -455,7 +579,7 @@ public function mapClassToType(string $className, ?OutputType $subType, Recursiv $typeClassName = $this->getTypeFromCacheByObjectClass($className); if ($typeClassName === null) { - $this->getMap(); + $this->getMaps(); } if (!isset($this->mapClassToTypeArray[$className])) { @@ -471,7 +595,7 @@ public function mapClassToType(string $className, ?OutputType $subType, Recursiv */ public function getSupportedClasses(): array { - return array_keys($this->getMap()); + return array_keys($this->getMapClassToType()); } /** @@ -485,7 +609,7 @@ public function canMapClassToInputType(string $className): bool $factory = $this->getFactoryFromCacheByObjectClass($className); if ($factory === null) { - $this->getMap(); + $this->getMaps(); } return isset($this->mapClassToFactory[$className]); } @@ -503,7 +627,7 @@ public function mapClassToInputType(string $className, RecursiveTypeMapperInterf $factory = $this->getFactoryFromCacheByObjectClass($className); if ($factory === null) { - $this->getMap(); + $this->getMaps(); } if (!isset($this->mapClassToFactory[$className])) { @@ -527,7 +651,7 @@ public function mapNameToType(string $typeName, RecursiveTypeMapperInterface $re if ($typeClassName === null) { $factory = $this->getFactoryFromCacheByGraphQLInputTypeName($typeName); if ($factory === null) { - $this->getMap(); + $this->getMaps(); } } @@ -561,7 +685,7 @@ public function canMapNameToType(string $typeName): bool return true; } - $this->getMap(); + $this->getMaps(); return isset($this->mapNameToType[$typeName]) || isset($this->mapInputNameToFactory[$typeName]); } @@ -578,7 +702,7 @@ public function canExtendTypeForClass(string $className, ObjectType $type): bool $extendTypeClassName = $this->getExtendTypesFromCacheByObjectClass($className); if ($extendTypeClassName === null) { - $this->getMap(); + $this->getExtendMaps(); } return isset($this->mapClassToExtendTypeArray[$className]); @@ -598,7 +722,7 @@ public function extendTypeForClass(string $className, ObjectType $type, Recursiv $extendTypeClassNames = $this->getExtendTypesFromCacheByObjectClass($className); if ($extendTypeClassNames === null) { - $this->getMap(); + $this->getExtendMaps(); } if (!isset($this->mapClassToExtendTypeArray[$className])) { @@ -631,7 +755,7 @@ public function canExtendTypeForName(string $typeName, ObjectType $type): bool return true; }*/ - $this->getMap(); + $this->getExtendMaps(); return isset($this->mapNameToExtendType[$typeName])/* || isset($this->mapInputNameToFactory[$typeName])*/; } @@ -651,7 +775,7 @@ public function extendTypeForName(string $typeName, ObjectType $type, RecursiveT if ($extendTypeClassNames === null) { /*$factory = $this->getFactoryFromCacheByGraphQLInputTypeName($typeName); if ($factory === null) {*/ - $this->getMap(); + $this->getExtendMaps(); //} } 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 44292f8..eb1732a 100644 --- a/tests/Integration/EndToEndTest.php +++ b/tests/Integration/EndToEndTest.php @@ -278,6 +278,7 @@ public function testEndToEndPorpaginas() getContactsIterator { items(limit: 1, offset: 1) { name + uppercaseName ... on User { email } @@ -297,6 +298,7 @@ public function testEndToEndPorpaginas() 'items' => [ [ 'name' => 'Bill', + 'uppercaseName' => 'BILL', 'email' => 'bill@example.com' ] ], @@ -369,7 +371,7 @@ public function testEndToEndPorpaginas() 'getContactsIterator' => [ 'items' => [ [ - 'name' => 'Joe' + 'name' => 'Joe', ], [ 'name' => 'Bill', diff --git a/tests/Mappers/GlobTypeMapperTest.php b/tests/Mappers/GlobTypeMapperTest.php index 340f9f3..70d89f1 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->assertInstanceOf(ObjectType::class, $mapper->extendTypeForClass(TestObject::class, $type, $this->getTypeMapper())); + $this->assertInstanceOf(ObjectType::class, $mapper->extendTypeForName('Foo', $type, $this->getTypeMapper())); + $this->assertTrue($mapper->canExtendTypeForName('Foo')); + $this->assertFalse($mapper->canExtendTypeForName('NotExists')); + + // 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->assertTrue($anotherMapperSameCache->canExtendTypeForName('Foo', $type)); + + $this->expectException(CannotMapTypeException::class); + $mapper->extendTypeForClass(\stdClass::class, $type, $this->getTypeMapper()); + } } From 17986a77a8aa4e079c10e28e7b7855d5ae09e8f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Tue, 15 Jan 2019 10:51:27 +0100 Subject: [PATCH 3/6] Major refactoring to fix issue with ExtendType mapping --- src/Mappers/CompositeTypeMapper.php | 37 +++--- src/Mappers/GlobTypeMapper.php | 69 ++++++----- src/Mappers/PorpaginasTypeMapper.php | 27 ++--- src/Mappers/RecursiveTypeMapper.php | 53 ++++++--- src/Mappers/RecursiveTypeMapperInterface.php | 5 +- src/Mappers/StaticTypeMapper.php | 31 ++--- src/Mappers/TypeMapperInterface.php | 25 ++-- src/TypeGenerator.php | 71 ++++++++---- src/Types/MutableObjectType.php | 115 +++++++++++++++++++ src/Types/TypeAnnotatedObjectType.php | 63 ++++++++++ tests/AbstractQueryProviderTest.php | 21 ++-- tests/Integration/EndToEndTest.php | 1 + tests/Mappers/CompositeTypeMapperTest.php | 13 ++- tests/Mappers/GlobTypeMapperTest.php | 14 +-- tests/Mappers/RecursiveTypeMapperTest.php | 5 +- tests/Mappers/StaticTypeMapperTest.php | 3 +- tests/TypeGeneratorTest.php | 1 + 17 files changed, 391 insertions(+), 163 deletions(-) create mode 100644 src/Types/MutableObjectType.php create mode 100644 src/Types/TypeAnnotatedObjectType.php diff --git a/src/Mappers/CompositeTypeMapper.php b/src/Mappers/CompositeTypeMapper.php index 130d87f..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)) { @@ -157,13 +158,13 @@ public function canMapNameToType(string $typeName): bool * Returns true if this type mapper can extend an existing type for the $className FQCN * * @param string $className - * @param ObjectType $type + * @param MutableObjectType $type * @return bool */ - public function canExtendTypeForClass(string $className, ObjectType $type): bool + public function canExtendTypeForClass(string $className, MutableObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): bool { foreach ($this->typeMappers as $typeMapper) { - if ($typeMapper->canExtendTypeForClass($className, $type)) { + if ($typeMapper->canExtendTypeForClass($className, $type, $recursiveTypeMapper)) { return true; } } @@ -174,32 +175,30 @@ public function canExtendTypeForClass(string $className, ObjectType $type): bool * Extends the existing GraphQL type that is mapped to $className. * * @param string $className - * @param ObjectType $type + * @param MutableObjectType $type * @param RecursiveTypeMapperInterface $recursiveTypeMapper - * @return ObjectType * @throws CannotMapTypeExceptionInterface */ - public function extendTypeForClass(string $className, ObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): ObjectType + public function extendTypeForClass(string $className, MutableObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): void { foreach ($this->typeMappers as $typeMapper) { - if ($typeMapper->canExtendTypeForClass($className, $type)) { - $type = $typeMapper->extendTypeForClass($className, $type, $recursiveTypeMapper); + if ($typeMapper->canExtendTypeForClass($className, $type, $recursiveTypeMapper)) { + $typeMapper->extendTypeForClass($className, $type, $recursiveTypeMapper); } } - return $type; } /** * Returns true if this type mapper can extend an existing type for the $typeName GraphQL type * * @param string $typeName - * @param ObjectType $type + * @param MutableObjectType $type * @return bool */ - public function canExtendTypeForName(string $typeName, ObjectType $type): bool + public function canExtendTypeForName(string $typeName, MutableObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): bool { foreach ($this->typeMappers as $typeMapper) { - if ($typeMapper->canExtendTypeForName($typeName, $type)) { + if ($typeMapper->canExtendTypeForName($typeName, $type, $recursiveTypeMapper)) { return true; } } @@ -210,18 +209,16 @@ public function canExtendTypeForName(string $typeName, ObjectType $type): bool * Extends the existing GraphQL type that is mapped to the $typeName GraphQL type. * * @param string $typeName - * @param ObjectType $type + * @param MutableObjectType $type * @param RecursiveTypeMapperInterface $recursiveTypeMapper - * @return ObjectType * @throws CannotMapTypeExceptionInterface */ - public function extendTypeForName(string $typeName, ObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): ObjectType + public function extendTypeForName(string $typeName, MutableObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): void { foreach ($this->typeMappers as $typeMapper) { - if ($typeMapper->canExtendTypeForName($typeName, $type)) { - $type = $typeMapper->extendTypeForName($typeName, $type, $recursiveTypeMapper); + if ($typeMapper->canExtendTypeForName($typeName, $type, $recursiveTypeMapper)) { + $typeMapper->extendTypeForName($typeName, $type, $recursiveTypeMapper); } } - return $type; } } diff --git a/src/Mappers/GlobTypeMapper.php b/src/Mappers/GlobTypeMapper.php index 9ed49b9..c58fc46 100644 --- a/src/Mappers/GlobTypeMapper.php +++ b/src/Mappers/GlobTypeMapper.php @@ -23,6 +23,7 @@ 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). @@ -195,7 +196,7 @@ private function getMapInputNameToFactory(): array * * @return array> */ - private function getExtendMaps(): array + private function getExtendMaps(RecursiveTypeMapperInterface $recursiveTypeMapper): array { if ($this->fullExtendMapComputed === false) { $namespace = str_replace('\\', '_', $this->namespace); @@ -206,7 +207,7 @@ private function getExtendMaps(): array if ($this->mapClassToExtendTypeArray === null || $this->mapNameToExtendType === null ) { - $this->buildExtendMap(); + $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); @@ -220,14 +221,14 @@ private function getExtendMaps(): array ]; } - private function getMapClassToExtendTypeArray(): array + private function getMapClassToExtendTypeArray(RecursiveTypeMapperInterface $recursiveTypeMapper): array { - return $this->getExtendMaps()['mapClassToExtendTypeArray']; + return $this->getExtendMaps($recursiveTypeMapper)['mapClassToExtendTypeArray']; } - private function getMapNameToExtendType(): array + private function getMapNameToExtendType(RecursiveTypeMapperInterface $recursiveTypeMapper): array { - return $this->getExtendMaps()['mapNameToExtendType']; + return $this->getExtendMaps($recursiveTypeMapper)['mapNameToExtendType']; } /** @@ -288,14 +289,14 @@ private function buildMap(): void } } - private function buildExtendMap(): void + 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()); + $this->storeExtendTypeInCache($className, $extendType, $refClass->getFileName(), $recursiveTypeMapper); } } } @@ -344,7 +345,7 @@ private function storeInputTypeInCache(ReflectionMethod $refMethod, string $inpu /** * Stores in cache the mapping ExtendTypeClass <=> Object class <=> GraphQL type name. */ - private function storeExtendTypeInCache(string $extendTypeClassName, ExtendType $extendType, string $typeFileName): void + private function storeExtendTypeInCache(string $extendTypeClassName, ExtendType $extendType, string $typeFileName, RecursiveTypeMapperInterface $recursiveTypeMapper): void { $objectClassName = $extendType->getClass(); $this->mapClassToExtendTypeArray[$objectClassName][$extendTypeClassName] = $extendTypeClassName; @@ -374,8 +375,9 @@ private function storeExtendTypeInCache(string $extendTypeClassName, ExtendType // - 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. - $type = new Type(['class'=>$extendType->getClass()]); - $typeName = $this->namingStrategy->getOutputTypeName($extendTypeClassName, $type); + $targetType = $recursiveTypeMapper->mapClassToType($extendType->getClass(), null); + $typeName = $targetType->name; + $this->mapNameToExtendType[$typeName][$extendTypeClassName] = $extendTypeClassName; $this->cache->set('globExtendTypeMapperByName_'.$typeName, [ 'filemtime' => filemtime($typeFileName), @@ -574,7 +576,7 @@ public function canMapClassToType(string $className): bool * @return ObjectType * @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); @@ -694,15 +696,15 @@ public function canMapNameToType(string $typeName): bool * Returns true if this type mapper can extend an existing type for the $className FQCN * * @param string $className - * @param ObjectType $type + * @param MutableObjectType $type * @return bool */ - public function canExtendTypeForClass(string $className, ObjectType $type): bool + public function canExtendTypeForClass(string $className, MutableObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): bool { $extendTypeClassName = $this->getExtendTypesFromCacheByObjectClass($className); if ($extendTypeClassName === null) { - $this->getExtendMaps(); + $this->getExtendMaps($recursiveTypeMapper); } return isset($this->mapClassToExtendTypeArray[$className]); @@ -712,17 +714,16 @@ public function canExtendTypeForClass(string $className, ObjectType $type): bool * Extends the existing GraphQL type that is mapped to $className. * * @param string $className - * @param ObjectType $type + * @param MutableObjectType $type * @param RecursiveTypeMapperInterface $recursiveTypeMapper - * @return ObjectType * @throws CannotMapTypeExceptionInterface */ - public function extendTypeForClass(string $className, ObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): ObjectType + public function extendTypeForClass(string $className, MutableObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): void { $extendTypeClassNames = $this->getExtendTypesFromCacheByObjectClass($className); if ($extendTypeClassNames === null) { - $this->getExtendMaps(); + $this->getExtendMaps($recursiveTypeMapper); } if (!isset($this->mapClassToExtendTypeArray[$className])) { @@ -730,19 +731,18 @@ public function extendTypeForClass(string $className, ObjectType $type, Recursiv } foreach ($this->mapClassToExtendTypeArray[$className] as $extendedTypeClass) { - $type = $this->typeGenerator->extendAnnotatedObject($this->container->get($extendedTypeClass), $type, $recursiveTypeMapper); + $this->typeGenerator->extendAnnotatedObject($this->container->get($extendedTypeClass), $type, $recursiveTypeMapper); } - return $type; } /** * Returns true if this type mapper can extend an existing type for the $typeName GraphQL type * * @param string $typeName - * @param ObjectType $type + * @param MutableObjectType $type * @return bool */ - public function canExtendTypeForName(string $typeName, ObjectType $type): bool + public function canExtendTypeForName(string $typeName, MutableObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): bool { $typeClassNames = $this->getExtendTypesFromCacheByGraphQLTypeName($typeName); @@ -755,7 +755,7 @@ public function canExtendTypeForName(string $typeName, ObjectType $type): bool return true; }*/ - $this->getExtendMaps(); + $this->getExtendMaps($recursiveTypeMapper); return isset($this->mapNameToExtendType[$typeName])/* || isset($this->mapInputNameToFactory[$typeName])*/; } @@ -764,32 +764,31 @@ public function canExtendTypeForName(string $typeName, ObjectType $type): bool * Extends the existing GraphQL type that is mapped to the $typeName GraphQL type. * * @param string $typeName - * @param ObjectType $type + * @param MutableObjectType $type * @param RecursiveTypeMapperInterface $recursiveTypeMapper - * @return ObjectType * @throws CannotMapTypeExceptionInterface */ - public function extendTypeForName(string $typeName, ObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): ObjectType + 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(); + $this->getExtendMaps($recursiveTypeMapper); //} } - if (isset($this->mapNameToExtendType[$typeName])) { - foreach ($this->mapNameToExtendType[$typeName] as $extendedTypeClass) { - $type = $this->typeGenerator->extendAnnotatedObject($this->container->get($extendedTypeClass), $type, $recursiveTypeMapper); - } - return $type; + 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); }*/ - - throw CannotMapTypeException::createForExtendName($typeName, $type); } } diff --git a/src/Mappers/PorpaginasTypeMapper.php b/src/Mappers/PorpaginasTypeMapper.php index 6ef7263..0aad6ac 100644 --- a/src/Mappers/PorpaginasTypeMapper.php +++ b/src/Mappers/PorpaginasTypeMapper.php @@ -15,6 +15,7 @@ use RuntimeException; use function strpos; use function substr; +use TheCodingMachine\GraphQL\Controllers\Types\MutableObjectType; class PorpaginasTypeMapper implements TypeMapperInterface { @@ -43,7 +44,7 @@ public function canMapClassToType(string $className): bool * @return ObjectType * @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 [ @@ -177,10 +178,11 @@ public function mapClassToInputType(string $className, RecursiveTypeMapperInterf * Returns true if this type mapper can extend an existing type for the $className FQCN * * @param string $className - * @param ObjectType $type + * @param MutableObjectType $type + * @param RecursiveTypeMapperInterface $recursiveTypeMapper * @return bool */ - public function canExtendTypeForClass(string $className, ObjectType $type): bool + public function canExtendTypeForClass(string $className, MutableObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): bool { return false; } @@ -189,12 +191,11 @@ public function canExtendTypeForClass(string $className, ObjectType $type): bool * Extends the existing GraphQL type that is mapped to $className. * * @param string $className - * @param ObjectType $type + * @param MutableObjectType $type * @param RecursiveTypeMapperInterface $recursiveTypeMapper - * @return ObjectType * @throws CannotMapTypeExceptionInterface */ - public function extendTypeForClass(string $className, ObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): ObjectType + public function extendTypeForClass(string $className, MutableObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): void { throw CannotMapTypeException::createForExtendType($className, $type); } @@ -203,10 +204,11 @@ public function extendTypeForClass(string $className, ObjectType $type, Recursiv * Returns true if this type mapper can extend an existing type for the $typeName GraphQL type * * @param string $typeName - * @param ObjectType $type + * @param MutableObjectType $type + * @param RecursiveTypeMapperInterface $recursiveTypeMapper * @return bool */ - public function canExtendTypeForName(string $typeName, ObjectType $type): bool + public function canExtendTypeForName(string $typeName, MutableObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): bool { return false; } @@ -215,12 +217,11 @@ public function canExtendTypeForName(string $typeName, ObjectType $type): bool * Extends the existing GraphQL type that is mapped to the $typeName GraphQL type. * * @param string $typeName - * @param ObjectType $type + * @param MutableObjectType $type * @param RecursiveTypeMapperInterface $recursiveTypeMapper - * @return ObjectType * @throws CannotMapTypeExceptionInterface */ - public function extendTypeForName(string $typeName, ObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): ObjectType + 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 b76e053..f9b5a1f 100644 --- a/src/Mappers/RecursiveTypeMapper.php +++ b/src/Mappers/RecursiveTypeMapper.php @@ -17,6 +17,7 @@ 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. @@ -94,11 +95,11 @@ 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 (OutputType&ObjectType)|(OutputType&InterfaceType)|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, ?OutputType $subType): ObjectType + public function mapClassToType(string $className, ?OutputType $subType): MutableObjectType { $closestClassName = $this->findClosestMatchingParent($className); if ($closestClassName === null) { @@ -108,12 +109,21 @@ public function mapClassToType(string $className, ?OutputType $subType): ObjectT // In the event this type was already part of cache, let's not extend it. if ($this->typeRegistry->hasType($type->name)) { - return $type; + $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; + //} } - $type = $this->extendType($className, $type); $this->typeRegistry->registerType($type); + $this->extendType($className, $type); + + $type->freeze(); + return $type; } @@ -137,15 +147,14 @@ private function findClosestMatchingParent(string $className): ?string * Extends a type using available type extenders. * * @param string $className - * @param ObjectType $type - * @return ObjectType + * @param MutableObjectType $type * @throws CannotMapTypeExceptionInterface */ - private function extendType(string $className, ObjectType $type): ObjectType + private function extendType(string $className, MutableObjectType $type): void { $classes = []; do { - if ($this->typeMapper->canExtendTypeForClass($className, $type)) { + if ($this->typeMapper->canExtendTypeForClass($className, $type, $this)) { $classes[] = $className; } } while ($className = get_parent_class($className)); @@ -153,10 +162,8 @@ private function extendType(string $className, ObjectType $type): ObjectType // Let's apply extenders from the most basic type. $classes = array_reverse($classes); foreach ($classes as $class) { - $type = $this->typeMapper->extendTypeForClass($class, $type, $this); + $this->typeMapper->extendTypeForClass($class, $type, $this); } - - return $type; } /** @@ -361,13 +368,25 @@ public function mapNameToType(string $typeName): Type } if ($this->typeMapper->canMapNameToType($typeName)) { $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->getStatus() === MutableObjectType::STATUS_FROZEN) { + return $type; + } + } + if (!$this->typeRegistry->hasType($typeName)) { - if ($type instanceof ObjectType) { - if ($this->typeMapper->canExtendTypeForName($typeName, $type)) { - $type = $this->typeMapper->extendTypeForName($typeName, $type, $this); - } - $this->typeRegistry->registerType($type); + $this->typeRegistry->registerType($type); + } + if ($type instanceof ObjectType) { + if ($this->typeMapper->canExtendTypeForName($typeName, $type, $this)) { + $type = $this->typeMapper->extendTypeForName($typeName, $type, $this); } + $type->freeze(); } return $type; } diff --git a/src/Mappers/RecursiveTypeMapperInterface.php b/src/Mappers/RecursiveTypeMapperInterface.php index 18fd47e..6cf2617 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,11 +30,11 @@ 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 (OutputType&ObjectType)|(OutputType&InterfaceType)|null $subType An optional sub-type if the main class is an iterator that needs to be typed. + * @param (OutputType&MutableObjectType)|(OutputType&InterfaceType)|null $subType An optional sub-type if the main class is an iterator that needs to be typed. * @return ObjectType * @throws CannotMapTypeExceptionInterface */ - public function mapClassToType(string $className, ?OutputType $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). diff --git a/src/Mappers/StaticTypeMapper.php b/src/Mappers/StaticTypeMapper.php index a0dbbb9..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) { @@ -187,10 +188,11 @@ public function canMapNameToType(string $typeName): bool * Returns true if this type mapper can extend an existing type for the $className FQCN * * @param string $className - * @param ObjectType $type + * @param MutableObjectType $type + * @param RecursiveTypeMapperInterface $recursiveTypeMapper * @return bool */ - public function canExtendTypeForClass(string $className, ObjectType $type): bool + public function canExtendTypeForClass(string $className, MutableObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): bool { return false; } @@ -199,12 +201,11 @@ public function canExtendTypeForClass(string $className, ObjectType $type): bool * Extends the existing GraphQL type that is mapped to $className. * * @param string $className - * @param ObjectType $type + * @param MutableObjectType $type * @param RecursiveTypeMapperInterface $recursiveTypeMapper - * @return ObjectType * @throws CannotMapTypeExceptionInterface */ - public function extendTypeForClass(string $className, ObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): ObjectType + public function extendTypeForClass(string $className, MutableObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): void { throw CannotMapTypeException::createForExtendType($className, $type); } @@ -213,10 +214,11 @@ public function extendTypeForClass(string $className, ObjectType $type, Recursiv * Returns true if this type mapper can extend an existing type for the $typeName GraphQL type * * @param string $typeName - * @param ObjectType $type + * @param MutableObjectType $type + * @param RecursiveTypeMapperInterface $recursiveTypeMapper * @return bool */ - public function canExtendTypeForName(string $typeName, ObjectType $type): bool + public function canExtendTypeForName(string $typeName, MutableObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): bool { return false; } @@ -225,12 +227,11 @@ public function canExtendTypeForName(string $typeName, ObjectType $type): bool * Extends the existing GraphQL type that is mapped to the $typeName GraphQL type. * * @param string $typeName - * @param ObjectType $type + * @param MutableObjectType $type * @param RecursiveTypeMapperInterface $recursiveTypeMapper - * @return ObjectType * @throws CannotMapTypeExceptionInterface */ - public function extendTypeForName(string $typeName, ObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): ObjectType + 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 36b722f..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. @@ -79,39 +80,39 @@ public function mapClassToInputType(string $className, RecursiveTypeMapperInterf * Returns true if this type mapper can extend an existing type for the $className FQCN * * @param string $className - * @param ObjectType $type + * @param MutableObjectType $type + * @param RecursiveTypeMapperInterface $recursiveTypeMapper * @return bool */ - public function canExtendTypeForClass(string $className, ObjectType $type): 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 ObjectType $type + * @param MutableObjectType $type * @param RecursiveTypeMapperInterface $recursiveTypeMapper - * @return ObjectType * @throws CannotMapTypeExceptionInterface */ - public function extendTypeForClass(string $className, ObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): ObjectType; + 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 ObjectType $type + * @param MutableObjectType $type + * @param RecursiveTypeMapperInterface $recursiveTypeMapper * @return bool */ - public function canExtendTypeForName(string $typeName, ObjectType $type): 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 ObjectType $type + * @param MutableObjectType $type * @param RecursiveTypeMapperInterface $recursiveTypeMapper - * @return ObjectType * @throws CannotMapTypeExceptionInterface */ - public function extendTypeForName(string $typeName, ObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): ObjectType; + public function extendTypeForName(string $typeName, MutableObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): void; } diff --git a/src/TypeGenerator.php b/src/TypeGenerator.php index 72f4204..9c6c88b 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,7 +24,7 @@ class TypeGenerator /** * @var FieldsBuilderFactory */ - private $controllerQueryProviderFactory; + private $fieldsBuilderFactory; /** * @var NamingStrategyInterface */ @@ -33,12 +35,12 @@ class TypeGenerator private $typeRegistry; public function __construct(AnnotationReader $annotationReader, - FieldsBuilderFactory $controllerQueryProviderFactory, + FieldsBuilderFactory $fieldsBuilderFactory, NamingStrategyInterface $namingStrategy, TypeRegistry $typeRegistry) { $this->annotationReader = $annotationReader; - $this->controllerQueryProviderFactory = $controllerQueryProviderFactory; + $this->fieldsBuilderFactory = $fieldsBuilderFactory; $this->namingStrategy = $namingStrategy; $this->typeRegistry = $typeRegistry; } @@ -49,7 +51,7 @@ public function __construct(AnnotationReader $annotationReader, * @return ObjectType * @throws \ReflectionException */ - public function mapAnnotatedObject($annotatedObject, RecursiveTypeMapperInterface $recursiveTypeMapper): ObjectType + public function mapAnnotatedObject($annotatedObject, RecursiveTypeMapperInterface $recursiveTypeMapper): TypeAnnotatedObjectType { $refTypeClass = new \ReflectionClass($annotatedObject); @@ -65,7 +67,9 @@ public function mapAnnotatedObject($annotatedObject, RecursiveTypeMapperInterfac return $this->typeRegistry->getObjectType($typeName); } - return new ObjectType([ + return TypeAnnotatedObjectType::createFromAnnotatedClass($typeName, $typeField->getClass(), $annotatedObject, $this->fieldsBuilderFactory, $recursiveTypeMapper); + + /*return new ObjectType([ 'name' => $typeName, 'fields' => function() use ($annotatedObject, $recursiveTypeMapper, $typeField) { $parentClass = get_parent_class($typeField->getClass()); @@ -86,15 +90,15 @@ public function mapAnnotatedObject($annotatedObject, RecursiveTypeMapperInterfac 'interfaces' => function() use ($typeField, $recursiveTypeMapper) { return $recursiveTypeMapper->findInterfaces($typeField->getClass()); } - ]); + ]);*/ } /** * @param object $annotatedObject An object with a ExtendType annotation. - * @param ObjectType $type + * @param MutableObjectType $type * @param RecursiveTypeMapperInterface $recursiveTypeMapper */ - public function extendAnnotatedObject($annotatedObject, ObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper) + public function extendAnnotatedObject($annotatedObject, MutableObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper) { $refTypeClass = new \ReflectionClass($annotatedObject); @@ -107,13 +111,15 @@ public function extendAnnotatedObject($annotatedObject, ObjectType $type, Recurs //$typeName = $this->namingStrategy->getOutputTypeName($refTypeClass->getName(), $extendTypeAnnotation); $typeName = $type->name; - if ($this->typeRegistry->hasType($typeName)) { + /*if ($this->typeRegistry->hasType($typeName)) { throw new GraphQLException(sprintf('Tried to extend GraphQL type "%s" that is already stored in the type registry.', $typeName)); } - return new ObjectType([ - 'name' => $typeName, - 'fields' => function() use ($annotatedObject, $recursiveTypeMapper, $type) { + 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) { @@ -122,19 +128,38 @@ public function extendAnnotatedObject($annotatedObject, ObjectType $type, Recurs } }*/ - $fieldProvider = $this->controllerQueryProviderFactory->buildFieldsBuilder($recursiveTypeMapper); - $fields = $fieldProvider->getFields($annotatedObject); + $fieldProvider = $this->fieldsBuilderFactory->buildFieldsBuilder($recursiveTypeMapper); + return $fieldProvider->getFields($annotatedObject); /*if ($parentType !== null) { $fields = $parentType->getFields() + $fields; }*/ - - $fields = $type->getFields() + $fields; - - return $fields; - }, - 'interfaces' => function() use ($type) { - return $type->getInterfaces(); - } - ]); + }); + + +// 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/Types/MutableObjectType.php b/src/Types/MutableObjectType.php new file mode 100644 index 0000000..752ebba --- /dev/null +++ b/src/Types/MutableObjectType.php @@ -0,0 +1,115 @@ + + */ + private $fieldsCallables = []; + + /** + * @var FieldDefinition[]|null + */ + private $finalFields; + + /** + * @param object $annotatedObject + * @param RecursiveTypeMapperInterface $recursiveTypeMapper + * @param Type $typeField + */ + 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..1e28596 --- /dev/null +++ b/src/Types/TypeAnnotatedObjectType.php @@ -0,0 +1,63 @@ +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 8b08bb7..90a38ee 100644 --- a/tests/AbstractQueryProviderTest.php +++ b/tests/AbstractQueryProviderTest.php @@ -30,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; @@ -50,10 +51,10 @@ abstract class AbstractQueryProviderTest extends TestCase 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(), @@ -63,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(), @@ -76,7 +77,7 @@ protected function getTestObjectType2() return $this->testObjectType2; } - protected function getInputTestObjectType() + protected function getInputTestObjectType(): InputObjectType { if ($this->inputTestObjectType === null) { $this->inputTestObjectType = new InputObjectType([ @@ -126,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; @@ -182,22 +183,22 @@ public function canMapNameToType(string $typeName): bool return $typeName === 'TestObject' || $typeName === 'TestObject2' || $typeName === 'TestObjectInput'; } - public function canExtendTypeForClass(string $className, ObjectType $type): bool + public function canExtendTypeForClass(string $className, MutableObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): bool { return false; } - public function extendTypeForClass(string $className, ObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): ObjectType + public function extendTypeForClass(string $className, MutableObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): void { throw CannotMapTypeException::createForExtendType($className, $type); } - public function canExtendTypeForName(string $typeName, ObjectType $type): bool + public function canExtendTypeForName(string $typeName, MutableObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): bool { return false; } - public function extendTypeForName(string $typeName, ObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): ObjectType + public function extendTypeForName(string $typeName, MutableObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): void { throw CannotMapTypeException::createForExtendName($typeName, $type); } diff --git a/tests/Integration/EndToEndTest.php b/tests/Integration/EndToEndTest.php index eb1732a..820f7a5 100644 --- a/tests/Integration/EndToEndTest.php +++ b/tests/Integration/EndToEndTest.php @@ -317,6 +317,7 @@ public function testEndToEndPorpaginas() 'items' => [ [ 'name' => 'Bill', + 'uppercaseName' => 'BILL', 'email' => 'bill@example.com' ] ], diff --git a/tests/Mappers/CompositeTypeMapperTest.php b/tests/Mappers/CompositeTypeMapperTest.php index f95cbdf..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(), @@ -103,22 +104,22 @@ public function canMapNameToType(string $typeName): bool return $typeName === 'TestObject'; } - public function canExtendTypeForClass(string $className, ObjectType $type): bool + public function canExtendTypeForClass(string $className, MutableObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): bool { return false; } - public function extendTypeForClass(string $className, ObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): ObjectType + public function extendTypeForClass(string $className, MutableObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): void { throw CannotMapTypeException::createForExtendType($className, $type); } - public function canExtendTypeForName(string $typeName, ObjectType $type): bool + public function canExtendTypeForName(string $typeName, MutableObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): bool { return false; } - public function extendTypeForName(string $typeName, ObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): ObjectType + public function extendTypeForName(string $typeName, MutableObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): void { throw CannotMapTypeException::createForExtendName($typeName, $type); } diff --git a/tests/Mappers/GlobTypeMapperTest.php b/tests/Mappers/GlobTypeMapperTest.php index 70d89f1..5478c00 100644 --- a/tests/Mappers/GlobTypeMapperTest.php +++ b/tests/Mappers/GlobTypeMapperTest.php @@ -171,16 +171,16 @@ public function testGlobTypeMapperExtend() $type = $mapper->mapClassToType(TestObject::class, null, $this->getTypeMapper()); - $this->assertTrue($mapper->canExtendTypeForClass(TestObject::class, $type)); - $this->assertInstanceOf(ObjectType::class, $mapper->extendTypeForClass(TestObject::class, $type, $this->getTypeMapper())); - $this->assertInstanceOf(ObjectType::class, $mapper->extendTypeForName('Foo', $type, $this->getTypeMapper())); - $this->assertTrue($mapper->canExtendTypeForName('Foo')); - $this->assertFalse($mapper->canExtendTypeForName('NotExists')); + $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->assertTrue($anotherMapperSameCache->canExtendTypeForName('Foo', $type)); + $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 f77f23b..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' ]); @@ -46,7 +47,7 @@ public function testMapClassToType() public function testMapNameToType() { - $objectType = new ObjectType([ + $objectType = new MutableObjectType([ 'name' => 'Foobar' ]); 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()); } From 9327194ef3707658a0927d4c3cadd128c03a6436 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Tue, 15 Jan 2019 10:59:14 +0100 Subject: [PATCH 4/6] Fixing PHPStan --- src/Mappers/GlobTypeMapper.php | 2 +- src/Mappers/PorpaginasTypeMapper.php | 4 ++-- src/Mappers/RecursiveTypeMapper.php | 6 +++--- src/Mappers/RecursiveTypeMapperInterface.php | 2 +- src/TypeGenerator.php | 6 +++--- src/TypeRegistry.php | 9 +++++---- src/Types/MutableObjectType.php | 7 +------ src/Types/TypeAnnotatedObjectType.php | 5 ----- 8 files changed, 16 insertions(+), 25 deletions(-) diff --git a/src/Mappers/GlobTypeMapper.php b/src/Mappers/GlobTypeMapper.php index c58fc46..832cb2f 100644 --- a/src/Mappers/GlobTypeMapper.php +++ b/src/Mappers/GlobTypeMapper.php @@ -573,7 +573,7 @@ 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): MutableObjectType diff --git a/src/Mappers/PorpaginasTypeMapper.php b/src/Mappers/PorpaginasTypeMapper.php index 0aad6ac..d96197b 100644 --- a/src/Mappers/PorpaginasTypeMapper.php +++ b/src/Mappers/PorpaginasTypeMapper.php @@ -20,7 +20,7 @@ class PorpaginasTypeMapper implements TypeMapperInterface { /** - * @var array + * @var array */ private $cache = []; @@ -41,7 +41,7 @@ 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): MutableObjectType diff --git a/src/Mappers/RecursiveTypeMapper.php b/src/Mappers/RecursiveTypeMapper.php index f9b5a1f..0ce0989 100644 --- a/src/Mappers/RecursiveTypeMapper.php +++ b/src/Mappers/RecursiveTypeMapper.php @@ -374,7 +374,7 @@ public function mapNameToType(string $typeName): Type if ($cachedType !== $type) { throw new \RuntimeException('Cached type in registry is not the type returned by type mapper.'); } - if ($cachedType->getStatus() === MutableObjectType::STATUS_FROZEN) { + if ($cachedType instanceof MutableObjectType && $cachedType->getStatus() === MutableObjectType::STATUS_FROZEN) { return $type; } } @@ -382,9 +382,9 @@ public function mapNameToType(string $typeName): Type if (!$this->typeRegistry->hasType($typeName)) { $this->typeRegistry->registerType($type); } - if ($type instanceof ObjectType) { + if ($type instanceof MutableObjectType) { if ($this->typeMapper->canExtendTypeForName($typeName, $type, $this)) { - $type = $this->typeMapper->extendTypeForName($typeName, $type, $this); + $this->typeMapper->extendTypeForName($typeName, $type, $this); } $type->freeze(); } diff --git a/src/Mappers/RecursiveTypeMapperInterface.php b/src/Mappers/RecursiveTypeMapperInterface.php index 6cf2617..98a54c2 100644 --- a/src/Mappers/RecursiveTypeMapperInterface.php +++ b/src/Mappers/RecursiveTypeMapperInterface.php @@ -31,7 +31,7 @@ public function canMapClassToType(string $className): bool; * * @param string $className The class name to look for (this function looks into parent classes if the class does not match a type). * @param (OutputType&MutableObjectType)|(OutputType&InterfaceType)|null $subType An optional sub-type if the main class is an iterator that needs to be typed. - * @return ObjectType + * @return MutableObjectType * @throws CannotMapTypeExceptionInterface */ public function mapClassToType(string $className, ?OutputType $subType): MutableObjectType; diff --git a/src/TypeGenerator.php b/src/TypeGenerator.php index 9c6c88b..ad5bcb9 100644 --- a/src/TypeGenerator.php +++ b/src/TypeGenerator.php @@ -48,10 +48,10 @@ public function __construct(AnnotationReader $annotationReader, /** * @param object $annotatedObject An object with a Type annotation. * @param RecursiveTypeMapperInterface $recursiveTypeMapper - * @return ObjectType + * @return MutableObjectType * @throws \ReflectionException */ - public function mapAnnotatedObject($annotatedObject, RecursiveTypeMapperInterface $recursiveTypeMapper): TypeAnnotatedObjectType + public function mapAnnotatedObject($annotatedObject, RecursiveTypeMapperInterface $recursiveTypeMapper): MutableObjectType { $refTypeClass = new \ReflectionClass($annotatedObject); @@ -64,7 +64,7 @@ public function mapAnnotatedObject($annotatedObject, RecursiveTypeMapperInterfac $typeName = $this->namingStrategy->getOutputTypeName($refTypeClass->getName(), $typeField); if ($this->typeRegistry->hasType($typeName)) { - return $this->typeRegistry->getObjectType($typeName); + return $this->typeRegistry->getMutableObjectType($typeName); } return TypeAnnotatedObjectType::createFromAnnotatedClass($typeName, $typeField->getClass(), $annotatedObject, $this->fieldsBuilderFactory, $recursiveTypeMapper); diff --git a/src/TypeRegistry.php b/src/TypeRegistry.php index 9325369..5012813 100644 --- a/src/TypeRegistry.php +++ b/src/TypeRegistry.php @@ -8,6 +8,7 @@ use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\InterfaceType; use GraphQL\Type\Definition\Type; +use TheCodingMachine\GraphQL\Controllers\Types\MutableObjectType; /** * A cache used to store already FULLY COMPUTED types. @@ -15,7 +16,7 @@ class TypeRegistry { /** - * @var array + * @var array */ private $outputTypes = []; @@ -51,11 +52,11 @@ public function getType(string $typeName): NamedType return $this->outputTypes[$typeName]; } - public function getObjectType(string $typeName): ObjectType + public function getMutableObjectType(string $typeName): MutableObjectType { $type = $this->getType($typeName); - if (!$type instanceof ObjectType) { - throw new GraphQLException('Expected GraphQL type "'.$typeName.'" to be an ObjectType. Got a '.get_class($type)); + 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 index 752ebba..89a014c 100644 --- a/src/Types/MutableObjectType.php +++ b/src/Types/MutableObjectType.php @@ -20,7 +20,7 @@ class MutableObjectType extends ObjectType public const STATUS_FROZEN = 'frozen'; /** - * @var bool + * @var string */ private $status; @@ -34,11 +34,6 @@ class MutableObjectType extends ObjectType */ private $finalFields; - /** - * @param object $annotatedObject - * @param RecursiveTypeMapperInterface $recursiveTypeMapper - * @param Type $typeField - */ public function __construct(array $config) { $this->status = self::STATUS_PENDING; diff --git a/src/Types/TypeAnnotatedObjectType.php b/src/Types/TypeAnnotatedObjectType.php index 1e28596..9484583 100644 --- a/src/Types/TypeAnnotatedObjectType.php +++ b/src/Types/TypeAnnotatedObjectType.php @@ -18,11 +18,6 @@ class TypeAnnotatedObjectType extends MutableObjectType */ private $className; - /** - * @param object $annotatedObject - * @param RecursiveTypeMapperInterface $recursiveTypeMapper - * @param Type $typeField - */ public function __construct(string $className, array $config) { $this->className = $className; From c5223fcfbf670556e579f9479b4abe1700d97a2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Tue, 15 Jan 2019 11:03:51 +0100 Subject: [PATCH 5/6] Fixing travis conf --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 94b6088..061fb68 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,8 +2,8 @@ language: php cache: directories: - $HOME/.composer/cache -env: - matrix: +matrix: + include: - php: 7.3 env: PREFER_LOWEST="" - php: 7.2 From b71fb345dd8463f8676a58aa9fa5603b66942c4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Tue, 15 Jan 2019 11:11:12 +0100 Subject: [PATCH 6/6] Starting to switch to descriptors of types instead of types --- src/Cache/CacheValidatorInterface.php | 19 +++ ...ileModificationTimeCacheValidatorTrait.php | 45 +++++++ src/Cache/InvalidCacheItemException.php | 13 ++ src/Cache/SelfValidatingCache.php | 52 ++++++++ src/Cache/SelfValidatingCacheInterface.php | 28 ++++ src/Schema/DescriptorInterface.php | 17 +++ .../ObjectType/ObjectTypeDescriptor.php | 126 ++++++++++++++++++ .../ObjectTypeDescriptorInterface.php | 38 ++++++ ...odificationTimeCacheValidatorTraitTest.php | 34 +++++ 9 files changed, 372 insertions(+) create mode 100644 src/Cache/CacheValidatorInterface.php create mode 100644 src/Cache/FileModificationTimeCacheValidatorTrait.php create mode 100644 src/Cache/InvalidCacheItemException.php create mode 100644 src/Cache/SelfValidatingCache.php create mode 100644 src/Cache/SelfValidatingCacheInterface.php create mode 100644 src/Schema/DescriptorInterface.php create mode 100644 src/Schema/ObjectType/ObjectTypeDescriptor.php create mode 100644 src/Schema/ObjectType/ObjectTypeDescriptorInterface.php create mode 100644 tests/Cache/FileModificationTimeCacheValidatorTraitTest.php 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 @@ + + */ + 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/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); + } +}