From eb783f382d4e591f281e7029393f930d023b85d6 Mon Sep 17 00:00:00 2001 From: Andrew Maslov Date: Wed, 20 May 2020 22:52:19 +0300 Subject: [PATCH 01/41] Implemented fields on properties --- src/AnnotationReader.php | 77 +++++- src/Annotations/FailWith.php | 2 +- src/Annotations/Field.php | 2 +- src/Annotations/HideIfUnauthorized.php | 2 +- src/Annotations/Logged.php | 2 +- src/Annotations/Right.php | 2 +- src/Annotations/Security.php | 2 +- src/FieldsBuilder.php | 253 ++++++++++++++---- src/InvalidDocBlockRuntimeException.php | 11 + src/InvalidPrefetchMethodRuntimeException.php | 14 +- src/Mappers/DuplicateMappingException.php | 45 +++- src/Mappers/Parameters/TypeHandler.php | 56 +++- src/Mappers/Root/BaseTypeMapper.php | 17 +- src/Mappers/Root/CompoundTypeMapper.php | 11 +- src/Mappers/Root/FinalRootTypeMapper.php | 5 +- src/Mappers/Root/IteratorTypeMapper.php | 21 +- src/Mappers/Root/MyCLabsEnumTypeMapper.php | 9 +- .../Root/NullableTypeMapperAdapter.php | 9 +- src/Mappers/Root/RootTypeMapperInterface.php | 7 +- src/Middlewares/MagicPropertyResolver.php | 2 +- src/Middlewares/SourcePropertyResolver.php | 83 ++++++ src/Middlewares/SourceResolver.php | 2 +- src/Middlewares/SourceResolverInterface.php | 17 ++ src/QueryField.php | 7 +- src/QueryFieldDescriptor.php | 36 ++- src/Reflection/CachedDocBlockFactory.php | 29 +- src/Utils/PropertyAccessor.php | 48 ++++ tests/Mappers/Root/VoidRootTypeMapper.php | 9 +- 28 files changed, 643 insertions(+), 137 deletions(-) create mode 100644 src/Middlewares/SourcePropertyResolver.php create mode 100644 src/Middlewares/SourceResolverInterface.php create mode 100644 src/Utils/PropertyAccessor.php diff --git a/src/AnnotationReader.php b/src/AnnotationReader.php index cffda2a7fb..58db700531 100644 --- a/src/AnnotationReader.php +++ b/src/AnnotationReader.php @@ -11,6 +11,7 @@ use ReflectionClass; use ReflectionMethod; use ReflectionParameter; +use ReflectionProperty; use RuntimeException; use TheCodingMachine\GraphQLite\Annotations\AbstractRequest; use TheCodingMachine\GraphQLite\Annotations\Decorate; @@ -64,6 +65,21 @@ class AnnotationReader */ private $mode; + /** + * @var object[] + */ + private $methodAnnotationCache = []; + + /** + * @var object[][] + */ + private $methodAnnotationsCache = []; + + /** + * @var object[][] + */ + private $propertyAnnotationsCache = []; + /** * @param string $mode One of self::LAX_MODE or self::STRICT_MODE * @param string[] $strictNamespaces @@ -219,10 +235,21 @@ public function getParameterAnnotationsPerParameter(array $refParameters): array }, $parameterAnnotationsPerParameter); } - public function getMiddlewareAnnotations(ReflectionMethod $refMethod): MiddlewareAnnotations + /** + * @param ReflectionMethod|ReflectionProperty $reflection + * + * @return MiddlewareAnnotations + * + * @throws AnnotationException + */ + public function getMiddlewareAnnotations($reflection): MiddlewareAnnotations { /** @var MiddlewareAnnotationInterface[] $middlewareAnnotations */ - $middlewareAnnotations = $this->getMethodAnnotations($refMethod, MiddlewareAnnotationInterface::class); + if ($reflection instanceof ReflectionMethod) { + $middlewareAnnotations = $this->getMethodAnnotations($reflection, MiddlewareAnnotationInterface::class); + } else { + $middlewareAnnotations = $this->getPropertyAnnotations($reflection, MiddlewareAnnotationInterface::class); + } return new MiddlewareAnnotations($middlewareAnnotations); } @@ -263,9 +290,6 @@ private function getClassAnnotation(ReflectionClass $refClass, string $annotatio return $type; } - /** @var array */ - private $methodAnnotationCache = []; - /** * Returns a method annotation and handles correctly errors. */ @@ -352,9 +376,6 @@ public function getClassAnnotations(ReflectionClass $refClass, string $annotatio return []; } - /** @var array> */ - private $methodAnnotationsCache = []; - /** * Returns the method's annotations. * @@ -396,6 +417,46 @@ public function getMethodAnnotations(ReflectionMethod $refMethod, string $annota return $toAddAnnotations; } + /** + * Returns the property's annotations. + * + * @param ReflectionProperty $refProperty + * @param string $annotationClass + * + * @return array + * + * @throws AnnotationException + */ + public function getPropertyAnnotations(ReflectionProperty $refProperty, string $annotationClass): array + { + $cacheKey = $refProperty->getDeclaringClass()->getName() . '::' . $refProperty->getName() . '_s_' . $annotationClass; + if (isset($this->propertyAnnotationsCache[$cacheKey])) { + return $this->propertyAnnotationsCache[$cacheKey]; + } + + $toAddAnnotations = []; + try { + $allAnnotations = $this->reader->getPropertyAnnotations($refProperty); + $toAddAnnotations = array_filter($allAnnotations, static function ($annotation) use ($annotationClass): bool { + return $annotation instanceof $annotationClass; + }); + } catch (AnnotationException $e) { + if ($this->mode === self::STRICT_MODE) { + throw $e; + } + + if ($this->mode === self::LAX_MODE) { + if ($this->isErrorImportant($annotationClass, $refProperty->getDocComment() ?: '', $refProperty->getDeclaringClass()->getName())) { + throw $e; + } + } + } + + $this->propertyAnnotationsCache[$cacheKey] = $toAddAnnotations; + + return $toAddAnnotations; + } + /** * @param ReflectionClass $refClass */ diff --git a/src/Annotations/FailWith.php b/src/Annotations/FailWith.php index 5eba69eb6d..7a68f56f61 100644 --- a/src/Annotations/FailWith.php +++ b/src/Annotations/FailWith.php @@ -9,7 +9,7 @@ /** * @Annotation - * @Target({"METHOD", "ANNOTATION"}) + * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) * @Attributes({ * @Attribute("value", type = "mixed"), * @Attribute("mode", type = "string") diff --git a/src/Annotations/Field.php b/src/Annotations/Field.php index 8b4273c3e0..6aefb6ee4e 100644 --- a/src/Annotations/Field.php +++ b/src/Annotations/Field.php @@ -6,7 +6,7 @@ /** * @Annotation - * @Target({"METHOD"}) + * @Target({"PROPERTY", "METHOD"}) * @Attributes({ * @Attribute("name", type = "string"), * @Attribute("outputType", type = "string"), diff --git a/src/Annotations/HideIfUnauthorized.php b/src/Annotations/HideIfUnauthorized.php index 9e84283f60..335d48c535 100644 --- a/src/Annotations/HideIfUnauthorized.php +++ b/src/Annotations/HideIfUnauthorized.php @@ -9,7 +9,7 @@ * or has no right associated. * * @Annotation - * @Target({"METHOD", "ANNOTATION"}) + * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) */ class HideIfUnauthorized implements MiddlewareAnnotationInterface { diff --git a/src/Annotations/Logged.php b/src/Annotations/Logged.php index b21364f174..25d290aa2e 100644 --- a/src/Annotations/Logged.php +++ b/src/Annotations/Logged.php @@ -6,7 +6,7 @@ /** * @Annotation - * @Target({"METHOD", "ANNOTATION"}) + * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) */ class Logged implements MiddlewareAnnotationInterface { diff --git a/src/Annotations/Right.php b/src/Annotations/Right.php index db691a9581..e19338b6a8 100644 --- a/src/Annotations/Right.php +++ b/src/Annotations/Right.php @@ -8,7 +8,7 @@ /** * @Annotation - * @Target({"ANNOTATION", "METHOD"}) + * @Target({"PROPERTY", "ANNOTATION", "METHOD"}) * @Attributes({ * @Attribute("name", type = "string"), * }) diff --git a/src/Annotations/Security.php b/src/Annotations/Security.php index e4a4c21a70..4d14fa6392 100644 --- a/src/Annotations/Security.php +++ b/src/Annotations/Security.php @@ -9,7 +9,7 @@ /** * @Annotation - * @Target({"ANNOTATION", "METHOD"}) + * @Target({"PROPERTY", "ANNOTATION", "METHOD"}) * @Attributes({ * @Attribute("expression", type = "string"), * @Attribute("failWith", type = "mixed"), diff --git a/src/FieldsBuilder.php b/src/FieldsBuilder.php index 7e53822e9e..1dcd2e2274 100644 --- a/src/FieldsBuilder.php +++ b/src/FieldsBuilder.php @@ -4,14 +4,19 @@ namespace TheCodingMachine\GraphQLite; +use Doctrine\Common\Annotations\AnnotationException; use GraphQL\Type\Definition\FieldDefinition; use GraphQL\Type\Definition\OutputType; use GraphQL\Type\Definition\Type; use phpDocumentor\Reflection\DocBlock; +use phpDocumentor\Reflection\DocBlock\Tags\Var_; +use Psr\SimpleCache\InvalidArgumentException; +use TheCodingMachine\GraphQLite\Utils\PropertyAccessor; use ReflectionClass; use ReflectionException; use ReflectionMethod; use ReflectionParameter; +use ReflectionProperty; use TheCodingMachine\GraphQLite\Annotations\Exceptions\InvalidParameterException; use TheCodingMachine\GraphQLite\Annotations\Field; use TheCodingMachine\GraphQLite\Annotations\Mutation; @@ -43,7 +48,6 @@ use function reset; use function rtrim; use function trim; -use function ucfirst; /** * A class in charge if returning list of fields for queries / mutations / entities / input types @@ -208,10 +212,10 @@ public function getParametersForDecorator(ReflectionMethod $refMethod): array private function getFieldsByAnnotations($controller, string $annotationName, bool $injectSource): array { $refClass = new ReflectionClass($controller); - $queryList = []; - /** @var array $refMethodByFields */ - $refMethodByFields = []; + + /** @var ReflectionMethod[]|ReflectionProperty[] $reflectorByFields */ + $reflectorByFields = []; $oldDeclaringClass = null; $context = null; @@ -224,30 +228,66 @@ private function getFieldsByAnnotations($controller, string $annotationName, boo } } - foreach ($refClass->getMethods(ReflectionMethod::IS_PUBLIC) as $refMethod) { - if ($closestMatchingTypeClass !== null && $closestMatchingTypeClass === $refMethod->getDeclaringClass()->getName()) { + /** @var ReflectionProperty[]|ReflectionMethod[] $reflectors */ + $reflectors = array_merge($refClass->getProperties(), $refClass->getMethods(ReflectionMethod::IS_PUBLIC)); + foreach ($reflectors as $reflector) { + if ($closestMatchingTypeClass !== null && $closestMatchingTypeClass === $reflector->getDeclaringClass()->getName()) { // Optimisation: no need to fetch annotations from parent classes that are ALREADY GraphQL types. // We will merge the fields anyway. break; } - // First, let's check the "Query" or "Mutation" or "Field" annotation - $queryAnnotation = $this->annotationReader->getRequestAnnotation($refMethod, $annotationName); + if ($reflector instanceof ReflectionMethod) { + $fields = $this->getFieldsByMethodAnnotations($controller, $refClass, $reflector, $annotationName, $injectSource); + } else { + $fields = $this->getFieldsByPropertyAnnotations($controller, $refClass, $reflector, $annotationName); + } - if ($queryAnnotation === null) { - continue; + $duplicates = array_intersect_key($reflectorByFields, $fields); + if ($duplicates) { + $name = key($duplicates); + throw DuplicateMappingException::createForQuery($refClass->getName(), $name, $reflectorByFields[$name], $reflector); } + $reflectorByFields = array_merge( + $reflectorByFields, + array_fill_keys(array_keys($fields), $reflector) + ); + + $queryList = array_merge($queryList, $fields); + } + + return $queryList; + } + + /** + * Gets fields by class method annotations. + * + * @param string|object $controller + * @param ReflectionClass $refClass + * @param ReflectionMethod $refMethod + * @param string $annotationName + * @param bool $injectSource + * + * @return array + * + * @throws AnnotationException + * @throws InvalidArgumentException + * @throws CannotMapTypeExceptionInterface + */ + private function getFieldsByMethodAnnotations($controller, ReflectionClass $refClass, ReflectionMethod $refMethod, string $annotationName, bool $injectSource): array + { + $fields = []; + + $annotations = $this->annotationReader->getMethodAnnotations($refMethod, $annotationName); + foreach ($annotations as $queryAnnotation) { $fieldDescriptor = new QueryFieldDescriptor(); $fieldDescriptor->setRefMethod($refMethod); $docBlockObj = $this->cachedDocBlockFactory->getDocBlock($refMethod); $docBlockComment = $docBlockObj->getSummary() . "\n" . $docBlockObj->getDescription()->render(); - $deprecated = $docBlockObj->getTagsByName('deprecated'); - if (count($deprecated) >= 1) { - $fieldDescriptor->setDeprecationReason(trim((string) $deprecated[0])); - } + $fieldDescriptor->setDeprecationReason($this->getDeprecationReason($docBlockObj)); $methodName = $refMethod->getName(); $name = $queryAnnotation->getName() ?: $this->namingStrategy->getFieldNameFromMethodName($methodName); @@ -255,27 +295,10 @@ private function getFieldsByAnnotations($controller, string $annotationName, boo $fieldDescriptor->setName($name); $fieldDescriptor->setComment($docBlockComment); - // Get parameters from the prefetchMethod method if any. - $prefetchMethodName = null; - $prefetchArgs = []; - if ($queryAnnotation instanceof Field) { - $prefetchMethodName = $queryAnnotation->getPrefetchMethod(); - if ($prefetchMethodName !== null) { - $fieldDescriptor->setPrefetchMethodName($prefetchMethodName); - try { - $prefetchRefMethod = $refClass->getMethod($prefetchMethodName); - } catch (ReflectionException $e) { - throw InvalidPrefetchMethodRuntimeException::methodNotFound($refMethod, $refClass, $prefetchMethodName, $e); - } - - $prefetchParameters = $prefetchRefMethod->getParameters(); - $firstPrefetchParameter = array_shift($prefetchParameters); - - $prefetchDocBlockObj = $this->cachedDocBlockFactory->getDocBlock($prefetchRefMethod); - - $prefetchArgs = $this->mapParameters($prefetchParameters, $prefetchDocBlockObj); - $fieldDescriptor->setPrefetchParameters($prefetchArgs); - } + [$prefetchMethodName, $prefetchArgs, $prefetchRefMethod] = $this->getPrefetchMethodInfo($refClass, $refMethod, $queryAnnotation); + if ($prefetchMethodName) { + $fieldDescriptor->setPrefetchMethodName($prefetchMethodName); + $fieldDescriptor->setPrefetchParameters($prefetchArgs); } $parameters = $refMethod->getParameters(); @@ -326,26 +349,96 @@ public function handle(QueryFieldDescriptor $fieldDescriptor): ?FieldDefinition }); if ($field !== null) { - if (isset($refMethodByFields[$name])) { - throw DuplicateMappingException::createForQuery($refClass->getName(), $name, $refMethodByFields[$name], $refMethod); + if (isset($fields[$name])) { + throw DuplicateMappingException::createForQueryInOneMethod($name, $refMethod); } - $queryList[$name] = $field; - $refMethodByFields[$name] = $refMethod; + $fields[$name] = $field; } + } - /*if ($unauthorized) { - $failWithValue = $failWith->getValue(); - $queryList[] = QueryField::alwaysReturn($fieldDescriptor, $failWithValue); - } elseif ($sourceClassName !== null) { - $fieldDescriptor->setTargetMethodOnSource($methodName); - $queryList[] = QueryField::selfField($fieldDescriptor); + return $fields; + } + + /** + * Gets fields by class property annotations. + * + * @param string|object $controller + * @param ReflectionClass $refClass + * @param ReflectionProperty $refProperty + * @param string $annotationName + * + * @return array + * + * @throws AnnotationException + * @throws InvalidArgumentException + * @throws CannotMapTypeException + */ + private function getFieldsByPropertyAnnotations($controller, ReflectionClass $refClass, ReflectionProperty $refProperty, string $annotationName): array + { + $fields = []; + + $annotations = $this->annotationReader->getPropertyAnnotations($refProperty, $annotationName); + foreach ($annotations as $queryAnnotation) { + $fieldDescriptor = new QueryFieldDescriptor(); + $fieldDescriptor->setRefProperty($refProperty); + + $docBlock = $this->cachedDocBlockFactory->getDocBlock($refProperty); + $docBlockComment = $docBlock->getSummary() . PHP_EOL . $docBlock->getDescription()->render(); + + /** @var Var_[] $varTags */ + $varTags = $docBlock->getTagsByName('var'); + if ($varTag = reset($varTags)) { + $docBlockComment .= PHP_EOL . $varTag->getDescription(); + } + + $fieldDescriptor->setDeprecationReason($this->getDeprecationReason($docBlock)); + + $name = $queryAnnotation->getName() ?: $refProperty->getName(); + $fieldDescriptor->setName($name); + $fieldDescriptor->setComment($docBlockComment); + + [$prefetchMethodName, $prefetchArgs] = $this->getPrefetchMethodInfo($refClass, $refProperty, $queryAnnotation); + if ($prefetchMethodName) { + $fieldDescriptor->setPrefetchMethodName($prefetchMethodName); + $fieldDescriptor->setPrefetchParameters($prefetchArgs); + } + + $outputType = $queryAnnotation->getOutputType(); + if ($outputType) { + $type = $this->typeResolver->mapNameToOutputType($outputType); } else { - $fieldDescriptor->setCallable([$controller, $methodName]); - $queryList[] = QueryField::externalField($fieldDescriptor); - }*/ + $type = $this->typeMapper->mapProperty($refProperty, $docBlock, false); + } + $fieldDescriptor->setType($type); + $fieldDescriptor->setInjectSource(false); + + if (is_string($controller)) { + $fieldDescriptor->setTargetPropertyOnSource($refProperty->getName()); + } else { + $fieldDescriptor->setCallable(function () use ($controller, $refProperty) { + /** @var $controller object */ + return PropertyAccessor::getValue($controller, $refProperty->getName()); + }); + } + + $fieldDescriptor->setMiddlewareAnnotations($this->annotationReader->getMiddlewareAnnotations($refProperty)); + + $field = $this->fieldMiddleware->process($fieldDescriptor, new class implements FieldHandlerInterface { + public function handle(QueryFieldDescriptor $fieldDescriptor): ?FieldDefinition + { + return QueryField::fromFieldDescriptor($fieldDescriptor); + } + }); + + if ($field !== null) { + if (isset($fields[$name])) { + throw DuplicateMappingException::createForQueryInOneProperty($name, $refProperty); + } + $fields[$name] = $field; + } } - return $queryList; + return $fields; } /** @@ -508,12 +601,8 @@ private function getMethodFromPropertyName(ReflectionClass $reflectionClass, str if ($reflectionClass->hasMethod($propertyName)) { $methodName = $propertyName; } else { - $upperCasePropertyName = ucfirst($propertyName); - if ($reflectionClass->hasMethod('get' . $upperCasePropertyName)) { - $methodName = 'get' . $upperCasePropertyName; - } elseif ($reflectionClass->hasMethod('is' . $upperCasePropertyName)) { - $methodName = 'is' . $upperCasePropertyName; - } else { + $methodName = PropertyAccessor::findGetter($reflectionClass->getName(), $propertyName); + if (!$methodName) { throw FieldNotFoundException::missingField($reflectionClass->getName(), $propertyName); } } @@ -571,4 +660,58 @@ private function mapParameters(array $refParameters, DocBlock $docBlock, ?Source return $args; } + + /** + * Extracts deprecation reason from doc block. + * + * @param DocBlock $docBlockObj + * + * @return string|null + */ + private function getDeprecationReason(DocBlock $docBlockObj): ?string + { + $deprecated = $docBlockObj->getTagsByName('deprecated'); + if (count($deprecated) >= 1) { + return trim((string) $deprecated[0]); + } + + return null; + } + + /** + * Extracts prefetch method info from annotation. + * + * @param ReflectionClass $refClass + * @param ReflectionMethod|ReflectionProperty $reflector + * @param object $annotation + * + * @return array + * + * @throws InvalidArgumentException + */ + private function getPrefetchMethodInfo(ReflectionClass $refClass, $reflector, object $annotation): array + { + $prefetchMethodName = null; + $prefetchArgs = []; + $prefetchRefMethod = null; + + if ($annotation instanceof Field) { + $prefetchMethodName = $annotation->getPrefetchMethod(); + if ($prefetchMethodName !== null) { + try { + $prefetchRefMethod = $refClass->getMethod($prefetchMethodName); + } catch (ReflectionException $e) { + throw InvalidPrefetchMethodRuntimeException::methodNotFound($reflector, $refClass, $prefetchMethodName, $e); + } + + $prefetchParameters = $prefetchRefMethod->getParameters(); + array_shift($prefetchParameters); + + $prefetchDocBlockObj = $this->cachedDocBlockFactory->getDocBlock($prefetchRefMethod); + $prefetchArgs = $this->mapParameters($prefetchParameters, $prefetchDocBlockObj); + } + } + + return [$prefetchMethodName, $prefetchArgs, $prefetchRefMethod]; + } } diff --git a/src/InvalidDocBlockRuntimeException.php b/src/InvalidDocBlockRuntimeException.php index 760c3a9a18..606ae19d8e 100644 --- a/src/InvalidDocBlockRuntimeException.php +++ b/src/InvalidDocBlockRuntimeException.php @@ -5,6 +5,7 @@ namespace TheCodingMachine\GraphQLite; use ReflectionMethod; +use ReflectionProperty; class InvalidDocBlockRuntimeException extends GraphQLRuntimeException { @@ -12,4 +13,14 @@ public static function tooManyReturnTags(ReflectionMethod $refMethod): self { throw new self('Method ' . $refMethod->getDeclaringClass()->getName() . '::' . $refMethod->getName() . ' has several @return annotations.'); } + + /** + * Creates an exception for property to have multiple var tags. + * + * @param ReflectionProperty $refProperty + */ + public static function tooManyVarTags(ReflectionProperty $refProperty) + { + throw new self('Property ' . $refProperty->getDeclaringClass()->getName() . '::' . $refProperty->getName() . ' has several @var annotations.'); + } } diff --git a/src/InvalidPrefetchMethodRuntimeException.php b/src/InvalidPrefetchMethodRuntimeException.php index 97ee7916ea..4476503b75 100644 --- a/src/InvalidPrefetchMethodRuntimeException.php +++ b/src/InvalidPrefetchMethodRuntimeException.php @@ -7,18 +7,24 @@ use ReflectionClass; use ReflectionException; use ReflectionMethod; +use ReflectionProperty; class InvalidPrefetchMethodRuntimeException extends GraphQLRuntimeException { /** - * @param ReflectionClass $reflectionClass + * @param ReflectionMethod|ReflectionProperty $reflector + * @param ReflectionClass $reflectionClass */ - public static function methodNotFound(ReflectionMethod $annotationMethod, ReflectionClass $reflectionClass, string $methodName, ReflectionException $previous): self + public static function methodNotFound($reflector, ReflectionClass $reflectionClass, string $methodName, ReflectionException $previous) { - throw new self('The @Field annotation in ' . $annotationMethod->getDeclaringClass()->getName() . '::' . $annotationMethod->getName() . ' specifies a "prefetch method" that could not be found. Unable to find method ' . $reflectionClass->getName() . '::' . $methodName . '.', 0, $previous); + throw new self('The @Field annotation in ' . $reflector->getDeclaringClass()->getName() . '::' . $reflector->getName() . ' specifies a "prefetch method" that could not be found. Unable to find method ' . $reflectionClass->getName() . '::' . $methodName . '.', 0, $previous); } - public static function prefetchDataIgnored(ReflectionMethod $annotationMethod, bool $isSecond): self + /** + * @param ReflectionMethod $annotationMethod + * @param bool $isSecond + */ + public static function prefetchDataIgnored(ReflectionMethod $annotationMethod, bool $isSecond) { throw new self('The @Field annotation in ' . $annotationMethod->getDeclaringClass()->getName() . '::' . $annotationMethod->getName() . ' specifies a "prefetch method" but the data from the prefetch method is not gathered. The "' . $annotationMethod->getName() . '" method should accept a ' . ($isSecond?'second':'first') . ' parameter that will contain data returned by the prefetch method.'); } diff --git a/src/Mappers/DuplicateMappingException.php b/src/Mappers/DuplicateMappingException.php index 40427a523b..8950fa8606 100644 --- a/src/Mappers/DuplicateMappingException.php +++ b/src/Mappers/DuplicateMappingException.php @@ -5,6 +5,7 @@ namespace TheCodingMachine\GraphQLite\Mappers; use ReflectionMethod; +use ReflectionProperty; use RuntimeException; use function sprintf; @@ -30,13 +31,53 @@ public static function createForTypeName(string $type, string $sourceClass1, str throw new self(sprintf("The type '%s' is created by 2 different classes: '%s' and '%s'", $type, $sourceClass1, $sourceClass2)); } - public static function createForQuery(string $sourceClass, string $queryName, ReflectionMethod $method1, ReflectionMethod $method2): self + /** + * @param string $sourceClass + * @param string $queryName + * @param ReflectionMethod|ReflectionProperty $firstReflector + * @param ReflectionMethod|ReflectionProperty $secondReflector + * + * @return static + */ + public static function createForQuery(string $sourceClass, string $queryName, $firstReflector, $secondReflector): self { - throw new self(sprintf("The query/mutation/field '%s' is declared twice in class '%s'. First in '%s::%s()', second in '%s::%s()'", $queryName, $sourceClass, $method1->getDeclaringClass()->getName(), $method1->getName(), $method2->getDeclaringClass()->getName(), $method2->getName())); + $firstName = sprintf('%s::%s', $firstReflector->getDeclaringClass()->getName(), $firstReflector->getName()); + if ($firstReflector instanceof ReflectionMethod) { + $firstName .= '()'; + } + + $secondName = sprintf('%s::%s', $secondReflector->getDeclaringClass()->getName(), $secondReflector->getName()); + if ($secondReflector instanceof ReflectionMethod) { + $secondName .= '()'; + } + + throw new self(sprintf("The query/mutation/field '%s' is declared twice in class '%s'. First in '%s', second in '%s'", $queryName, $sourceClass, $firstName, $secondName)); } public static function createForQueryInTwoControllers(string $sourceClass1, string $sourceClass2, string $queryName): self { throw new self(sprintf("The query/mutation '%s' is declared twice: in class '%s' and in class '%s'", $queryName, $sourceClass1, $sourceClass2)); } + + /** + * @param string $queryName + * @param ReflectionMethod $method + * + * @return self + */ + public static function createForQueryInOneMethod(string $queryName, ReflectionMethod $method): self + { + throw new self(sprintf("The query/mutation/field '%s' is declared twice in '%s::%s()'", $queryName, $method->getDeclaringClass()->getName(), $method->getName())); + } + + /** + * @param string $queryName + * @param ReflectionProperty $property + * + * @return self + */ + public static function createForQueryInOneProperty(string $queryName, ReflectionProperty $property): self + { + throw new self(sprintf("The query/mutation/field '%s' is declared twice in '%s::%s'", $queryName, $property->getDeclaringClass()->getName(), $property->getName())); + } } diff --git a/src/Mappers/Parameters/TypeHandler.php b/src/Mappers/Parameters/TypeHandler.php index f67f491e90..4856319e74 100644 --- a/src/Mappers/Parameters/TypeHandler.php +++ b/src/Mappers/Parameters/TypeHandler.php @@ -9,6 +9,7 @@ use GraphQL\Type\Definition\Type as GraphQLType; use phpDocumentor\Reflection\DocBlock; use phpDocumentor\Reflection\DocBlock\Tags\Return_; +use phpDocumentor\Reflection\DocBlock\Tags\Var_; use phpDocumentor\Reflection\Fqsen; use phpDocumentor\Reflection\Type; use phpDocumentor\Reflection\TypeResolver as PhpDocumentorTypeResolver; @@ -24,6 +25,7 @@ use ReflectionMethod; use ReflectionNamedType; use ReflectionParameter; +use ReflectionProperty; use ReflectionType; use TheCodingMachine\GraphQLite\Annotations\HideParameter; use TheCodingMachine\GraphQLite\Annotations\ParameterAnnotations; @@ -107,6 +109,28 @@ private function getDocBlocReturnType(DocBlock $docBlock, ReflectionMethod $refM return $docBlockReturnType; } + /** + * Gets property type from its dock block. + * + * @param DocBlock $docBlock + * @param ReflectionProperty $refProperty + * + * @return Type|null + */ + private function getDocBlockPropertyType(DocBlock $docBlock, ReflectionProperty $refProperty): ?Type + { + /** @var Var_[] $varTags */ + $varTags = $docBlock->getTagsByName('var'); + + if (!$varTags) { + return null; + } elseif (count($varTags) > 1) { + throw InvalidDocBlockRuntimeException::tooManyVarTags($refProperty); + } + + return reset($varTags)->getType(); + } + public function mapParameter(ReflectionParameter $parameter, DocBlock $docBlock, ?Type $paramTagType, ParameterAnnotations $parameterAnnotations): ParameterInterface { $hideParameter = $parameterAnnotations->getAnnotationByType(HideParameter::class); @@ -164,7 +188,37 @@ public function mapParameter(ReflectionParameter $parameter, DocBlock $docBlock, return new InputTypeParameter($parameter->getName(), $type, $hasDefaultValue, $defaultValue, $this->argumentResolver); } - private function mapType(Type $type, ?Type $docBlockType, bool $isNullable, bool $mapToInputType, ReflectionMethod $refMethod, DocBlock $docBlockObj, ?string $argumentName = null): GraphQLType + /** + * Map class property to a GraphQL type. + * + * @param ReflectionProperty $refProperty + * @param DocBlock $docBlock + * @param bool $toInput + * + * @return GraphQLType + * + * @throws CannotMapTypeException + */ + public function mapProperty(ReflectionProperty $refProperty, DocBlock $docBlock, bool $toInput): GraphQLType + { + $propertyType = null; + + // getType function on property reflection is available only since PHP 7.4 + if (method_exists($refProperty, 'getType') && $propertyType = $refProperty->getType()) { + $phpdocType = $this->reflectionTypeToPhpDocType($propertyType, $refProperty->getDeclaringClass()); + } else { + $phpdocType = new Mixed_(); + } + + $docBlockPropertyType = $this->getDocBlockPropertyType($docBlock, $refProperty); + + $type = $this->mapType($phpdocType, $docBlockPropertyType, $propertyType ? $propertyType->allowsNull() : false, $toInput, null, $docBlock); + assert($type instanceof GraphQLType && $type instanceof OutputType); + + return $type; + } + + private function mapType(Type $type, ?Type $docBlockType, bool $isNullable, bool $mapToInputType, ?ReflectionMethod $refMethod, DocBlock $docBlockObj, ?string $argumentName = null): GraphQLType { $graphQlType = null; if ($isNullable && ! $type instanceof Nullable) { diff --git a/src/Mappers/Root/BaseTypeMapper.php b/src/Mappers/Root/BaseTypeMapper.php index bf9960e91d..088a405448 100644 --- a/src/Mappers/Root/BaseTypeMapper.php +++ b/src/Mappers/Root/BaseTypeMapper.php @@ -23,7 +23,6 @@ use phpDocumentor\Reflection\Types\Object_; use phpDocumentor\Reflection\Types\String_; use Psr\Http\Message\UploadedFileInterface; -use ReflectionMethod; use TheCodingMachine\GraphQLite\Mappers\CannotMapTypeException; use TheCodingMachine\GraphQLite\Mappers\CannotMapTypeExceptionInterface; use TheCodingMachine\GraphQLite\Mappers\RecursiveTypeMapperInterface; @@ -59,7 +58,7 @@ public function __construct(RootTypeMapperInterface $next, RecursiveTypeMapperIn * * @throws CannotMapTypeExceptionInterface */ - public function toGraphQLOutputType(Type $type, ?OutputType $subType, ReflectionMethod $refMethod, DocBlock $docBlockObj): OutputType + public function toGraphQLOutputType(Type $type, ?OutputType $subType, $reflector, DocBlock $docBlockObj): OutputType { $mappedType = $this->mapBaseType($type); if ($mappedType !== null) { @@ -67,9 +66,9 @@ public function toGraphQLOutputType(Type $type, ?OutputType $subType, Reflection } if ($type instanceof Array_) { - $innerType = $this->topRootTypeMapper->toGraphQLOutputType($type->getValueType(), $subType, $refMethod, $docBlockObj); + $innerType = $this->topRootTypeMapper->toGraphQLOutputType($type->getValueType(), $subType, $reflector, $docBlockObj); /*if ($innerType === null) { - return $this->next->toGraphQLOutputType($type, $subType, $refMethod, $docBlockObj); + return $this->next->toGraphQLOutputType($type, $subType, $reflector, $docBlockObj); }*/ return GraphQLType::listOf($innerType); @@ -80,7 +79,7 @@ public function toGraphQLOutputType(Type $type, ?OutputType $subType, Reflection return $this->recursiveTypeMapper->mapClassToInterfaceOrType($className, $subType); } - return $this->next->toGraphQLOutputType($type, $subType, $refMethod, $docBlockObj); + return $this->next->toGraphQLOutputType($type, $subType, $reflector, $docBlockObj); } /** @@ -88,16 +87,16 @@ public function toGraphQLOutputType(Type $type, ?OutputType $subType, Reflection * * @return InputType&GraphQLType */ - public function toGraphQLInputType(Type $type, ?InputType $subType, string $argumentName, ReflectionMethod $refMethod, DocBlock $docBlockObj): InputType + public function toGraphQLInputType(Type $type, ?InputType $subType, string $argumentName, $reflector, DocBlock $docBlockObj): InputType { $mappedType = $this->mapBaseType($type); if ($mappedType !== null) { return $mappedType; } if ($type instanceof Array_) { - $innerType = $this->topRootTypeMapper->toGraphQLInputType($type->getValueType(), $subType, $argumentName, $refMethod, $docBlockObj); + $innerType = $this->topRootTypeMapper->toGraphQLInputType($type->getValueType(), $subType, $argumentName, $reflector, $docBlockObj); /*if ($innerType === null) { - return $this->next->toGraphQLInputType($type, $subType, $argumentName, $refMethod, $docBlockObj); + return $this->next->toGraphQLInputType($type, $subType, $argumentName, $reflector, $docBlockObj); }*/ return GraphQLType::listOf($innerType); @@ -108,7 +107,7 @@ public function toGraphQLInputType(Type $type, ?InputType $subType, string $argu return $this->recursiveTypeMapper->mapClassToInputType($className); } - return $this->next->toGraphQLInputType($type, $subType, $argumentName, $refMethod, $docBlockObj); + return $this->next->toGraphQLInputType($type, $subType, $argumentName, $reflector, $docBlockObj); } /** diff --git a/src/Mappers/Root/CompoundTypeMapper.php b/src/Mappers/Root/CompoundTypeMapper.php index e22ff0f1b7..15cbbc4720 100644 --- a/src/Mappers/Root/CompoundTypeMapper.php +++ b/src/Mappers/Root/CompoundTypeMapper.php @@ -15,7 +15,6 @@ use phpDocumentor\Reflection\Type; use phpDocumentor\Reflection\Types\Compound; use phpDocumentor\Reflection\Types\Iterable_; -use ReflectionMethod; use RuntimeException; use TheCodingMachine\GraphQLite\Mappers\CannotMapTypeException; use TheCodingMachine\GraphQLite\Mappers\RecursiveTypeMapperInterface; @@ -56,10 +55,10 @@ public function __construct(RootTypeMapperInterface $next, RootTypeMapperInterfa * * @return OutputType&GraphQLType */ - public function toGraphQLOutputType(Type $type, ?OutputType $subType, ReflectionMethod $refMethod, DocBlock $docBlockObj): OutputType + public function toGraphQLOutputType(Type $type, ?OutputType $subType, $reflector, DocBlock $docBlockObj): OutputType { if (! $type instanceof Compound) { - return $this->next->toGraphQLOutputType($type, $subType, $refMethod, $docBlockObj); + return $this->next->toGraphQLOutputType($type, $subType, $reflector, $docBlockObj); } $filteredDocBlockTypes = iterator_to_array($type); @@ -73,7 +72,7 @@ public function toGraphQLOutputType(Type $type, ?OutputType $subType, Reflection $mustBeIterable = true; continue; } - $unionTypes[] = $this->topRootTypeMapper->toGraphQLOutputType($singleDocBlockType, null, $refMethod, $docBlockObj); + $unionTypes[] = $this->topRootTypeMapper->toGraphQLOutputType($singleDocBlockType, null, $reflector, $docBlockObj); } if ($mustBeIterable && empty($unionTypes)) { @@ -96,10 +95,10 @@ public function toGraphQLOutputType(Type $type, ?OutputType $subType, Reflection * * @return InputType&GraphQLType */ - public function toGraphQLInputType(Type $type, ?InputType $subType, string $argumentName, ReflectionMethod $refMethod, DocBlock $docBlockObj): InputType + public function toGraphQLInputType(Type $type, ?InputType $subType, string $argumentName, $reflector, DocBlock $docBlockObj): InputType { if (! $type instanceof Compound) { - return $this->next->toGraphQLInputType($type, $subType, $argumentName, $refMethod, $docBlockObj); + return $this->next->toGraphQLInputType($type, $subType, $argumentName, $reflector, $docBlockObj); } // At this point, the |null has been removed and the |iterable has been removed too. diff --git a/src/Mappers/Root/FinalRootTypeMapper.php b/src/Mappers/Root/FinalRootTypeMapper.php index 6a5ca701a2..db2def2bf8 100644 --- a/src/Mappers/Root/FinalRootTypeMapper.php +++ b/src/Mappers/Root/FinalRootTypeMapper.php @@ -10,7 +10,6 @@ use GraphQL\Type\Definition\Type as GraphQLType; use phpDocumentor\Reflection\DocBlock; use phpDocumentor\Reflection\Type; -use ReflectionMethod; use TheCodingMachine\GraphQLite\Mappers\CannotMapTypeException; use TheCodingMachine\GraphQLite\Mappers\RecursiveTypeMapperInterface; @@ -35,7 +34,7 @@ public function __construct(RecursiveTypeMapperInterface $recursiveTypeMapper) * * @return OutputType&GraphQLType */ - public function toGraphQLOutputType(Type $type, ?OutputType $subType, ReflectionMethod $refMethod, DocBlock $docBlockObj): OutputType + public function toGraphQLOutputType(Type $type, ?OutputType $subType, $reflector, DocBlock $docBlockObj): OutputType { throw CannotMapTypeException::createForPhpDocType($type); } @@ -45,7 +44,7 @@ public function toGraphQLOutputType(Type $type, ?OutputType $subType, Reflection * * @return InputType&GraphQLType */ - public function toGraphQLInputType(Type $type, ?InputType $subType, string $argumentName, ReflectionMethod $refMethod, DocBlock $docBlockObj): InputType + public function toGraphQLInputType(Type $type, ?InputType $subType, string $argumentName, $reflector, DocBlock $docBlockObj): InputType { throw CannotMapTypeException::createForPhpDocType($type); } diff --git a/src/Mappers/Root/IteratorTypeMapper.php b/src/Mappers/Root/IteratorTypeMapper.php index 07e7695054..d935611816 100644 --- a/src/Mappers/Root/IteratorTypeMapper.php +++ b/src/Mappers/Root/IteratorTypeMapper.php @@ -19,7 +19,6 @@ use phpDocumentor\Reflection\Types\Compound; use phpDocumentor\Reflection\Types\Object_; use ReflectionClass; -use ReflectionMethod; use TheCodingMachine\GraphQLite\Mappers\CannotMapTypeException; use TheCodingMachine\GraphQLite\Mappers\CannotMapTypeExceptionInterface; use Webmozart\Assert\Assert; @@ -49,11 +48,11 @@ public function __construct(RootTypeMapperInterface $next, RootTypeMapperInterfa * * @return OutputType&GraphQLType */ - public function toGraphQLOutputType(Type $type, ?OutputType $subType, ReflectionMethod $refMethod, DocBlock $docBlockObj): OutputType + public function toGraphQLOutputType(Type $type, ?OutputType $subType, $reflector, DocBlock $docBlockObj): OutputType { if (! $type instanceof Compound) { try { - return $this->next->toGraphQLOutputType($type, $subType, $refMethod, $docBlockObj); + return $this->next->toGraphQLOutputType($type, $subType, $reflector, $docBlockObj); } catch (CannotMapTypeException $e) { if ($type instanceof Object_) { /** @@ -70,12 +69,12 @@ public function toGraphQLOutputType(Type $type, ?OutputType $subType, Reflection } } - $result = $this->toGraphQLType($type, function (Type $type, ?OutputType $subType) use ($refMethod, $docBlockObj) { - return $this->topRootTypeMapper->toGraphQLOutputType($type, $subType, $refMethod, $docBlockObj); + $result = $this->toGraphQLType($type, function (Type $type, ?OutputType $subType) use ($reflector, $docBlockObj) { + return $this->topRootTypeMapper->toGraphQLOutputType($type, $subType, $reflector, $docBlockObj); }, true); if ($result === null) { - return $this->next->toGraphQLOutputType($type, $subType, $refMethod, $docBlockObj); + return $this->next->toGraphQLOutputType($type, $subType, $reflector, $docBlockObj); } Assert::isInstanceOf($result, OutputType::class); @@ -87,22 +86,22 @@ public function toGraphQLOutputType(Type $type, ?OutputType $subType, Reflection * * @return InputType&GraphQLType */ - public function toGraphQLInputType(Type $type, ?InputType $subType, string $argumentName, ReflectionMethod $refMethod, DocBlock $docBlockObj): InputType + public function toGraphQLInputType(Type $type, ?InputType $subType, string $argumentName, $reflector, DocBlock $docBlockObj): InputType { if (! $type instanceof Compound) { //try { - return $this->next->toGraphQLInputType($type, $subType, $argumentName, $refMethod, $docBlockObj); + return $this->next->toGraphQLInputType($type, $subType, $argumentName, $reflector, $docBlockObj); /*} catch (CannotMapTypeException $e) { $this->throwIterableMissingTypeHintException($e, $type); }*/ } - $result = $this->toGraphQLType($type, function (Type $type, ?InputType $subType) use ($refMethod, $docBlockObj, $argumentName) { - return $this->topRootTypeMapper->toGraphQLInputType($type, $subType, $argumentName, $refMethod, $docBlockObj); + $result = $this->toGraphQLType($type, function (Type $type, ?InputType $subType) use ($reflector, $docBlockObj, $argumentName) { + return $this->topRootTypeMapper->toGraphQLInputType($type, $subType, $argumentName, $reflector, $docBlockObj); }, false); if ($result === null) { - return $this->next->toGraphQLInputType($type, $subType, $argumentName, $refMethod, $docBlockObj); + return $this->next->toGraphQLInputType($type, $subType, $argumentName, $reflector, $docBlockObj); } Assert::isInstanceOf($result, InputType::class); diff --git a/src/Mappers/Root/MyCLabsEnumTypeMapper.php b/src/Mappers/Root/MyCLabsEnumTypeMapper.php index f0151003bf..cb26bdd2d9 100644 --- a/src/Mappers/Root/MyCLabsEnumTypeMapper.php +++ b/src/Mappers/Root/MyCLabsEnumTypeMapper.php @@ -14,7 +14,6 @@ use phpDocumentor\Reflection\Type; use phpDocumentor\Reflection\Types\Object_; use ReflectionClass; -use ReflectionMethod; use Symfony\Contracts\Cache\CacheInterface; use TheCodingMachine\GraphQLite\AnnotationReader; use TheCodingMachine\GraphQLite\Types\MyCLabsEnumType; @@ -57,11 +56,11 @@ public function __construct(RootTypeMapperInterface $next, AnnotationReader $ann * * @return OutputType&GraphQLType */ - public function toGraphQLOutputType(Type $type, ?OutputType $subType, ReflectionMethod $refMethod, DocBlock $docBlockObj): OutputType + public function toGraphQLOutputType(Type $type, ?OutputType $subType, $reflector, DocBlock $docBlockObj): OutputType { $result = $this->map($type); if ($result === null) { - return $this->next->toGraphQLOutputType($type, $subType, $refMethod, $docBlockObj); + return $this->next->toGraphQLOutputType($type, $subType, $reflector, $docBlockObj); } return $result; @@ -72,11 +71,11 @@ public function toGraphQLOutputType(Type $type, ?OutputType $subType, Reflection * * @return InputType&GraphQLType */ - public function toGraphQLInputType(Type $type, ?InputType $subType, string $argumentName, ReflectionMethod $refMethod, DocBlock $docBlockObj): InputType + public function toGraphQLInputType(Type $type, ?InputType $subType, string $argumentName, $reflector, DocBlock $docBlockObj): InputType { $result = $this->map($type); if ($result === null) { - return $this->next->toGraphQLInputType($type, $subType, $argumentName, $refMethod, $docBlockObj); + return $this->next->toGraphQLInputType($type, $subType, $argumentName, $reflector, $docBlockObj); } return $result; diff --git a/src/Mappers/Root/NullableTypeMapperAdapter.php b/src/Mappers/Root/NullableTypeMapperAdapter.php index 5aebc5651e..b2077275d0 100644 --- a/src/Mappers/Root/NullableTypeMapperAdapter.php +++ b/src/Mappers/Root/NullableTypeMapperAdapter.php @@ -14,7 +14,6 @@ use phpDocumentor\Reflection\Types\Compound; use phpDocumentor\Reflection\Types\Null_; use phpDocumentor\Reflection\Types\Nullable; -use ReflectionMethod; use TheCodingMachine\GraphQLite\Mappers\CannotMapTypeException; use TheCodingMachine\GraphQLite\Types\ResolvableMutableInputObjectType; use function array_filter; @@ -42,7 +41,7 @@ public function setNext(RootTypeMapperInterface $next): void * * @return OutputType&GraphQLType */ - public function toGraphQLOutputType(Type $type, ?OutputType $subType, ReflectionMethod $refMethod, DocBlock $docBlockObj): OutputType + public function toGraphQLOutputType(Type $type, ?OutputType $subType, $reflector, DocBlock $docBlockObj): OutputType { // Let's check a "null" value in the docblock $isNullable = $this->isNullable($type); @@ -55,7 +54,7 @@ public function toGraphQLOutputType(Type $type, ?OutputType $subType, Reflection $type = $nonNullableType; } - $graphQlType = $this->next->toGraphQLOutputType($type, $subType, $refMethod, $docBlockObj); + $graphQlType = $this->next->toGraphQLOutputType($type, $subType, $reflector, $docBlockObj); if (! $isNullable && $graphQlType instanceof NullableType) { $graphQlType = GraphQLType::nonNull($graphQlType); @@ -69,7 +68,7 @@ public function toGraphQLOutputType(Type $type, ?OutputType $subType, Reflection * * @return InputType&GraphQLType */ - public function toGraphQLInputType(Type $type, ?InputType $subType, string $argumentName, ReflectionMethod $refMethod, DocBlock $docBlockObj): InputType + public function toGraphQLInputType(Type $type, ?InputType $subType, string $argumentName, $reflector, DocBlock $docBlockObj): InputType { // Let's check a "null" value in the docblock $isNullable = $this->isNullable($type); @@ -82,7 +81,7 @@ public function toGraphQLInputType(Type $type, ?InputType $subType, string $argu $type = $nonNullableType; } - $graphQlType = $this->next->toGraphQLInputType($type, $subType, $argumentName, $refMethod, $docBlockObj); + $graphQlType = $this->next->toGraphQLInputType($type, $subType, $argumentName, $reflector, $docBlockObj); // The type is non nullable if the PHP argument is non nullable // There is an exception: if the PHP argument is non nullable but points to a factory that can called without passing any argument, diff --git a/src/Mappers/Root/RootTypeMapperInterface.php b/src/Mappers/Root/RootTypeMapperInterface.php index 687e68700b..6481058a76 100644 --- a/src/Mappers/Root/RootTypeMapperInterface.php +++ b/src/Mappers/Root/RootTypeMapperInterface.php @@ -11,6 +11,7 @@ use phpDocumentor\Reflection\DocBlock; use phpDocumentor\Reflection\Type; use ReflectionMethod; +use ReflectionProperty; /** * Maps a method return type or argument to a GraphQL Type. @@ -26,17 +27,19 @@ interface RootTypeMapperInterface { /** * @param (OutputType&GraphQLType)|null $subType + * @param ReflectionMethod|ReflectionProperty $reflector * * @return OutputType&GraphQLType */ - public function toGraphQLOutputType(Type $type, ?OutputType $subType, ReflectionMethod $refMethod, DocBlock $docBlockObj): OutputType; + public function toGraphQLOutputType(Type $type, ?OutputType $subType, $reflector, DocBlock $docBlockObj): OutputType; /** * @param (InputType&GraphQLType)|null $subType + * @param ReflectionMethod|ReflectionProperty $reflector * * @return InputType&GraphQLType */ - public function toGraphQLInputType(Type $type, ?InputType $subType, string $argumentName, ReflectionMethod $refMethod, DocBlock $docBlockObj): InputType; + public function toGraphQLInputType(Type $type, ?InputType $subType, string $argumentName, $reflector, DocBlock $docBlockObj): InputType; /** * Returns a GraphQL type by name. diff --git a/src/Middlewares/MagicPropertyResolver.php b/src/Middlewares/MagicPropertyResolver.php index 7eeb71c0f9..e9cbaf04b6 100644 --- a/src/Middlewares/MagicPropertyResolver.php +++ b/src/Middlewares/MagicPropertyResolver.php @@ -16,7 +16,7 @@ * * @internal */ -class MagicPropertyResolver implements ResolverInterface +class MagicPropertyResolver implements SourceResolverInterface { /** @var string */ private $propertyName; diff --git a/src/Middlewares/SourcePropertyResolver.php b/src/Middlewares/SourcePropertyResolver.php new file mode 100644 index 0000000000..8a3dc90296 --- /dev/null +++ b/src/Middlewares/SourcePropertyResolver.php @@ -0,0 +1,83 @@ +propertyName = $propertyName; + } + + /** + * {@inheritdoc} + */ + public function setObject(object $object): void + { + $this->object = $object; + } + + /** + * {@inheritdoc} + */ + public function getObject(): object + { + Assert::notNull($this->object); + + return $this->object; + } + + /** + * @param mixed $args + * + * @return mixed + */ + public function __invoke(...$args) + { + if ($this->object === null) { + throw new GraphQLRuntimeException('You must call "setObject" on SourceResolver before invoking the object.'); + } + + return PropertyAccessor::getValue($this->object, $this->propertyName, ...$args); + } + + /** + * {@inheritdoc} + */ + public function toString(): string + { + $class = $this->getObject(); + if (is_object($class)) { + $class = get_class($class); + } + + return $class . '::' . $this->propertyName; + } +} diff --git a/src/Middlewares/SourceResolver.php b/src/Middlewares/SourceResolver.php index 2329876200..f9f2e3e3ea 100644 --- a/src/Middlewares/SourceResolver.php +++ b/src/Middlewares/SourceResolver.php @@ -15,7 +15,7 @@ * * @internal */ -class SourceResolver implements ResolverInterface +class SourceResolver implements SourceResolverInterface { /** @var string */ private $methodName; diff --git a/src/Middlewares/SourceResolverInterface.php b/src/Middlewares/SourceResolverInterface.php new file mode 100644 index 0000000000..66d09f16bc --- /dev/null +++ b/src/Middlewares/SourceResolverInterface.php @@ -0,0 +1,17 @@ +setObject($source); } /*if ($resolve !== null) { @@ -105,7 +104,7 @@ public function __construct(string $name, OutputType $type, array $arguments, Re return new Deferred(function () use ($prefetchBuffer, $source, $args, $context, $info, $prefetchArgs, $prefetchMethodName, $arguments, $resolveFn, $originalResolver) { if (! $prefetchBuffer->hasResult($args)) { - if ($originalResolver instanceof SourceResolver || $originalResolver instanceof MagicPropertyResolver) { + if ($originalResolver instanceof SourceResolverInterface) { $originalResolver->setObject($source); } diff --git a/src/QueryFieldDescriptor.php b/src/QueryFieldDescriptor.php index aaf015c496..1625d3c478 100644 --- a/src/QueryFieldDescriptor.php +++ b/src/QueryFieldDescriptor.php @@ -6,12 +6,13 @@ use GraphQL\Type\Definition\OutputType; use GraphQL\Type\Definition\Type; -use InvalidArgumentException; use ReflectionMethod; +use ReflectionProperty; use TheCodingMachine\GraphQLite\Annotations\MiddlewareAnnotations; use TheCodingMachine\GraphQLite\Middlewares\MagicPropertyResolver; use TheCodingMachine\GraphQLite\Middlewares\ResolverInterface; use TheCodingMachine\GraphQLite\Middlewares\ServiceResolver; +use TheCodingMachine\GraphQLite\Middlewares\SourcePropertyResolver; use TheCodingMachine\GraphQLite\Middlewares\SourceResolver; use TheCodingMachine\GraphQLite\Parameters\ParameterInterface; @@ -37,6 +38,8 @@ class QueryFieldDescriptor /** @var string|null */ private $targetMethodOnSource; /** @var string|null */ + private $targetPropertyOnSource; + /** @var string|null */ private $magicProperty; /** * Whether we should inject the source as the first parameter or not. @@ -52,6 +55,8 @@ class QueryFieldDescriptor private $middlewareAnnotations; /** @var ReflectionMethod */ private $refMethod; + /** @var ReflectionProperty */ + private $refProperty; /** @var ResolverInterface */ private $originalResolver; /** @var callable */ @@ -139,6 +144,7 @@ public function setCallable(callable $callable): void } $this->callable = $callable; $this->targetMethodOnSource = null; + $this->targetPropertyOnSource = null; $this->magicProperty = null; } @@ -149,6 +155,21 @@ public function setTargetMethodOnSource(string $targetMethodOnSource): void } $this->callable = null; $this->targetMethodOnSource = $targetMethodOnSource; + $this->targetPropertyOnSource = null; + $this->magicProperty = null; + } + + /** + * @param string|null $targetPropertyOnSource + */ + public function setTargetPropertyOnSource(?string $targetPropertyOnSource): void + { + if ($this->originalResolver !== null) { + throw new GraphQLRuntimeException('You cannot modify the target method via setTargetMethodOnSource because it was already used. You can still wrap the callable using getResolver/setResolver'); + } + $this->callable = null; + $this->targetMethodOnSource = null; + $this->targetPropertyOnSource = $targetPropertyOnSource; $this->magicProperty = null; } @@ -159,6 +180,7 @@ public function setMagicProperty(string $magicProperty): void } $this->callable = null; $this->targetMethodOnSource = null; + $this->targetPropertyOnSource = null; $this->magicProperty = $magicProperty; } @@ -212,6 +234,16 @@ public function setRefMethod(ReflectionMethod $refMethod): void $this->refMethod = $refMethod; } + public function getRefProperty(): ReflectionProperty + { + return $this->refProperty; + } + + public function setRefProperty(ReflectionProperty $refProperty): void + { + $this->refProperty = $refProperty; + } + /** * Returns the original callable that will be used to resolve the field. */ @@ -225,6 +257,8 @@ public function getOriginalResolver(): ResolverInterface $this->originalResolver = new ServiceResolver($this->callable); } elseif ($this->targetMethodOnSource !== null) { $this->originalResolver = new SourceResolver($this->targetMethodOnSource); + } elseif ($this->targetPropertyOnSource !== null) { + $this->originalResolver = new SourcePropertyResolver($this->targetPropertyOnSource); } elseif ($this->magicProperty !== null) { $this->originalResolver = new MagicPropertyResolver($this->magicProperty); } else { diff --git a/src/Reflection/CachedDocBlockFactory.php b/src/Reflection/CachedDocBlockFactory.php index 0a8b20c429..e1d0686a6b 100644 --- a/src/Reflection/CachedDocBlockFactory.php +++ b/src/Reflection/CachedDocBlockFactory.php @@ -9,8 +9,10 @@ use phpDocumentor\Reflection\Types\Context; use phpDocumentor\Reflection\Types\ContextFactory; use Psr\SimpleCache\CacheInterface; +use Psr\SimpleCache\InvalidArgumentException; use ReflectionClass; use ReflectionMethod; +use ReflectionProperty; use Webmozart\Assert\Assert; use function filemtime; use function md5; @@ -43,15 +45,21 @@ public function __construct(CacheInterface $cache, ?DocBlockFactory $docBlockFac /** * Fetches a DocBlock object from a ReflectionMethod + * + * @param ReflectionMethod|ReflectionProperty $reflector + * + * @return DocBlock + * + * @throws InvalidArgumentException */ - public function getDocBlock(ReflectionMethod $refMethod): DocBlock + public function getDocBlock($reflector): DocBlock { - $key = 'docblock_' . md5($refMethod->getDeclaringClass()->getName() . '::' . $refMethod->getName()); + $key = 'docblock_' . md5($reflector->getDeclaringClass()->getName() . '::' . $reflector->getName() . '::' . get_class($reflector)); if (isset($this->docBlockArrayCache[$key])) { return $this->docBlockArrayCache[$key]; } - $fileName = $refMethod->getFileName(); + $fileName = $reflector->getDeclaringClass()->getFileName(); Assert::string($fileName); $cacheItem = $this->cache->get($key); @@ -68,7 +76,7 @@ public function getDocBlock(ReflectionMethod $refMethod): DocBlock } } - $docBlock = $this->doGetDocBlock($refMethod); + $docBlock = $this->doGetDocBlock($reflector); $this->cache->set($key, [ 'time' => filemtime($fileName), @@ -79,15 +87,20 @@ public function getDocBlock(ReflectionMethod $refMethod): DocBlock return $docBlock; } - private function doGetDocBlock(ReflectionMethod $refMethod): DocBlock + /** + * @param ReflectionMethod|ReflectionProperty $reflector + * + * @return DocBlock + */ + private function doGetDocBlock($reflector): DocBlock { - $docComment = $refMethod->getDocComment() ?: '/** */'; + $docComment = $reflector->getDocComment() ?: '/** */'; - $refClass = $refMethod->getDeclaringClass(); + $refClass = $reflector->getDeclaringClass(); $refClassName = $refClass->getName(); if (! isset($this->contextArrayCache[$refClassName])) { - $this->contextArrayCache[$refClassName] = $this->contextFactory->createFromReflector($refMethod); + $this->contextArrayCache[$refClassName] = $this->contextFactory->createFromReflector($reflector); } return $this->docBlockFactory->create($docComment, $this->contextArrayCache[$refClassName]); diff --git a/src/Utils/PropertyAccessor.php b/src/Utils/PropertyAccessor.php new file mode 100644 index 0000000000..6380dd50e9 --- /dev/null +++ b/src/Utils/PropertyAccessor.php @@ -0,0 +1,48 @@ +$method(...$args); + } + + return $object->$propertyName; + } +} diff --git a/tests/Mappers/Root/VoidRootTypeMapper.php b/tests/Mappers/Root/VoidRootTypeMapper.php index 366426d3d6..5c06ee5a9e 100644 --- a/tests/Mappers/Root/VoidRootTypeMapper.php +++ b/tests/Mappers/Root/VoidRootTypeMapper.php @@ -10,7 +10,6 @@ use GraphQL\Type\Definition\Type as GraphQLType; use phpDocumentor\Reflection\DocBlock; use phpDocumentor\Reflection\Type; -use ReflectionMethod; class VoidRootTypeMapper implements RootTypeMapperInterface { @@ -29,9 +28,9 @@ public function __construct(RootTypeMapperInterface $next) * * @return OutputType&GraphQLType */ - public function toGraphQLOutputType(Type $type, ?OutputType $subType, ReflectionMethod $refMethod, DocBlock $docBlockObj): OutputType + public function toGraphQLOutputType(Type $type, ?OutputType $subType, $reflector, DocBlock $docBlockObj): OutputType { - return $this->next->toGraphQLOutputType($type, $subType, $refMethod, $docBlockObj); + return $this->next->toGraphQLOutputType($type, $subType, $reflector, $docBlockObj); } /** @@ -39,9 +38,9 @@ public function toGraphQLOutputType(Type $type, ?OutputType $subType, Reflection * * @return InputType&GraphQLType */ - public function toGraphQLInputType(Type $type, ?InputType $subType, string $argumentName, ReflectionMethod $refMethod, DocBlock $docBlockObj): InputType + public function toGraphQLInputType(Type $type, ?InputType $subType, string $argumentName, $reflector, DocBlock $docBlockObj): InputType { - return $this->next->toGraphQLInputType($type, $subType, $argumentName, $refMethod, $docBlockObj); + return $this->next->toGraphQLInputType($type, $subType, $argumentName, $reflector, $docBlockObj); } /** From ac99000c58017b1740260f709a58047de4eaac11 Mon Sep 17 00:00:00 2001 From: Andrew Maslov Date: Sat, 6 Jun 2020 22:14:15 +0300 Subject: [PATCH 02/41] Implemented input type mapping from a class --- src/AnnotationReader.php | 23 ++++ src/Annotations/Field.php | 37 +++++++ src/Annotations/Input.php | 119 +++++++++++++++++++++ src/FailedResolvingInputType.php | 28 +++++ src/FieldsBuilder.php | 46 +++++++- src/InputTypeGenerator.php | 18 ++++ src/Mappers/AbstractTypeMapper.php | 38 ++++++- src/Mappers/DuplicateMappingException.php | 10 ++ src/Mappers/GlobAnnotationsCache.php | 27 +++++ src/Mappers/GlobTypeMapperCache.php | 36 +++++++ src/Mappers/Parameters/TypeHandler.php | 53 +++++++++- src/NamingStrategy.php | 8 +- src/NamingStrategyInterface.php | 9 +- src/Parameters/InputTypeParameter.php | 23 ++++ src/Parameters/InputTypeProperty.php | 35 +++++++ src/Types/InputType.php | 121 ++++++++++++++++++++++ src/Utils/PropertyAccessor.php | 38 ++++++- 17 files changed, 655 insertions(+), 14 deletions(-) create mode 100644 src/Annotations/Input.php create mode 100644 src/FailedResolvingInputType.php create mode 100644 src/Parameters/InputTypeProperty.php create mode 100644 src/Types/InputType.php diff --git a/src/AnnotationReader.php b/src/AnnotationReader.php index 58db700531..afdc7544d7 100644 --- a/src/AnnotationReader.php +++ b/src/AnnotationReader.php @@ -20,6 +20,7 @@ use TheCodingMachine\GraphQLite\Annotations\Exceptions\InvalidParameterException; use TheCodingMachine\GraphQLite\Annotations\ExtendType; use TheCodingMachine\GraphQLite\Annotations\Factory; +use TheCodingMachine\GraphQLite\Annotations\Input; use TheCodingMachine\GraphQLite\Annotations\MiddlewareAnnotationInterface; use TheCodingMachine\GraphQLite\Annotations\MiddlewareAnnotations; use TheCodingMachine\GraphQLite\Annotations\ParameterAnnotationInterface; @@ -113,6 +114,28 @@ public function getTypeAnnotation(ReflectionClass $refClass): ?Type return $type; } + /** + * @param ReflectionClass $refClass + * + * @return array|Input[] + * + * @throws AnnotationException + */ + public function getInputAnnotations(ReflectionClass $refClass): array + { + try { + /** @var Input[] $inputs */ + $inputs = $this->getClassAnnotations($refClass, Input::class); + foreach ($inputs as $input) { + $input->setClass($refClass->getName()); + } + } catch (ClassNotFoundException $e) { + throw ClassNotFoundException::wrapException($e, $refClass->getName()); + } + + return $inputs; + } + /** * @param ReflectionClass $refClass * diff --git a/src/Annotations/Field.php b/src/Annotations/Field.php index 6aefb6ee4e..fe629fd8e9 100644 --- a/src/Annotations/Field.php +++ b/src/Annotations/Field.php @@ -4,6 +4,8 @@ namespace TheCodingMachine\GraphQLite\Annotations; +use Doctrine\Common\Annotations\Annotation\Attribute; + /** * @Annotation * @Target({"PROPERTY", "METHOD"}) @@ -11,6 +13,8 @@ * @Attribute("name", type = "string"), * @Attribute("outputType", type = "string"), * @Attribute("prefetchMethod", type = "string"), + * @Attribute("for", type = "string[]"), + * @Attribute("description", type = "string"), * }) */ class Field extends AbstractRequest @@ -18,6 +22,18 @@ class Field extends AbstractRequest /** @var string|null */ private $prefetchMethod; + /** + * Input/Output type names for which this fields should be applied to. + * + * @var string[]|null + */ + private $for = null; + + /** + * @var string|null + */ + private $description; + /** * @param mixed[] $attributes */ @@ -25,6 +41,11 @@ public function __construct(array $attributes = []) { parent::__construct($attributes); $this->prefetchMethod = $attributes['prefetchMethod'] ?? null; + $this->description = $attributes['description'] ?? null; + + if (!empty($attributes['for'])) { + $this->for = (array) $attributes['for']; + } } /** @@ -34,4 +55,20 @@ public function getPrefetchMethod(): ?string { return $this->prefetchMethod; } + + /** + * @return string[]|null + */ + public function getFor(): ?array + { + return $this->for; + } + + /** + * @return string|null + */ + public function getDescription(): ?string + { + return $this->description; + } } diff --git a/src/Annotations/Input.php b/src/Annotations/Input.php new file mode 100644 index 0000000000..a003012c58 --- /dev/null +++ b/src/Annotations/Input.php @@ -0,0 +1,119 @@ +class = $attributes['class'] ?? null; + $this->name = $attributes['name'] ?? null; + $this->default = $attributes['default'] ?? !isset($attributes['name']); + $this->description = $attributes['description'] ?? null; + $this->update = $attributes['update'] ?? false; + } + + /** + * Returns the fully qualified class name of the targeted class. + * + * @return string + */ + public function getClass(): string + { + if ($this->class === null) { + throw new RuntimeException('Empty class for @Input annotation. You MUST create the Input annotation object using the GraphQLite AnnotationReader'); + } + + return $this->class; + } + + /** + * @param string $class + */ + public function setClass(string $class): void + { + $this->class = $class; + } + + /** + * Returns the GraphQL input name for this type. + */ + public function getName(): ?string + { + return $this->name; + } + + /** + * Returns true if this type should map the targeted class by default. + */ + public function isDefault(): bool + { + return $this->default; + } + + /** + * Returns description about this input type. + * + * @return string|null + */ + public function getDescription(): ?string + { + return $this->description; + } + + /** + * Returns true if this type should behave as update resource. + * Such input type has all fields optional and without default value in the documentation. + * + * @return bool + */ + public function isUpdate(): bool + { + return $this->update; + } +} diff --git a/src/FailedResolvingInputType.php b/src/FailedResolvingInputType.php new file mode 100644 index 0000000000..553c838cb1 --- /dev/null +++ b/src/FailedResolvingInputType.php @@ -0,0 +1,28 @@ +getProperties(); + + $fields = []; + $defaultProperties = $refClass->getDefaultProperties(); + foreach ($reflectors as $reflector) { + + /** @var Annotations\Field[] $annotations */ + $annotations = $this->annotationReader->getPropertyAnnotations($reflector, Annotations\Field::class); + $docBlock = $this->cachedDocBlockFactory->getDocBlock($reflector); + + foreach ($annotations as $annotation) { + $for = $annotation->getFor(); + if ($for && !in_array($inputName, $for)) { + continue; + } + + $name = $annotation->getName() ?: $reflector->getName(); + + $field = $this->typeMapper->mapInputProperty($reflector, $docBlock, $name, $defaultProperties[$reflector->getName()] ?? null, $isUpdate ? true : null); + if ($description = $annotation->getDescription()) { + $field->setDescription($description); + } + + $fields[$name] = $field; + } + } + + return $fields; + } + /** * Track Field annotation in a self targeted type * @@ -407,7 +451,7 @@ private function getFieldsByPropertyAnnotations($controller, ReflectionClass $re if ($outputType) { $type = $this->typeResolver->mapNameToOutputType($outputType); } else { - $type = $this->typeMapper->mapProperty($refProperty, $docBlock, false); + $type = $this->typeMapper->mapPropertyType($refProperty, $docBlock, false); } $fieldDescriptor->setType($type); $fieldDescriptor->setInjectSource(false); diff --git a/src/InputTypeGenerator.php b/src/InputTypeGenerator.php index 7bcc538803..31df86b604 100644 --- a/src/InputTypeGenerator.php +++ b/src/InputTypeGenerator.php @@ -8,6 +8,7 @@ use Psr\Container\ContainerInterface; use ReflectionFunctionAbstract; use ReflectionMethod; +use TheCodingMachine\GraphQLite\Types\InputType; use TheCodingMachine\GraphQLite\Types\ResolvableMutableInputInterface; use TheCodingMachine\GraphQLite\Types\ResolvableMutableInputObjectType; use Webmozart\Assert\Assert; @@ -53,6 +54,23 @@ public function mapFactoryMethod(string $factory, string $methodName, ContainerI return $this->cache[$inputName]; } + /** + * @param string $className + * @param string $inputName + * @param string|null $description + * @param bool $isUpdate + * + * @return InputType + */ + public function mapInput(string $className, string $inputName, ?string $description, bool $isUpdate): InputType + { + if (!isset($this->cache[$inputName])) { + $this->cache[$inputName] = new InputType($className, $inputName, $description, $isUpdate, $this->fieldsBuilder); + } + + return $this->cache[$inputName]; + } + public static function canBeInstantiatedWithoutParameter(ReflectionFunctionAbstract $refMethod, bool $skipFirstArgument): bool { $nbParams = $refMethod->getNumberOfRequiredParameters(); diff --git a/src/Mappers/AbstractTypeMapper.php b/src/Mappers/AbstractTypeMapper.php index 91caf8193f..7f96ffb30c 100644 --- a/src/Mappers/AbstractTypeMapper.php +++ b/src/Mappers/AbstractTypeMapper.php @@ -133,6 +133,7 @@ private function buildMap(): GlobTypeMapperCache { $globTypeMapperCache = new GlobTypeMapperCache(); + /** @var ReflectionClass[] $classes */ $classes = $this->getClassList(); foreach ($classes as $className => $refClass) { $annotationsCache = $this->mapClassToAnnotationsCache->get($refClass, function () use ($refClass, $className) { @@ -147,6 +148,13 @@ private function buildMap(): GlobTypeMapperCache $containsAnnotations = true; } + $inputs = $this->annotationReader->getInputAnnotations($refClass); + foreach ($inputs as $input) { + $inputName = $this->namingStrategy->getInputTypeName($className, $input); + $annotationsCache->registerInput($inputName, $className, $input); + $containsAnnotations = true; + } + $isAbstract = $refClass->isAbstract(); foreach ($refClass->getMethods(ReflectionMethod::IS_PUBLIC) as $method) { @@ -283,7 +291,11 @@ public function getSupportedClasses(): array */ public function canMapClassToInputType(string $className): bool { - return $this->getMaps()->getFactoryByObjectClass($className) !== null; + if ($this->getMaps()->getFactoryByObjectClass($className) !== null) { + return true; + } + + return $this->getMaps()->getInputByObjectClass($className) !== null; } /** @@ -299,11 +311,17 @@ public function mapClassToInputType(string $className): ResolvableMutableInputIn { $factory = $this->getMaps()->getFactoryByObjectClass($className); - if ($factory === null) { - throw CannotMapTypeException::createForInputType($className); + if ($factory !== null) { + return $this->inputTypeGenerator->mapFactoryMethod($factory[0], $factory[1], $this->container); + } + + $input = $this->getMaps()->getInputByObjectClass($className); + if ($input !== null) { + [ $typeName, $description, $isUpdate ] = $input; + return $this->inputTypeGenerator->mapInput($className, $typeName, $description, $isUpdate); } - return $this->inputTypeGenerator->mapFactoryMethod($factory[0], $factory[1], $this->container); + throw CannotMapTypeException::createForInputType($className); } /** @@ -329,6 +347,12 @@ public function mapNameToType(string $typeName): Type return $this->inputTypeGenerator->mapFactoryMethod($factory[0], $factory[1], $this->container); } + $input = $this->getMaps()->getInputByGraphQLInputTypeName($typeName); + if ($input !== null) { + [ $className, $description, $isUpdate ] = $input; + return $this->inputTypeGenerator->mapInput($className, $typeName, $description, $isUpdate); + } + throw CannotMapTypeException::createForName($typeName); } @@ -347,7 +371,11 @@ public function canMapNameToType(string $typeName): bool $factory = $this->getMaps()->getFactoryByGraphQLInputTypeName($typeName); - return $factory !== null; + if ($factory !== null) { + return true; + } + + return $this->getMaps()->getInputByGraphQLInputTypeName($typeName) !== null; } /** diff --git a/src/Mappers/DuplicateMappingException.php b/src/Mappers/DuplicateMappingException.php index 8950fa8606..a97d364fd6 100644 --- a/src/Mappers/DuplicateMappingException.php +++ b/src/Mappers/DuplicateMappingException.php @@ -80,4 +80,14 @@ public static function createForQueryInOneProperty(string $queryName, Reflection { throw new self(sprintf("The query/mutation/field '%s' is declared twice in '%s::%s'", $queryName, $property->getDeclaringClass()->getName(), $property->getName())); } + + /** + * @param string $sourceClass + * + * @return static + */ + public static function createForInput(string $sourceClass): self + { + throw new self(sprintf("The class '%s' should be mapped to only one GraphQL Input type. Two default inputs are declared as default via @Input annotation.", $sourceClass)); + } } diff --git a/src/Mappers/GlobAnnotationsCache.php b/src/Mappers/GlobAnnotationsCache.php index 0dd6e75e88..8009e828b4 100644 --- a/src/Mappers/GlobAnnotationsCache.php +++ b/src/Mappers/GlobAnnotationsCache.php @@ -4,6 +4,8 @@ namespace TheCodingMachine\GraphQLite\Mappers; +use TheCodingMachine\GraphQLite\Annotations\Input; + /** * An object containing a description of ALL annotations relevant to GlobTypeMapper for a given class. * @@ -26,6 +28,9 @@ final class GlobAnnotationsCache /** @var array}> An array mapping a decorator method name to an input name / declaring class */ private $decorators = []; + /** @var array, 1:bool, 2:string, 3:bool}> An array mapping an input type name to an input name / declaring class */ + private $inputs = []; + /** * @param class-string $className */ @@ -86,4 +91,26 @@ public function getDecorators(): array { return $this->decorators; } + + /** + * Register a new input. + * + * @param string $name + * @param string $className + * @param Input $input + */ + public function registerInput(string $name, string $className, Input $input): void + { + $this->inputs[$name] = [$className, $input->isDefault(), $input->getDescription(), $input->isUpdate()]; + } + + /** + * Returns registered inputs. + * + * @return array, 1:bool, 2:string, 3:bool}> + */ + public function getInputs(): array + { + return $this->inputs; + } } diff --git a/src/Mappers/GlobTypeMapperCache.php b/src/Mappers/GlobTypeMapperCache.php index a19e088f44..7b1278d4aa 100644 --- a/src/Mappers/GlobTypeMapperCache.php +++ b/src/Mappers/GlobTypeMapperCache.php @@ -22,6 +22,10 @@ class GlobTypeMapperCache private $mapInputNameToFactory = []; /** @var array> Maps a GraphQL type name to one or many decorators (with the @Decorator annotation) */ private $mapInputNameToDecorator = []; + /** @var array> Maps a domain class to the input */ + private $mapClassToInput = []; + /** @var array> Maps a GraphQL type name to the input */ + private $mapNameToInput = []; /** * Merges annotations of a given class in the global cache. @@ -64,6 +68,18 @@ public function registerAnnotations(ReflectionClass $refClass, GlobAnnotationsCa $this->mapInputNameToFactory[$inputName] = $refArray; } + foreach ($globAnnotationsCache->getInputs() as $inputName => [$inputClassName, $isDefault, $description, $isUpdate]) { + if ($isDefault) { + if (isset($this->mapClassToInput[$inputClassName])) { + throw DuplicateMappingException::createForInput($refClass->getName()); + } + + $this->mapClassToInput[$inputClassName] = [$inputName, $description, $isUpdate]; + } + + $this->mapNameToInput[$inputName] = [$inputClassName, $description, $isUpdate]; + } + foreach ($globAnnotationsCache->getDecorators() as $methodName => [$inputName, $declaringClass]) { $this->mapInputNameToDecorator[$inputName][] = [$declaringClass, $methodName]; } @@ -118,4 +134,24 @@ public function getFactoryByObjectClass(string $className): ?array { return $this->mapClassToFactory[$className] ?? null; } + + /** + * @param string $graphqlTypeName + * + * @return array|null + */ + public function getInputByGraphQLInputTypeName(string $graphqlTypeName): ?array + { + return $this->mapNameToInput[$graphqlTypeName] ?? null; + } + + /** + * @param string $className + * + * @return array|null + */ + public function getInputByObjectClass(string $className): ?array + { + return $this->mapClassToInput[$className] ?? null; + } } diff --git a/src/Mappers/Parameters/TypeHandler.php b/src/Mappers/Parameters/TypeHandler.php index 4856319e74..15096ec15c 100644 --- a/src/Mappers/Parameters/TypeHandler.php +++ b/src/Mappers/Parameters/TypeHandler.php @@ -36,6 +36,7 @@ use TheCodingMachine\GraphQLite\Mappers\Root\RootTypeMapperInterface; use TheCodingMachine\GraphQLite\Parameters\DefaultValueParameter; use TheCodingMachine\GraphQLite\Parameters\InputTypeParameter; +use TheCodingMachine\GraphQLite\Parameters\InputTypeProperty; use TheCodingMachine\GraphQLite\Parameters\ParameterInterface; use TheCodingMachine\GraphQLite\Types\ArgumentResolver; use TheCodingMachine\GraphQLite\Types\TypeResolver; @@ -194,12 +195,13 @@ public function mapParameter(ReflectionParameter $parameter, DocBlock $docBlock, * @param ReflectionProperty $refProperty * @param DocBlock $docBlock * @param bool $toInput + * @param string|null $argumentName * * @return GraphQLType * * @throws CannotMapTypeException */ - public function mapProperty(ReflectionProperty $refProperty, DocBlock $docBlock, bool $toInput): GraphQLType + public function mapPropertyType(ReflectionProperty $refProperty, DocBlock $docBlock, bool $toInput, ?string $argumentName = null, ?bool $isNullable = null): GraphQLType { $propertyType = null; @@ -212,12 +214,59 @@ public function mapProperty(ReflectionProperty $refProperty, DocBlock $docBlock, $docBlockPropertyType = $this->getDocBlockPropertyType($docBlock, $refProperty); - $type = $this->mapType($phpdocType, $docBlockPropertyType, $propertyType ? $propertyType->allowsNull() : false, $toInput, null, $docBlock); + if (null === $isNullable) { + $isNullable = $propertyType ? $propertyType->allowsNull() : false; + } + + $type = $this->mapType($phpdocType, $docBlockPropertyType, $isNullable, $toInput, null, $docBlock, $argumentName); assert($type instanceof GraphQLType && $type instanceof OutputType); return $type; } + /** + * Maps class property into input property. + * + * @param ReflectionProperty $refProperty + * @param DocBlock $docBlock + * @param string|null $argumentName + * @param mixed $defaultValue + * @param bool|null $isNullable + * + * @return InputTypeProperty + */ + public function mapInputProperty(ReflectionProperty $refProperty, DocBlock $docBlock, ?string $argumentName = null, $defaultValue = null, ?bool $isNullable = null): InputTypeProperty + { + $docBlockComment = $docBlock->getSummary() . PHP_EOL . $docBlock->getDescription()->render(); + + /** @var Var_[] $varTags */ + $varTags = $docBlock->getTagsByName('var'); + if ($varTag = reset($varTags)) { + $docBlockComment .= PHP_EOL . $varTag->getDescription(); + + if (null === $isNullable && $varType = $varTag->getType()) { + $isNullable = in_array('null', explode('|', (string) $varType)); + } + } + + if (null === $isNullable) { + $isNullable = false; + // getType function on property reflection is available only since PHP 7.4 + if (method_exists($refProperty, 'getType') && $refType = $refProperty->getType()) { + $isNullable = $refType->allowsNull(); + } + } + + /** @var InputType $type */ + $type = $this->mapPropertyType($refProperty, $docBlock, true, $argumentName, $isNullable); + $hasDefault = $defaultValue !== null || $isNullable; + + $inputProperty = new InputTypeProperty($refProperty->getName(), $argumentName, $type, $hasDefault, $defaultValue, $this->argumentResolver); + $inputProperty->setDescription(trim($docBlockComment)); + + return $inputProperty; + } + private function mapType(Type $type, ?Type $docBlockType, bool $isNullable, bool $mapToInputType, ?ReflectionMethod $refMethod, DocBlock $docBlockObj, ?string $argumentName = null): GraphQLType { $graphQlType = null; diff --git a/src/NamingStrategy.php b/src/NamingStrategy.php index 1fa2b5a31f..8a35849cb2 100644 --- a/src/NamingStrategy.php +++ b/src/NamingStrategy.php @@ -4,7 +4,6 @@ namespace TheCodingMachine\GraphQLite; -use TheCodingMachine\GraphQLite\Annotations\Factory; use TheCodingMachine\GraphQLite\Annotations\Type; use function lcfirst; use function str_replace; @@ -61,9 +60,12 @@ public function getOutputTypeName(string $typeClassName, Type $type): string return $typeClassName; } - public function getInputTypeName(string $className, Factory $factory): string + /** + * {@inheritdoc} + */ + public function getInputTypeName(string $className, $input): string { - $inputTypeName = $factory->getName(); + $inputTypeName = $input->getName(); if ($inputTypeName !== null) { return $inputTypeName; } diff --git a/src/NamingStrategyInterface.php b/src/NamingStrategyInterface.php index 21e12e8785..0e69634ff7 100644 --- a/src/NamingStrategyInterface.php +++ b/src/NamingStrategyInterface.php @@ -5,6 +5,7 @@ namespace TheCodingMachine\GraphQLite; use TheCodingMachine\GraphQLite\Annotations\Factory; +use TheCodingMachine\GraphQLite\Annotations\Input; use TheCodingMachine\GraphQLite\Annotations\Type; /** @@ -29,7 +30,13 @@ public function getConcreteNameFromInterfaceName(string $name): string; */ public function getOutputTypeName(string $typeClassName, Type $type): string; - public function getInputTypeName(string $className, Factory $factory): string; + /** + * @param string $className + * @param Input|Factory $input + * + * @return string + */ + public function getInputTypeName(string $className, $input): string; /** * Returns the name of a GraphQL field from the name of the annotated method. diff --git a/src/Parameters/InputTypeParameter.php b/src/Parameters/InputTypeParameter.php index bf47709ddb..18554cda6c 100644 --- a/src/Parameters/InputTypeParameter.php +++ b/src/Parameters/InputTypeParameter.php @@ -22,6 +22,8 @@ class InputTypeParameter implements InputTypeParameterInterface private $defaultValue; /** @var ArgumentResolver */ private $argumentResolver; + /** @var string */ + private $description; /** * @param InputType&Type $type @@ -61,6 +63,13 @@ public function resolve(?object $source, array $args, $context, ResolveInfo $inf throw MissingArgumentException::create($this->name); } + /** + * @return string + */ + public function getName(): string { + return $this->name; + } + public function getType(): InputType { return $this->type; @@ -78,4 +87,18 @@ public function getDefaultValue() { return $this->defaultValue; } + + /** + * @return string + */ + public function getDescription(): string { + return $this->description; + } + + /** + * @param string $description + */ + public function setDescription(string $description): void { + $this->description = $description; + } } diff --git a/src/Parameters/InputTypeProperty.php b/src/Parameters/InputTypeProperty.php new file mode 100644 index 0000000000..23ed721fd3 --- /dev/null +++ b/src/Parameters/InputTypeProperty.php @@ -0,0 +1,35 @@ +propertyName = $propertyName; + } + + /** + * @return string + */ + public function getPropertyName(): string { + return $this->propertyName; + } +} diff --git a/src/Types/InputType.php b/src/Types/InputType.php new file mode 100644 index 0000000000..91cb937019 --- /dev/null +++ b/src/Types/InputType.php @@ -0,0 +1,121 @@ +fields = $fieldsBuilder->getInputFields($className, $inputName, $isUpdate); + + $fields = function() use ($isUpdate) { + $fields = []; + foreach ($this->fields as $name => $field) { + $type = $field->getType(); + + $fields[$name] = [ + 'type' => $type, + 'description' => $field->getDescription(), + ]; + + if ($field->hasDefaultValue() && !$isUpdate) { + $fields[$name]['defaultValue'] = $field->getDefaultValue(); + } + } + + return $fields; + }; + + $config = [ + 'name' => $inputName, + 'description' => $description, + 'fields' => $fields, + ]; + + parent::__construct($config); + $this->className = $className; + } + + /** + * {@inheritdoc} + */ + public function resolve(?object $source, array $args, $context, ResolveInfo $resolveInfo): object + { + $mappedValues = []; + foreach ($this->fields as $field) { + $name = $field->getName(); + if (array_key_exists($name, $args)) { + $mappedValues[$field->getPropertyName()] = $field->resolve($source, $args, $context, $resolveInfo); + } + } + + $instance = $this->createInstance($mappedValues); + foreach ($mappedValues as $property => $value) { + PropertyAccessor::setValue($instance, $property, $value); + } + + return $instance; + } + + /** + * {@inheritdoc} + */ + public function decorate(callable $decorator): void + { + throw FailedResolvingInputType::createForDecorator(); + } + + /** + * Creates an instance of the input class. + * + * @param array $values + * + * @return object + */ + private function createInstance(array $values) + { + $refClass = new ReflectionClass($this->className); + $constructor = $refClass->getConstructor(); + $constructorParameters = $constructor ? $constructor->getParameters() : []; + + $parameters = []; + foreach ($constructorParameters as $parameter) { + $name = $parameter->getName(); + if (!$parameter->isDefaultValueAvailable() && empty($values[$name])) { + throw FailedResolvingInputType::createForMissingConstructorParameter($refClass->getName(), $name); + } + + $parameters[] = $values[$name] ?? $parameter->getDefaultValue(); + } + + return $refClass->newInstanceArgs($parameters); + } +} diff --git a/src/Utils/PropertyAccessor.php b/src/Utils/PropertyAccessor.php index 6380dd50e9..5002e5c74c 100644 --- a/src/Utils/PropertyAccessor.php +++ b/src/Utils/PropertyAccessor.php @@ -16,7 +16,7 @@ class PropertyAccessor * * @return string|null */ - static public function findGetter(string $class, string $propertyName): ?string + public static function findGetter(string $class, string $propertyName): ?string { $name = ucfirst($propertyName); @@ -30,6 +30,26 @@ static public function findGetter(string $class, string $propertyName): ?string return null; } + /** + * Finds a setter for a property. + * + * @param string $class + * @param string $propertyName + * + * @return string|null + */ + public static function findSetter(string $class, string $propertyName): ?string + { + $name = ucfirst($propertyName); + + $methodName = "set$name"; + if (method_exists($class, $methodName)) { + return $methodName; + } + + return null; + } + /** * @param object $object * @param string $propertyName @@ -37,7 +57,7 @@ static public function findGetter(string $class, string $propertyName): ?string * * @return mixed */ - static public function getValue(object $object, string $propertyName, ...$args) + public static function getValue(object $object, string $propertyName, ...$args) { if ($method = self::findGetter(get_class($object), $propertyName)) { return $object->$method(...$args); @@ -45,4 +65,18 @@ static public function getValue(object $object, string $propertyName, ...$args) return $object->$propertyName; } + + /** + * @param object $instance + * @param string $propertyName + * @param mixed $value + */ + public static function setValue(object $instance, string $propertyName, $value): void + { + if ($setter = self::findSetter(get_class($instance), $propertyName)) { + $instance->$setter($value); + } else { + $instance->$propertyName = $value; + } + } } From 0bd8d3942266a5b7ccb90934a48984f4c01fa773 Mon Sep 17 00:00:00 2001 From: Andrew Maslov Date: Sun, 7 Jun 2020 19:25:00 +0300 Subject: [PATCH 03/41] Implemented @Field "for" for @Type --- src/FieldsBuilder.php | 43 +++++++++++++++++------- src/TypeGenerator.php | 4 +-- src/Types/TypeAnnotatedInterfaceType.php | 6 ++-- src/Types/TypeAnnotatedObjectType.php | 6 ++-- 4 files changed, 39 insertions(+), 20 deletions(-) diff --git a/src/FieldsBuilder.php b/src/FieldsBuilder.php index 570d1ca7a7..236a86f3c7 100644 --- a/src/FieldsBuilder.php +++ b/src/FieldsBuilder.php @@ -115,9 +115,9 @@ public function getMutations(object $controller): array /** * @return array QueryField indexed by name. */ - public function getFields(object $controller): array + public function getFields(object $controller, ?string $typeName = null): array { - $fieldAnnotations = $this->getFieldsByAnnotations($controller, Annotations\Field::class, true); + $fieldAnnotations = $this->getFieldsByAnnotations($controller, Annotations\Field::class, true, $typeName); $refClass = new ReflectionClass($controller); @@ -186,12 +186,13 @@ public function getInputFields(string $className, string $inputName, bool $isUpd * Track Field annotation in a self targeted type * * @param class-string $className + * @param string|null $typeName * * @return array QueryField indexed by name. */ - public function getSelfFields(string $className): array + public function getSelfFields(string $className, ?string $typeName = null): array { - $fieldAnnotations = $this->getFieldsByAnnotations($className, Annotations\Field::class, false); + $fieldAnnotations = $this->getFieldsByAnnotations($className, Annotations\Field::class, false, $typeName); $refClass = new ReflectionClass($className); @@ -247,13 +248,15 @@ public function getParametersForDecorator(ReflectionMethod $refMethod): array /** * @param object|class-string $controller The controller instance, or the name of the source class name - * @param bool $injectSource Whether to inject the source object or not as the first argument. True for @Field (unless @Type has no class attribute), false for @Query and @Mutation + * @param string $annotationName + * @param bool $injectSource Whether to inject the source object or not as the first argument. True for @Field (unless @Type has no class attribute), false for @Query and @Mutation + * @param string|null $typeName Type name for which fields should be extracted for. * * @return array * * @throws ReflectionException */ - private function getFieldsByAnnotations($controller, string $annotationName, bool $injectSource): array + private function getFieldsByAnnotations($controller, string $annotationName, bool $injectSource, ?string $typeName = null): array { $refClass = new ReflectionClass($controller); $queryList = []; @@ -282,9 +285,9 @@ private function getFieldsByAnnotations($controller, string $annotationName, boo } if ($reflector instanceof ReflectionMethod) { - $fields = $this->getFieldsByMethodAnnotations($controller, $refClass, $reflector, $annotationName, $injectSource); + $fields = $this->getFieldsByMethodAnnotations($controller, $refClass, $reflector, $annotationName, $injectSource, $typeName); } else { - $fields = $this->getFieldsByPropertyAnnotations($controller, $refClass, $reflector, $annotationName); + $fields = $this->getFieldsByPropertyAnnotations($controller, $refClass, $reflector, $annotationName, $typeName); } $duplicates = array_intersect_key($reflectorByFields, $fields); @@ -312,19 +315,27 @@ private function getFieldsByAnnotations($controller, string $annotationName, boo * @param ReflectionMethod $refMethod * @param string $annotationName * @param bool $injectSource + * @param string|null $typeName * - * @return array + * @return FieldDefinition[] * * @throws AnnotationException * @throws InvalidArgumentException * @throws CannotMapTypeExceptionInterface */ - private function getFieldsByMethodAnnotations($controller, ReflectionClass $refClass, ReflectionMethod $refMethod, string $annotationName, bool $injectSource): array + private function getFieldsByMethodAnnotations($controller, ReflectionClass $refClass, ReflectionMethod $refMethod, string $annotationName, bool $injectSource, ?string $typeName = null): array { $fields = []; $annotations = $this->annotationReader->getMethodAnnotations($refMethod, $annotationName); foreach ($annotations as $queryAnnotation) { + if ($typeName && $queryAnnotation instanceof Field) { + $for = $queryAnnotation->getFor(); + if ($for && !in_array($typeName, $for)) { + continue; + } + } + $fieldDescriptor = new QueryFieldDescriptor(); $fieldDescriptor->setRefMethod($refMethod); @@ -410,19 +421,27 @@ public function handle(QueryFieldDescriptor $fieldDescriptor): ?FieldDefinition * @param ReflectionClass $refClass * @param ReflectionProperty $refProperty * @param string $annotationName + * @param string|null $typeName * - * @return array + * @return FieldDefinition[] * * @throws AnnotationException * @throws InvalidArgumentException * @throws CannotMapTypeException */ - private function getFieldsByPropertyAnnotations($controller, ReflectionClass $refClass, ReflectionProperty $refProperty, string $annotationName): array + private function getFieldsByPropertyAnnotations($controller, ReflectionClass $refClass, ReflectionProperty $refProperty, string $annotationName, ?string $typeName = null): array { $fields = []; $annotations = $this->annotationReader->getPropertyAnnotations($refProperty, $annotationName); foreach ($annotations as $queryAnnotation) { + if ($typeName && $queryAnnotation instanceof Field) { + $for = $queryAnnotation->getFor(); + if ($for && !in_array($typeName, $for)) { + continue; + } + } + $fieldDescriptor = new QueryFieldDescriptor(); $fieldDescriptor->setRefProperty($refProperty); diff --git a/src/TypeGenerator.php b/src/TypeGenerator.php index 6529721698..ec9173bfbd 100644 --- a/src/TypeGenerator.php +++ b/src/TypeGenerator.php @@ -138,7 +138,7 @@ public function extendAnnotatedObject(object $annotatedObject, MutableInterface } */ - $type->addFields(function () use ($annotatedObject) { + $type->addFields(function () use ($annotatedObject, $typeName) { /*$parentClass = get_parent_class($extendTypeAnnotation->getClass()); $parentType = null; if ($parentClass !== false) { @@ -146,7 +146,7 @@ public function extendAnnotatedObject(object $annotatedObject, MutableInterface $parentType = $recursiveTypeMapper->mapClassToType($parentClass, null); } }*/ - return $this->fieldsBuilder->getFields($annotatedObject); + return $this->fieldsBuilder->getFields($annotatedObject, $typeName); /*if ($parentType !== null) { $fields = $parentType->getFields() + $fields; diff --git a/src/Types/TypeAnnotatedInterfaceType.php b/src/Types/TypeAnnotatedInterfaceType.php index 0f46d806da..9c2c6759d8 100644 --- a/src/Types/TypeAnnotatedInterfaceType.php +++ b/src/Types/TypeAnnotatedInterfaceType.php @@ -41,7 +41,7 @@ public static function createFromAnnotatedClass(string $typeName, string $classN { return new self($className, [ 'name' => $typeName, - 'fields' => static function () use ($annotatedObject, $className, $fieldsBuilder) { + 'fields' => static function () use ($annotatedObject, $className, $fieldsBuilder, $typeName) { // There is no need for an interface that extends another interface to get all its fields. // Indeed, if the interface is used, the extended interfaces will be used too. Therefore, fetching the fields // and putting them in the child interface is a waste of resources. @@ -64,9 +64,9 @@ public static function createFromAnnotatedClass(string $typeName, string $classN }*/ if ($annotatedObject !== null) { - $fields = $fieldsBuilder->getFields($annotatedObject); + $fields = $fieldsBuilder->getFields($annotatedObject, $typeName); } else { - $fields = $fieldsBuilder->getSelfFields($className); + $fields = $fieldsBuilder->getSelfFields($className, $typeName); } //$fields += $interfaceFields; diff --git a/src/Types/TypeAnnotatedObjectType.php b/src/Types/TypeAnnotatedObjectType.php index f0b6a894eb..dbf8c63e69 100644 --- a/src/Types/TypeAnnotatedObjectType.php +++ b/src/Types/TypeAnnotatedObjectType.php @@ -29,7 +29,7 @@ public static function createFromAnnotatedClass(string $typeName, string $classN { return new self($className, [ 'name' => $typeName, - 'fields' => static function () use ($annotatedObject, $recursiveTypeMapper, $className, $fieldsBuilder) { + 'fields' => static function () use ($annotatedObject, $recursiveTypeMapper, $className, $fieldsBuilder, $typeName) { $parentClass = get_parent_class($className); $parentType = null; if ($parentClass !== false) { @@ -39,9 +39,9 @@ public static function createFromAnnotatedClass(string $typeName, string $classN } if ($annotatedObject !== null) { - $fields = $fieldsBuilder->getFields($annotatedObject); + $fields = $fieldsBuilder->getFields($annotatedObject, $typeName); } else { - $fields = $fieldsBuilder->getSelfFields($className); + $fields = $fieldsBuilder->getSelfFields($className, $typeName); } if ($parentType !== null) { $finalFields = $parentType->getFields(); From 38661b0311f6b168e61f35b1c180ab8e9ff42e29 Mon Sep 17 00:00:00 2001 From: Andrew Maslov Date: Mon, 8 Jun 2020 16:09:40 +0300 Subject: [PATCH 04/41] Implemented inputType for Field annotation --- src/Annotations/Field.php | 15 +++++++++++++++ src/FieldsBuilder.php | 3 ++- src/Mappers/Parameters/TypeHandler.php | 15 +++++++++++---- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/src/Annotations/Field.php b/src/Annotations/Field.php index fe629fd8e9..b2122c027b 100644 --- a/src/Annotations/Field.php +++ b/src/Annotations/Field.php @@ -15,6 +15,7 @@ * @Attribute("prefetchMethod", type = "string"), * @Attribute("for", type = "string[]"), * @Attribute("description", type = "string"), + * @Attribute("inputType", type = "string"), * }) */ class Field extends AbstractRequest @@ -34,6 +35,11 @@ class Field extends AbstractRequest */ private $description; + /** + * @var string|null + */ + private $inputType; + /** * @param mixed[] $attributes */ @@ -42,6 +48,7 @@ public function __construct(array $attributes = []) parent::__construct($attributes); $this->prefetchMethod = $attributes['prefetchMethod'] ?? null; $this->description = $attributes['description'] ?? null; + $this->inputType = $attributes['inputType'] ?? null; if (!empty($attributes['for'])) { $this->for = (array) $attributes['for']; @@ -71,4 +78,12 @@ public function getDescription(): ?string { return $this->description; } + + /** + * @return string|null + */ + public function getInputType(): ?string + { + return $this->inputType; + } } diff --git a/src/FieldsBuilder.php b/src/FieldsBuilder.php index 236a86f3c7..4c2b8a3277 100644 --- a/src/FieldsBuilder.php +++ b/src/FieldsBuilder.php @@ -148,6 +148,7 @@ public function getFields(object $controller, ?string $typeName = null): array * @throws AnnotationException * @throws InvalidArgumentException * @throws ReflectionException + * @throws CannotMapTypeException */ public function getInputFields(string $className, string $inputName, bool $isUpdate = false): array { @@ -170,7 +171,7 @@ public function getInputFields(string $className, string $inputName, bool $isUpd $name = $annotation->getName() ?: $reflector->getName(); - $field = $this->typeMapper->mapInputProperty($reflector, $docBlock, $name, $defaultProperties[$reflector->getName()] ?? null, $isUpdate ? true : null); + $field = $this->typeMapper->mapInputProperty($reflector, $docBlock, $name, $annotation->getInputType(), $defaultProperties[$reflector->getName()] ?? null, $isUpdate ? true : null); if ($description = $annotation->getDescription()) { $field->setDescription($description); } diff --git a/src/Mappers/Parameters/TypeHandler.php b/src/Mappers/Parameters/TypeHandler.php index 15096ec15c..d09b0bbf51 100644 --- a/src/Mappers/Parameters/TypeHandler.php +++ b/src/Mappers/Parameters/TypeHandler.php @@ -232,10 +232,13 @@ public function mapPropertyType(ReflectionProperty $refProperty, DocBlock $docBl * @param string|null $argumentName * @param mixed $defaultValue * @param bool|null $isNullable + * @param string|null $inputTypeName * * @return InputTypeProperty + * + * @throws CannotMapTypeException */ - public function mapInputProperty(ReflectionProperty $refProperty, DocBlock $docBlock, ?string $argumentName = null, $defaultValue = null, ?bool $isNullable = null): InputTypeProperty + public function mapInputProperty(ReflectionProperty $refProperty, DocBlock $docBlock, ?string $argumentName = null, ?string $inputTypeName = null, $defaultValue = null, ?bool $isNullable = null): InputTypeProperty { $docBlockComment = $docBlock->getSummary() . PHP_EOL . $docBlock->getDescription()->render(); @@ -257,11 +260,15 @@ public function mapInputProperty(ReflectionProperty $refProperty, DocBlock $docB } } - /** @var InputType $type */ - $type = $this->mapPropertyType($refProperty, $docBlock, true, $argumentName, $isNullable); + if ($inputTypeName) { + $inputType = $this->typeResolver->mapNameToInputType($inputTypeName); + } else { + $inputType = $this->mapPropertyType($refProperty, $docBlock, true, $argumentName, $isNullable); + } + $hasDefault = $defaultValue !== null || $isNullable; - $inputProperty = new InputTypeProperty($refProperty->getName(), $argumentName, $type, $hasDefault, $defaultValue, $this->argumentResolver); + $inputProperty = new InputTypeProperty($refProperty->getName(), $argumentName, $inputType, $hasDefault, $defaultValue, $this->argumentResolver); $inputProperty->setDescription(trim($docBlockComment)); return $inputProperty; From 4f3869f6e82fd79575997ab41a4fd6be03ed6153 Mon Sep 17 00:00:00 2001 From: Andrew Maslov Date: Mon, 8 Jun 2020 18:38:25 +0300 Subject: [PATCH 05/41] Adjusted code for phpstan --- phpstan.neon | 1 - src/AnnotationReader.php | 25 +++++---- src/FieldsBuilder.php | 54 +++++++++---------- src/InputTypeGenerator.php | 24 +++++---- src/InvalidDocBlockRuntimeException.php | 2 +- src/InvalidPrefetchMethodRuntimeException.php | 4 +- src/Mappers/AbstractTypeMapper.php | 2 +- src/Mappers/CannotMapTypeException.php | 6 ++- src/Mappers/GlobAnnotationsCache.php | 10 ++-- src/Mappers/GlobTypeMapperCache.php | 8 +-- src/Mappers/Parameters/TypeHandler.php | 32 +++++++---- src/NamingStrategyInterface.php | 6 ++- src/Parameters/InputTypeProperty.php | 3 +- src/QueryFieldDescriptor.php | 10 ++-- src/Types/InputType.php | 22 +++++--- 15 files changed, 121 insertions(+), 88 deletions(-) diff --git a/phpstan.neon b/phpstan.neon index fdd930053e..54c2acef96 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -3,7 +3,6 @@ parameters: ignoreErrors: - "#PHPDoc tag \\@throws with type Psr\\\\Container\\\\ContainerExceptionInterface is not subtype of Throwable#" #- "#Property TheCodingMachine\\\\GraphQLite\\\\Types\\\\ResolvableInputObjectType::\\$resolve \\(array&callable\\) does not accept array#" - - "#Variable \\$prefetchRefMethod might not be defined.#" #- "#Parameter \\#2 \\$type of class TheCodingMachine\\\\GraphQLite\\\\Parameters\\\\InputTypeParameter constructor expects GraphQL\\\\Type\\\\Definition\\\\InputType&GraphQL\\\\Type\\\\Definition\\\\Type, GraphQL\\\\Type\\\\Definition\\\\InputType\\|GraphQL\\\\Type\\\\Definition\\\\Type given.#" - "#Parameter .* of class ReflectionMethod constructor expects string, object\\|string given.#" - diff --git a/src/AnnotationReader.php b/src/AnnotationReader.php index afdc7544d7..0c32e06bbd 100644 --- a/src/AnnotationReader.php +++ b/src/AnnotationReader.php @@ -67,17 +67,17 @@ class AnnotationReader private $mode; /** - * @var object[] + * @var array */ private $methodAnnotationCache = []; /** - * @var object[][] + * @var array> */ private $methodAnnotationsCache = []; /** - * @var object[][] + * @var array> */ private $propertyAnnotationsCache = []; @@ -115,11 +115,13 @@ public function getTypeAnnotation(ReflectionClass $refClass): ?Type } /** - * @param ReflectionClass $refClass + * @param ReflectionClass $refClass * - * @return array|Input[] + * @return Input[] * * @throws AnnotationException + * + * @template T of object */ public function getInputAnnotations(ReflectionClass $refClass): array { @@ -267,7 +269,6 @@ public function getParameterAnnotationsPerParameter(array $refParameters): array */ public function getMiddlewareAnnotations($reflection): MiddlewareAnnotations { - /** @var MiddlewareAnnotationInterface[] $middlewareAnnotations */ if ($reflection instanceof ReflectionMethod) { $middlewareAnnotations = $this->getMethodAnnotations($reflection, MiddlewareAnnotationInterface::class); } else { @@ -443,18 +444,22 @@ public function getMethodAnnotations(ReflectionMethod $refMethod, string $annota /** * Returns the property's annotations. * - * @param ReflectionProperty $refProperty - * @param string $annotationClass + * @param class-string $annotationClass * - * @return array + * @return array * * @throws AnnotationException + * + * @template T of object */ public function getPropertyAnnotations(ReflectionProperty $refProperty, string $annotationClass): array { $cacheKey = $refProperty->getDeclaringClass()->getName() . '::' . $refProperty->getName() . '_s_' . $annotationClass; if (isset($this->propertyAnnotationsCache[$cacheKey])) { - return $this->propertyAnnotationsCache[$cacheKey]; + /** @var array $annotations */ + $annotations = $this->propertyAnnotationsCache[$cacheKey]; + + return $annotations; } $toAddAnnotations = []; diff --git a/src/FieldsBuilder.php b/src/FieldsBuilder.php index 4c2b8a3277..732468de11 100644 --- a/src/FieldsBuilder.php +++ b/src/FieldsBuilder.php @@ -11,6 +11,8 @@ use phpDocumentor\Reflection\DocBlock; use phpDocumentor\Reflection\DocBlock\Tags\Var_; use Psr\SimpleCache\InvalidArgumentException; +use TheCodingMachine\GraphQLite\Annotations\AbstractRequest; +use TheCodingMachine\GraphQLite\Parameters\InputTypeProperty; use TheCodingMachine\GraphQLite\Utils\PropertyAccessor; use ReflectionClass; use ReflectionException; @@ -139,16 +141,14 @@ public function getFields(object $controller, ?string $typeName = null): array } /** - * @param string $className - * @param string $inputName - * @param bool $isUpdate + * @param class-string $className + * @param string $inputName + * @param bool $isUpdate * - * @return array + * @return array * * @throws AnnotationException - * @throws InvalidArgumentException * @throws ReflectionException - * @throws CannotMapTypeException */ public function getInputFields(string $className, string $inputName, bool $isUpdate = false): array { @@ -248,10 +248,10 @@ public function getParametersForDecorator(ReflectionMethod $refMethod): array } /** - * @param object|class-string $controller The controller instance, or the name of the source class name - * @param string $annotationName - * @param bool $injectSource Whether to inject the source object or not as the first argument. True for @Field (unless @Type has no class attribute), false for @Query and @Mutation - * @param string|null $typeName Type name for which fields should be extracted for. + * @param object|class-string $controller The controller instance, or the name of the source class name + * @param class-string $annotationName + * @param bool $injectSource Whether to inject the source object or not as the first argument. True for @Field (unless @Type has no class attribute), false for @Query and @Mutation + * @param string|null $typeName Type name for which fields should be extracted for. * * @return array * @@ -293,6 +293,7 @@ private function getFieldsByAnnotations($controller, string $annotationName, boo $duplicates = array_intersect_key($reflectorByFields, $fields); if ($duplicates) { + /** @var string $name */ $name = key($duplicates); throw DuplicateMappingException::createForQuery($refClass->getName(), $name, $reflectorByFields[$name], $reflector); } @@ -305,24 +306,23 @@ private function getFieldsByAnnotations($controller, string $annotationName, boo $queryList = array_merge($queryList, $fields); } + /** @var array $queryList */ return $queryList; } /** * Gets fields by class method annotations. * - * @param string|object $controller - * @param ReflectionClass $refClass - * @param ReflectionMethod $refMethod - * @param string $annotationName - * @param bool $injectSource - * @param string|null $typeName + * @param string|object $controller + * @param ReflectionClass $refClass + * @param ReflectionMethod $refMethod + * @param class-string $annotationName + * @param bool $injectSource + * @param string|null $typeName * * @return FieldDefinition[] * * @throws AnnotationException - * @throws InvalidArgumentException - * @throws CannotMapTypeExceptionInterface */ private function getFieldsByMethodAnnotations($controller, ReflectionClass $refClass, ReflectionMethod $refMethod, string $annotationName, bool $injectSource, ?string $typeName = null): array { @@ -362,7 +362,7 @@ private function getFieldsByMethodAnnotations($controller, ReflectionClass $refC $firstParameter = array_shift($parameters); // TODO: check that $first_parameter type is correct. } - if ($prefetchMethodName !== null) { + if ($prefetchMethodName !== null && $prefetchRefMethod !== null) { $secondParameter = array_shift($parameters); if ($secondParameter === null) { throw InvalidPrefetchMethodRuntimeException::prefetchDataIgnored($prefetchRefMethod, $injectSource); @@ -418,17 +418,15 @@ public function handle(QueryFieldDescriptor $fieldDescriptor): ?FieldDefinition /** * Gets fields by class property annotations. * - * @param string|object $controller - * @param ReflectionClass $refClass - * @param ReflectionProperty $refProperty - * @param string $annotationName - * @param string|null $typeName + * @param string|object $controller + * @param ReflectionClass $refClass + * @param ReflectionProperty $refProperty + * @param class-string $annotationName + * @param string|null $typeName * * @return FieldDefinition[] * * @throws AnnotationException - * @throws InvalidArgumentException - * @throws CannotMapTypeException */ private function getFieldsByPropertyAnnotations($controller, ReflectionClass $refClass, ReflectionProperty $refProperty, string $annotationName, ?string $typeName = null): array { @@ -473,6 +471,8 @@ private function getFieldsByPropertyAnnotations($controller, ReflectionClass $re } else { $type = $this->typeMapper->mapPropertyType($refProperty, $docBlock, false); } + + /** @var OutputType&Type $type */ $fieldDescriptor->setType($type); $fieldDescriptor->setInjectSource(false); @@ -749,7 +749,7 @@ private function getDeprecationReason(DocBlock $docBlockObj): ?string * @param ReflectionMethod|ReflectionProperty $reflector * @param object $annotation * - * @return array + * @return array{0: string|null, 1: array, 2: ReflectionMethod|null} * * @throws InvalidArgumentException */ diff --git a/src/InputTypeGenerator.php b/src/InputTypeGenerator.php index 31df86b604..257aaea62c 100644 --- a/src/InputTypeGenerator.php +++ b/src/InputTypeGenerator.php @@ -20,7 +20,9 @@ class InputTypeGenerator { /** @var array */ - private $cache = []; + private $factoryCache = []; + /** @var array */ + private $inputCache = []; /** @var InputTypeUtils */ private $inputTypeUtils; /** @var FieldsBuilder */ @@ -46,29 +48,29 @@ public function mapFactoryMethod(string $factory, string $methodName, ContainerI [$inputName, $className] = $this->inputTypeUtils->getInputTypeNameAndClassName($method); - if (! isset($this->cache[$inputName])) { + if (! isset($this->factoryCache[$inputName])) { // TODO: add comment argument. - $this->cache[$inputName] = new ResolvableMutableInputObjectType($inputName, $this->fieldsBuilder, $object, $methodName, null, $this->canBeInstantiatedWithoutParameter($method, false)); + $this->factoryCache[$inputName] = new ResolvableMutableInputObjectType($inputName, $this->fieldsBuilder, $object, $methodName, null, $this->canBeInstantiatedWithoutParameter($method, false)); } - return $this->cache[$inputName]; + return $this->factoryCache[$inputName]; } /** - * @param string $className - * @param string $inputName - * @param string|null $description - * @param bool $isUpdate + * @param class-string $className + * @param string $inputName + * @param string|null $description + * @param bool $isUpdate * * @return InputType */ public function mapInput(string $className, string $inputName, ?string $description, bool $isUpdate): InputType { - if (!isset($this->cache[$inputName])) { - $this->cache[$inputName] = new InputType($className, $inputName, $description, $isUpdate, $this->fieldsBuilder); + if (!isset($this->inputCache[$inputName])) { + $this->inputCache[$inputName] = new InputType($className, $inputName, $description, $isUpdate, $this->fieldsBuilder); } - return $this->cache[$inputName]; + return $this->inputCache[$inputName]; } public static function canBeInstantiatedWithoutParameter(ReflectionFunctionAbstract $refMethod, bool $skipFirstArgument): bool diff --git a/src/InvalidDocBlockRuntimeException.php b/src/InvalidDocBlockRuntimeException.php index 606ae19d8e..0ff15f44bf 100644 --- a/src/InvalidDocBlockRuntimeException.php +++ b/src/InvalidDocBlockRuntimeException.php @@ -19,7 +19,7 @@ public static function tooManyReturnTags(ReflectionMethod $refMethod): self * * @param ReflectionProperty $refProperty */ - public static function tooManyVarTags(ReflectionProperty $refProperty) + public static function tooManyVarTags(ReflectionProperty $refProperty): self { throw new self('Property ' . $refProperty->getDeclaringClass()->getName() . '::' . $refProperty->getName() . ' has several @var annotations.'); } diff --git a/src/InvalidPrefetchMethodRuntimeException.php b/src/InvalidPrefetchMethodRuntimeException.php index 4476503b75..4be1e19faa 100644 --- a/src/InvalidPrefetchMethodRuntimeException.php +++ b/src/InvalidPrefetchMethodRuntimeException.php @@ -15,7 +15,7 @@ class InvalidPrefetchMethodRuntimeException extends GraphQLRuntimeException * @param ReflectionMethod|ReflectionProperty $reflector * @param ReflectionClass $reflectionClass */ - public static function methodNotFound($reflector, ReflectionClass $reflectionClass, string $methodName, ReflectionException $previous) + public static function methodNotFound($reflector, ReflectionClass $reflectionClass, string $methodName, ReflectionException $previous): self { throw new self('The @Field annotation in ' . $reflector->getDeclaringClass()->getName() . '::' . $reflector->getName() . ' specifies a "prefetch method" that could not be found. Unable to find method ' . $reflectionClass->getName() . '::' . $methodName . '.', 0, $previous); } @@ -24,7 +24,7 @@ public static function methodNotFound($reflector, ReflectionClass $reflectionCla * @param ReflectionMethod $annotationMethod * @param bool $isSecond */ - public static function prefetchDataIgnored(ReflectionMethod $annotationMethod, bool $isSecond) + public static function prefetchDataIgnored(ReflectionMethod $annotationMethod, bool $isSecond): self { throw new self('The @Field annotation in ' . $annotationMethod->getDeclaringClass()->getName() . '::' . $annotationMethod->getName() . ' specifies a "prefetch method" but the data from the prefetch method is not gathered. The "' . $annotationMethod->getName() . '" method should accept a ' . ($isSecond?'second':'first') . ' parameter that will contain data returned by the prefetch method.'); } diff --git a/src/Mappers/AbstractTypeMapper.php b/src/Mappers/AbstractTypeMapper.php index 7f96ffb30c..864f8ef7c0 100644 --- a/src/Mappers/AbstractTypeMapper.php +++ b/src/Mappers/AbstractTypeMapper.php @@ -133,7 +133,7 @@ private function buildMap(): GlobTypeMapperCache { $globTypeMapperCache = new GlobTypeMapperCache(); - /** @var ReflectionClass[] $classes */ + /** @var array,ReflectionClass> $classes */ $classes = $this->getClassList(); foreach ($classes as $className => $refClass) { $annotationsCache = $this->mapClassToAnnotationsCache->get($refClass, function () use ($refClass, $className) { diff --git a/src/Mappers/CannotMapTypeException.php b/src/Mappers/CannotMapTypeException.php index 213bcf3a6b..706e1997cc 100644 --- a/src/Mappers/CannotMapTypeException.php +++ b/src/Mappers/CannotMapTypeException.php @@ -16,6 +16,7 @@ use phpDocumentor\Reflection\Types\Iterable_; use phpDocumentor\Reflection\Types\Mixed_; use ReflectionMethod; +use ReflectionProperty; use TheCodingMachine\GraphQLite\Annotations\ExtendType; use function array_map; use function assert; @@ -130,9 +131,10 @@ public static function extendTypeWithBadTargetedClass(string $className, ExtendT } /** - * @param Array_|Iterable_|Mixed_ $type + * @param Array_|Iterable_|Mixed_ $type + * @param ReflectionMethod|ReflectionProperty $reflector */ - public static function createForMissingPhpDoc(PhpDocumentorType $type, ReflectionMethod $refMethod, ?string $argumentName = null): self + public static function createForMissingPhpDoc(PhpDocumentorType $type, $reflector, ?string $argumentName = null): self { $typeStr = ''; if ($type instanceof Array_) { diff --git a/src/Mappers/GlobAnnotationsCache.php b/src/Mappers/GlobAnnotationsCache.php index 8009e828b4..5b23a6abc6 100644 --- a/src/Mappers/GlobAnnotationsCache.php +++ b/src/Mappers/GlobAnnotationsCache.php @@ -28,7 +28,7 @@ final class GlobAnnotationsCache /** @var array}> An array mapping a decorator method name to an input name / declaring class */ private $decorators = []; - /** @var array, 1:bool, 2:string, 3:bool}> An array mapping an input type name to an input name / declaring class */ + /** @var array, 1: bool, 2: string|null, 3: bool}> An array mapping an input type name to an input name / declaring class */ private $inputs = []; /** @@ -95,9 +95,9 @@ public function getDecorators(): array /** * Register a new input. * - * @param string $name - * @param string $className - * @param Input $input + * @param string $name + * @param class-string $className + * @param Input $input */ public function registerInput(string $name, string $className, Input $input): void { @@ -107,7 +107,7 @@ public function registerInput(string $name, string $className, Input $input): vo /** * Returns registered inputs. * - * @return array, 1:bool, 2:string, 3:bool}> + * @return array, 1: bool, 2: string|null, 3: bool}> */ public function getInputs(): array { diff --git a/src/Mappers/GlobTypeMapperCache.php b/src/Mappers/GlobTypeMapperCache.php index 7b1278d4aa..0d96027bf8 100644 --- a/src/Mappers/GlobTypeMapperCache.php +++ b/src/Mappers/GlobTypeMapperCache.php @@ -22,9 +22,9 @@ class GlobTypeMapperCache private $mapInputNameToFactory = []; /** @var array> Maps a GraphQL type name to one or many decorators (with the @Decorator annotation) */ private $mapInputNameToDecorator = []; - /** @var array> Maps a domain class to the input */ + /** @var array,array{0: string, 1: string|null, 2: bool}> Maps a domain class to the input */ private $mapClassToInput = []; - /** @var array> Maps a GraphQL type name to the input */ + /** @var array, 1: string|null, 2: bool}> Maps a GraphQL type name to the input */ private $mapNameToInput = []; /** @@ -138,7 +138,7 @@ public function getFactoryByObjectClass(string $className): ?array /** * @param string $graphqlTypeName * - * @return array|null + * @return array{0: class-string, 1: string|null, 2: bool}|null */ public function getInputByGraphQLInputTypeName(string $graphqlTypeName): ?array { @@ -148,7 +148,7 @@ public function getInputByGraphQLInputTypeName(string $graphqlTypeName): ?array /** * @param string $className * - * @return array|null + * @return array{0: string, 1: string|null, 2: bool}|null */ public function getInputByObjectClass(string $className): ?array { diff --git a/src/Mappers/Parameters/TypeHandler.php b/src/Mappers/Parameters/TypeHandler.php index d09b0bbf51..d86d71ad49 100644 --- a/src/Mappers/Parameters/TypeHandler.php +++ b/src/Mappers/Parameters/TypeHandler.php @@ -197,7 +197,7 @@ public function mapParameter(ReflectionParameter $parameter, DocBlock $docBlock, * @param bool $toInput * @param string|null $argumentName * - * @return GraphQLType + * @return InputType&GraphQLType * * @throws CannotMapTypeException */ @@ -218,7 +218,8 @@ public function mapPropertyType(ReflectionProperty $refProperty, DocBlock $docBl $isNullable = $propertyType ? $propertyType->allowsNull() : false; } - $type = $this->mapType($phpdocType, $docBlockPropertyType, $isNullable, $toInput, null, $docBlock, $argumentName); + /** @var InputType&GraphQLType $type */ + $type = $this->mapType($phpdocType, $docBlockPropertyType, $isNullable, $toInput, $refProperty, $docBlock, $argumentName); assert($type instanceof GraphQLType && $type instanceof OutputType); return $type; @@ -267,14 +268,27 @@ public function mapInputProperty(ReflectionProperty $refProperty, DocBlock $docB } $hasDefault = $defaultValue !== null || $isNullable; + $fieldName = $argumentName ?? $refProperty->getName(); - $inputProperty = new InputTypeProperty($refProperty->getName(), $argumentName, $inputType, $hasDefault, $defaultValue, $this->argumentResolver); + $inputProperty = new InputTypeProperty($refProperty->getName(), $fieldName, $inputType, $hasDefault, $defaultValue, $this->argumentResolver); $inputProperty->setDescription(trim($docBlockComment)); return $inputProperty; } - private function mapType(Type $type, ?Type $docBlockType, bool $isNullable, bool $mapToInputType, ?ReflectionMethod $refMethod, DocBlock $docBlockObj, ?string $argumentName = null): GraphQLType + /** + * @param Type $type + * @param Type|null $docBlockType + * @param bool $isNullable + * @param bool $mapToInputType + * @param ReflectionMethod|ReflectionProperty $reflector + * @param DocBlock $docBlockObj + * @param string|null $argumentName + * + * @return (InputType&GraphQLType)|(OutputType&GraphQLType) + * @throws CannotMapTypeException + */ + private function mapType(Type $type, ?Type $docBlockType, bool $isNullable, bool $mapToInputType, $reflector, DocBlock $docBlockObj, ?string $argumentName = null): GraphQLType { $graphQlType = null; if ($isNullable && ! $type instanceof Nullable) { @@ -286,21 +300,21 @@ private function mapType(Type $type, ?Type $docBlockType, bool $isNullable, bool if ($innerType instanceof Array_ || $innerType instanceof Iterable_ || $innerType instanceof Mixed_) { // We need to use the docBlockType if ($docBlockType === null) { - throw CannotMapTypeException::createForMissingPhpDoc($innerType, $refMethod, $argumentName); + throw CannotMapTypeException::createForMissingPhpDoc($innerType, $reflector, $argumentName); } if ($mapToInputType === true) { Assert::notNull($argumentName); - $graphQlType = $this->rootTypeMapper->toGraphQLInputType($docBlockType, null, $argumentName, $refMethod, $docBlockObj); + $graphQlType = $this->rootTypeMapper->toGraphQLInputType($docBlockType, null, $argumentName, $reflector, $docBlockObj); } else { - $graphQlType = $this->rootTypeMapper->toGraphQLOutputType($docBlockType, null, $refMethod, $docBlockObj); + $graphQlType = $this->rootTypeMapper->toGraphQLOutputType($docBlockType, null, $reflector, $docBlockObj); } } else { $completeType = $this->appendTypes($type, $docBlockType); if ($mapToInputType === true) { Assert::notNull($argumentName); - $graphQlType = $this->rootTypeMapper->toGraphQLInputType($completeType, null, $argumentName, $refMethod, $docBlockObj); + $graphQlType = $this->rootTypeMapper->toGraphQLInputType($completeType, null, $argumentName, $reflector, $docBlockObj); } else { - $graphQlType = $this->rootTypeMapper->toGraphQLOutputType($completeType, null, $refMethod, $docBlockObj); + $graphQlType = $this->rootTypeMapper->toGraphQLOutputType($completeType, null, $reflector, $docBlockObj); } } diff --git a/src/NamingStrategyInterface.php b/src/NamingStrategyInterface.php index 0e69634ff7..1faf52fcd2 100644 --- a/src/NamingStrategyInterface.php +++ b/src/NamingStrategyInterface.php @@ -31,8 +31,10 @@ public function getConcreteNameFromInterfaceName(string $name): string; public function getOutputTypeName(string $typeClassName, Type $type): string; /** - * @param string $className - * @param Input|Factory $input + * Returns the GraphQL input object type name based on the type className and the Input annotation. + * + * @param class-string $className + * @param Input|Factory $input * * @return string */ diff --git a/src/Parameters/InputTypeProperty.php b/src/Parameters/InputTypeProperty.php index 23ed721fd3..812be5ed42 100644 --- a/src/Parameters/InputTypeProperty.php +++ b/src/Parameters/InputTypeProperty.php @@ -3,6 +3,7 @@ namespace TheCodingMachine\GraphQLite\Parameters; use GraphQL\Type\Definition\InputType; +use GraphQL\Type\Definition\Type; use TheCodingMachine\GraphQLite\Types\ArgumentResolver; class InputTypeProperty extends InputTypeParameter @@ -16,7 +17,7 @@ class InputTypeProperty extends InputTypeParameter /** * @param string $propertyName * @param string $fieldName - * @param InputType $type + * @param InputType&Type $type * @param bool $hasDefaultValue * @param mixed $defaultValue * @param ArgumentResolver $argumentResolver diff --git a/src/QueryFieldDescriptor.php b/src/QueryFieldDescriptor.php index 1625d3c478..ab36383d26 100644 --- a/src/QueryFieldDescriptor.php +++ b/src/QueryFieldDescriptor.php @@ -33,7 +33,7 @@ class QueryFieldDescriptor private $prefetchParameters = []; /** @var string|null */ private $prefetchMethodName; - /** @var (callable&array{0:object, 1:string})|null */ + /** @var (callable&array{0:object, 1:string})|callable|null */ private $callable; /** @var string|null */ private $targetMethodOnSource; @@ -135,7 +135,7 @@ public function setPrefetchMethodName(?string $prefetchMethodName): void * This should not be used in the context of a field middleware. * Use getResolver/setResolver if you want to wrap the resolver in another method. * - * @param callable&array{0:object, 1:string} $callable + * @param (callable&array{0:object, 1:string})|callable $callable */ public function setCallable(callable $callable): void { @@ -253,8 +253,10 @@ public function getOriginalResolver(): ResolverInterface return $this->originalResolver; } - if ($this->callable !== null) { - $this->originalResolver = new ServiceResolver($this->callable); + if (is_array($this->callable)) { + /** @var callable&array{0:object, 1:string} $callable */ + $callable = $this->callable; + $this->originalResolver = new ServiceResolver($callable); } elseif ($this->targetMethodOnSource !== null) { $this->originalResolver = new SourceResolver($this->targetMethodOnSource); } elseif ($this->targetPropertyOnSource !== null) { diff --git a/src/Types/InputType.php b/src/Types/InputType.php index 91cb937019..338098e4c5 100644 --- a/src/Types/InputType.php +++ b/src/Types/InputType.php @@ -21,16 +21,16 @@ class InputType extends MutableInputObjectType implements ResolvableMutableInput private $fields; /** - * @var string + * @var class-string */ private $className; /** - * @param string $className - * @param string $inputName - * @param string|null $description - * @param bool $isUpdate - * @param FieldsBuilder $fieldsBuilder + * @param class-string $className + * @param string $inputName + * @param string|null $description + * @param bool $isUpdate + * @param FieldsBuilder $fieldsBuilder */ public function __construct(string $className, string $inputName, ?string $description, bool $isUpdate, FieldsBuilder $fieldsBuilder) { @@ -64,8 +64,14 @@ public function __construct(string $className, string $inputName, ?string $descr $this->className = $className; } + /** - * {@inheritdoc} + * @param object|null $source + * @param array $args + * @param mixed $context + * @param ResolveInfo $resolveInfo + * + * @return object */ public function resolve(?object $source, array $args, $context, ResolveInfo $resolveInfo): object { @@ -96,7 +102,7 @@ public function decorate(callable $decorator): void /** * Creates an instance of the input class. * - * @param array $values + * @param array $values * * @return object */ From 7e647754cfe91f04dd6f73ca585e4bac86d0baea Mon Sep 17 00:00:00 2001 From: Andrew Maslov Date: Mon, 8 Jun 2020 20:31:26 +0300 Subject: [PATCH 06/41] Removed class attribute from Input annotation --- src/Annotations/Input.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Annotations/Input.php b/src/Annotations/Input.php index a003012c58..d9fdb28218 100644 --- a/src/Annotations/Input.php +++ b/src/Annotations/Input.php @@ -12,7 +12,6 @@ * @Annotation * @Target({"CLASS"}) * @Attributes({ - * @Attribute("class", type = "string"), * @Attribute("name", type = "string"), * @Attribute("default", type = "bool"), * @Attribute("decsription", type = "string"), @@ -51,7 +50,6 @@ class Input */ public function __construct(array $attributes = []) { - $this->class = $attributes['class'] ?? null; $this->name = $attributes['name'] ?? null; $this->default = $attributes['default'] ?? !isset($attributes['name']); $this->description = $attributes['description'] ?? null; From ef0f7516f87efba5e17e71379934f782e06cd652 Mon Sep 17 00:00:00 2001 From: Andrew Maslov Date: Thu, 11 Jun 2020 23:03:22 +0300 Subject: [PATCH 07/41] Fixed return type in mapPropertyType either for InputType or OutputType --- src/Mappers/Parameters/TypeHandler.php | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/Mappers/Parameters/TypeHandler.php b/src/Mappers/Parameters/TypeHandler.php index d86d71ad49..535da1e686 100644 --- a/src/Mappers/Parameters/TypeHandler.php +++ b/src/Mappers/Parameters/TypeHandler.php @@ -197,7 +197,7 @@ public function mapParameter(ReflectionParameter $parameter, DocBlock $docBlock, * @param bool $toInput * @param string|null $argumentName * - * @return InputType&GraphQLType + * @return (InputType&GraphQLType)|(OutputType&GraphQLType) * * @throws CannotMapTypeException */ @@ -218,11 +218,7 @@ public function mapPropertyType(ReflectionProperty $refProperty, DocBlock $docBl $isNullable = $propertyType ? $propertyType->allowsNull() : false; } - /** @var InputType&GraphQLType $type */ - $type = $this->mapType($phpdocType, $docBlockPropertyType, $isNullable, $toInput, $refProperty, $docBlock, $argumentName); - assert($type instanceof GraphQLType && $type instanceof OutputType); - - return $type; + return $this->mapType($phpdocType, $docBlockPropertyType, $isNullable, $toInput, $refProperty, $docBlock, $argumentName); } /** @@ -264,6 +260,7 @@ public function mapInputProperty(ReflectionProperty $refProperty, DocBlock $docB if ($inputTypeName) { $inputType = $this->typeResolver->mapNameToInputType($inputTypeName); } else { + /** @var InputType&GraphQLType $inputType */ $inputType = $this->mapPropertyType($refProperty, $docBlock, true, $argumentName, $isNullable); } From e97b6843491ed3ad573a356b196195dc2417b57b Mon Sep 17 00:00:00 2001 From: Andrew Maslov Date: Sat, 13 Jun 2020 14:34:09 +0300 Subject: [PATCH 08/41] Fixed the typo with description attribute on Input annotation --- src/Annotations/Input.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Annotations/Input.php b/src/Annotations/Input.php index d9fdb28218..f4cf7e72d1 100644 --- a/src/Annotations/Input.php +++ b/src/Annotations/Input.php @@ -14,7 +14,7 @@ * @Attributes({ * @Attribute("name", type = "string"), * @Attribute("default", type = "bool"), - * @Attribute("decsription", type = "string"), + * @Attribute("description", type = "string"), * @Attribute("update", type = "bool"), * }) */ From 93432f8516c4f24935ee749802f8de7bb4c98b66 Mon Sep 17 00:00:00 2001 From: Andrew Maslov Date: Sat, 13 Jun 2020 14:39:31 +0300 Subject: [PATCH 09/41] Fixed callable PHPDoc type in QueryFiledDescriptor --- src/QueryFieldDescriptor.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/QueryFieldDescriptor.php b/src/QueryFieldDescriptor.php index ab36383d26..d10ff532f1 100644 --- a/src/QueryFieldDescriptor.php +++ b/src/QueryFieldDescriptor.php @@ -33,7 +33,7 @@ class QueryFieldDescriptor private $prefetchParameters = []; /** @var string|null */ private $prefetchMethodName; - /** @var (callable&array{0:object, 1:string})|callable|null */ + /** @var callable|null */ private $callable; /** @var string|null */ private $targetMethodOnSource; @@ -135,7 +135,7 @@ public function setPrefetchMethodName(?string $prefetchMethodName): void * This should not be used in the context of a field middleware. * Use getResolver/setResolver if you want to wrap the resolver in another method. * - * @param (callable&array{0:object, 1:string})|callable $callable + * @param callable $callable */ public function setCallable(callable $callable): void { From 0005802111a11e8cc8e2d0391368c803229370ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Fri, 26 Jun 2020 15:22:50 +0200 Subject: [PATCH 10/41] Applying cs-fix --- phpstan.neon | 2 - src/AnnotationReader.php | 14 +--- src/Annotations/Field.php | 20 ++--- src/Annotations/Input.php | 33 ++------ src/FailedResolvingInputType.php | 13 +-- src/FieldsBuilder.php | 83 +++++++++---------- src/InputTypeGenerator.php | 7 +- src/InvalidDocBlockRuntimeException.php | 2 - src/InvalidPrefetchMethodRuntimeException.php | 5 -- src/Mappers/DuplicateMappingException.php | 16 ---- src/Mappers/GlobAnnotationsCache.php | 2 - src/Mappers/GlobTypeMapperCache.php | 4 - src/Mappers/Parameters/TypeHandler.php | 68 ++++++++------- src/Mappers/Root/BaseTypeMapper.php | 4 + src/Mappers/Root/CompoundTypeMapper.php | 4 + src/Mappers/Root/FinalRootTypeMapper.php | 4 + src/Mappers/Root/IteratorTypeMapper.php | 4 + src/Mappers/Root/MyCLabsEnumTypeMapper.php | 4 + .../Root/NullableTypeMapperAdapter.php | 4 + src/Middlewares/SourcePropertyResolver.php | 22 +---- src/Middlewares/SourceResolverInterface.php | 5 +- src/NamingStrategyInterface.php | 2 - src/Parameters/InputTypeParameter.php | 18 ++-- src/Parameters/InputTypeProperty.php | 20 ++--- src/Reflection/CachedDocBlockFactory.php | 5 +- src/Types/InputType.php | 44 ++++------ src/Utils/PropertyAccessor.php | 31 +++---- tests/Mappers/Root/VoidRootTypeMapper.php | 4 + 28 files changed, 168 insertions(+), 276 deletions(-) diff --git a/phpstan.neon b/phpstan.neon index 54c2acef96..bdf3d4d7e1 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -30,8 +30,6 @@ parameters: message: '#Method TheCodingMachine\\GraphQLite\\AnnotationReader::getMethodAnnotations\(\) should return array but returns array.#' path: src/AnnotationReader.php - '#Call to an undefined method GraphQL\\Error\\ClientAware::getMessage()#' - # Needed because of a bug in PHP-CS - - '#PHPDoc tag @param for parameter \$args with type mixed is not subtype of native type array.#' #- # message: '#If condition is always true#' diff --git a/src/AnnotationReader.php b/src/AnnotationReader.php index 0c32e06bbd..bc8aea3a54 100644 --- a/src/AnnotationReader.php +++ b/src/AnnotationReader.php @@ -66,19 +66,13 @@ class AnnotationReader */ private $mode; - /** - * @var array - */ + /** @var array */ private $methodAnnotationCache = []; - /** - * @var array> - */ + /** @var array> */ private $methodAnnotationsCache = []; - /** - * @var array> - */ + /** @var array> */ private $propertyAnnotationsCache = []; /** @@ -263,8 +257,6 @@ public function getParameterAnnotationsPerParameter(array $refParameters): array /** * @param ReflectionMethod|ReflectionProperty $reflection * - * @return MiddlewareAnnotations - * * @throws AnnotationException */ public function getMiddlewareAnnotations($reflection): MiddlewareAnnotations diff --git a/src/Annotations/Field.php b/src/Annotations/Field.php index b2122c027b..ea82625f66 100644 --- a/src/Annotations/Field.php +++ b/src/Annotations/Field.php @@ -30,14 +30,10 @@ class Field extends AbstractRequest */ private $for = null; - /** - * @var string|null - */ + /** @var string|null */ private $description; - /** - * @var string|null - */ + /** @var string|null */ private $inputType; /** @@ -50,9 +46,11 @@ public function __construct(array $attributes = []) $this->description = $attributes['description'] ?? null; $this->inputType = $attributes['inputType'] ?? null; - if (!empty($attributes['for'])) { - $this->for = (array) $attributes['for']; + if (empty($attributes['for'])) { + return; } + + $this->for = (array) $attributes['for']; } /** @@ -71,17 +69,11 @@ public function getFor(): ?array return $this->for; } - /** - * @return string|null - */ public function getDescription(): ?string { return $this->description; } - /** - * @return string|null - */ public function getInputType(): ?string { return $this->inputType; diff --git a/src/Annotations/Input.php b/src/Annotations/Input.php index f4cf7e72d1..e195629cdf 100644 --- a/src/Annotations/Input.php +++ b/src/Annotations/Input.php @@ -1,5 +1,7 @@ name = $attributes['name'] ?? null; - $this->default = $attributes['default'] ?? !isset($attributes['name']); + $this->default = $attributes['default'] ?? ! isset($attributes['name']); $this->description = $attributes['description'] ?? null; $this->update = $attributes['update'] ?? false; } /** * Returns the fully qualified class name of the targeted class. - * - * @return string */ public function getClass(): string { @@ -70,9 +60,6 @@ public function getClass(): string return $this->class; } - /** - * @param string $class - */ public function setClass(string $class): void { $this->class = $class; @@ -96,8 +83,6 @@ public function isDefault(): bool /** * Returns description about this input type. - * - * @return string|null */ public function getDescription(): ?string { @@ -107,8 +92,6 @@ public function getDescription(): ?string /** * Returns true if this type should behave as update resource. * Such input type has all fields optional and without default value in the documentation. - * - * @return bool */ public function isUpdate(): bool { diff --git a/src/FailedResolvingInputType.php b/src/FailedResolvingInputType.php index 553c838cb1..e914bb545e 100644 --- a/src/FailedResolvingInputType.php +++ b/src/FailedResolvingInputType.php @@ -1,26 +1,19 @@ $className - * @param string $inputName - * @param bool $isUpdate * * @return array * @@ -165,14 +169,15 @@ public function getInputFields(string $className, string $inputName, bool $isUpd foreach ($annotations as $annotation) { $for = $annotation->getFor(); - if ($for && !in_array($inputName, $for)) { + if ($for && ! in_array($inputName, $for)) { continue; } $name = $annotation->getName() ?: $reflector->getName(); $field = $this->typeMapper->mapInputProperty($reflector, $docBlock, $name, $annotation->getInputType(), $defaultProperties[$reflector->getName()] ?? null, $isUpdate ? true : null); - if ($description = $annotation->getDescription()) { + $description = $annotation->getDescription(); + if ($description) { $field->setDescription($description); } @@ -187,7 +192,6 @@ public function getInputFields(string $className, string $inputName, bool $isUpd * Track Field annotation in a self targeted type * * @param class-string $className - * @param string|null $typeName * * @return array QueryField indexed by name. */ @@ -260,6 +264,7 @@ public function getParametersForDecorator(ReflectionMethod $refMethod): array private function getFieldsByAnnotations($controller, string $annotationName, bool $injectSource, ?string $typeName = null): array { $refClass = new ReflectionClass($controller); + /** @var array $queryList */ $queryList = []; /** @var ReflectionMethod[]|ReflectionProperty[] $reflectorByFields */ @@ -293,20 +298,19 @@ private function getFieldsByAnnotations($controller, string $annotationName, boo $duplicates = array_intersect_key($reflectorByFields, $fields); if ($duplicates) { - /** @var string $name */ $name = key($duplicates); + assert(is_string($name)); throw DuplicateMappingException::createForQuery($refClass->getName(), $name, $reflectorByFields[$name], $reflector); } $reflectorByFields = array_merge( - $reflectorByFields, - array_fill_keys(array_keys($fields), $reflector) + $reflectorByFields, + array_fill_keys(array_keys($fields), $reflector) ); $queryList = array_merge($queryList, $fields); } - /** @var array $queryList */ return $queryList; } @@ -314,13 +318,9 @@ private function getFieldsByAnnotations($controller, string $annotationName, boo * Gets fields by class method annotations. * * @param string|object $controller - * @param ReflectionClass $refClass - * @param ReflectionMethod $refMethod * @param class-string $annotationName - * @param bool $injectSource - * @param string|null $typeName * - * @return FieldDefinition[] + * @return array * * @throws AnnotationException */ @@ -332,7 +332,7 @@ private function getFieldsByMethodAnnotations($controller, ReflectionClass $refC foreach ($annotations as $queryAnnotation) { if ($typeName && $queryAnnotation instanceof Field) { $for = $queryAnnotation->getFor(); - if ($for && !in_array($typeName, $for)) { + if ($for && ! in_array($typeName, $for)) { continue; } } @@ -404,12 +404,14 @@ public function handle(QueryFieldDescriptor $fieldDescriptor): ?FieldDefinition } }); - if ($field !== null) { - if (isset($fields[$name])) { - throw DuplicateMappingException::createForQueryInOneMethod($name, $refMethod); - } - $fields[$name] = $field; + if ($field === null) { + continue; + } + + if (isset($fields[$name])) { + throw DuplicateMappingException::createForQueryInOneMethod($name, $refMethod); } + $fields[$name] = $field; } return $fields; @@ -419,12 +421,9 @@ public function handle(QueryFieldDescriptor $fieldDescriptor): ?FieldDefinition * Gets fields by class property annotations. * * @param string|object $controller - * @param ReflectionClass $refClass - * @param ReflectionProperty $refProperty * @param class-string $annotationName - * @param string|null $typeName * - * @return FieldDefinition[] + * @return array * * @throws AnnotationException */ @@ -436,7 +435,7 @@ private function getFieldsByPropertyAnnotations($controller, ReflectionClass $re foreach ($annotations as $queryAnnotation) { if ($typeName && $queryAnnotation instanceof Field) { $for = $queryAnnotation->getFor(); - if ($for && !in_array($typeName, $for)) { + if ($for && ! in_array($typeName, $for)) { continue; } } @@ -449,7 +448,8 @@ private function getFieldsByPropertyAnnotations($controller, ReflectionClass $re /** @var Var_[] $varTags */ $varTags = $docBlock->getTagsByName('var'); - if ($varTag = reset($varTags)) { + $varTag = reset($varTags); + if ($varTag) { $docBlockComment .= PHP_EOL . $varTag->getDescription(); } @@ -470,17 +470,16 @@ private function getFieldsByPropertyAnnotations($controller, ReflectionClass $re $type = $this->typeResolver->mapNameToOutputType($outputType); } else { $type = $this->typeMapper->mapPropertyType($refProperty, $docBlock, false); + assert($type instanceof OutputType); } - /** @var OutputType&Type $type */ $fieldDescriptor->setType($type); $fieldDescriptor->setInjectSource(false); if (is_string($controller)) { $fieldDescriptor->setTargetPropertyOnSource($refProperty->getName()); } else { - $fieldDescriptor->setCallable(function () use ($controller, $refProperty) { - /** @var $controller object */ + $fieldDescriptor->setCallable(static function () use ($controller, $refProperty) { return PropertyAccessor::getValue($controller, $refProperty->getName()); }); } @@ -494,12 +493,14 @@ public function handle(QueryFieldDescriptor $fieldDescriptor): ?FieldDefinition } }); - if ($field !== null) { - if (isset($fields[$name])) { - throw DuplicateMappingException::createForQueryInOneProperty($name, $refProperty); - } - $fields[$name] = $field; + if ($field === null) { + continue; + } + + if (isset($fields[$name])) { + throw DuplicateMappingException::createForQueryInOneProperty($name, $refProperty); } + $fields[$name] = $field; } return $fields; @@ -666,7 +667,7 @@ private function getMethodFromPropertyName(ReflectionClass $reflectionClass, str $methodName = $propertyName; } else { $methodName = PropertyAccessor::findGetter($reflectionClass->getName(), $propertyName); - if (!$methodName) { + if (! $methodName) { throw FieldNotFoundException::missingField($reflectionClass->getName(), $propertyName); } } @@ -727,10 +728,6 @@ private function mapParameters(array $refParameters, DocBlock $docBlock, ?Source /** * Extracts deprecation reason from doc block. - * - * @param DocBlock $docBlockObj - * - * @return string|null */ private function getDeprecationReason(DocBlock $docBlockObj): ?string { @@ -745,9 +742,7 @@ private function getDeprecationReason(DocBlock $docBlockObj): ?string /** * Extracts prefetch method info from annotation. * - * @param ReflectionClass $refClass * @param ReflectionMethod|ReflectionProperty $reflector - * @param object $annotation * * @return array{0: string|null, 1: array, 2: ReflectionMethod|null} * diff --git a/src/InputTypeGenerator.php b/src/InputTypeGenerator.php index 257aaea62c..7d29697b6b 100644 --- a/src/InputTypeGenerator.php +++ b/src/InputTypeGenerator.php @@ -58,15 +58,10 @@ public function mapFactoryMethod(string $factory, string $methodName, ContainerI /** * @param class-string $className - * @param string $inputName - * @param string|null $description - * @param bool $isUpdate - * - * @return InputType */ public function mapInput(string $className, string $inputName, ?string $description, bool $isUpdate): InputType { - if (!isset($this->inputCache[$inputName])) { + if (! isset($this->inputCache[$inputName])) { $this->inputCache[$inputName] = new InputType($className, $inputName, $description, $isUpdate, $this->fieldsBuilder); } diff --git a/src/InvalidDocBlockRuntimeException.php b/src/InvalidDocBlockRuntimeException.php index 0ff15f44bf..08843abbb3 100644 --- a/src/InvalidDocBlockRuntimeException.php +++ b/src/InvalidDocBlockRuntimeException.php @@ -16,8 +16,6 @@ public static function tooManyReturnTags(ReflectionMethod $refMethod): self /** * Creates an exception for property to have multiple var tags. - * - * @param ReflectionProperty $refProperty */ public static function tooManyVarTags(ReflectionProperty $refProperty): self { diff --git a/src/InvalidPrefetchMethodRuntimeException.php b/src/InvalidPrefetchMethodRuntimeException.php index 4be1e19faa..8bf93129cc 100644 --- a/src/InvalidPrefetchMethodRuntimeException.php +++ b/src/InvalidPrefetchMethodRuntimeException.php @@ -13,17 +13,12 @@ class InvalidPrefetchMethodRuntimeException extends GraphQLRuntimeException { /** * @param ReflectionMethod|ReflectionProperty $reflector - * @param ReflectionClass $reflectionClass */ public static function methodNotFound($reflector, ReflectionClass $reflectionClass, string $methodName, ReflectionException $previous): self { throw new self('The @Field annotation in ' . $reflector->getDeclaringClass()->getName() . '::' . $reflector->getName() . ' specifies a "prefetch method" that could not be found. Unable to find method ' . $reflectionClass->getName() . '::' . $methodName . '.', 0, $previous); } - /** - * @param ReflectionMethod $annotationMethod - * @param bool $isSecond - */ public static function prefetchDataIgnored(ReflectionMethod $annotationMethod, bool $isSecond): self { throw new self('The @Field annotation in ' . $annotationMethod->getDeclaringClass()->getName() . '::' . $annotationMethod->getName() . ' specifies a "prefetch method" but the data from the prefetch method is not gathered. The "' . $annotationMethod->getName() . '" method should accept a ' . ($isSecond?'second':'first') . ' parameter that will contain data returned by the prefetch method.'); diff --git a/src/Mappers/DuplicateMappingException.php b/src/Mappers/DuplicateMappingException.php index a97d364fd6..d91e3a7b61 100644 --- a/src/Mappers/DuplicateMappingException.php +++ b/src/Mappers/DuplicateMappingException.php @@ -32,8 +32,6 @@ public static function createForTypeName(string $type, string $sourceClass1, str } /** - * @param string $sourceClass - * @param string $queryName * @param ReflectionMethod|ReflectionProperty $firstReflector * @param ReflectionMethod|ReflectionProperty $secondReflector * @@ -59,31 +57,17 @@ public static function createForQueryInTwoControllers(string $sourceClass1, stri throw new self(sprintf("The query/mutation '%s' is declared twice: in class '%s' and in class '%s'", $queryName, $sourceClass1, $sourceClass2)); } - /** - * @param string $queryName - * @param ReflectionMethod $method - * - * @return self - */ public static function createForQueryInOneMethod(string $queryName, ReflectionMethod $method): self { throw new self(sprintf("The query/mutation/field '%s' is declared twice in '%s::%s()'", $queryName, $method->getDeclaringClass()->getName(), $method->getName())); } - /** - * @param string $queryName - * @param ReflectionProperty $property - * - * @return self - */ public static function createForQueryInOneProperty(string $queryName, ReflectionProperty $property): self { throw new self(sprintf("The query/mutation/field '%s' is declared twice in '%s::%s'", $queryName, $property->getDeclaringClass()->getName(), $property->getName())); } /** - * @param string $sourceClass - * * @return static */ public static function createForInput(string $sourceClass): self diff --git a/src/Mappers/GlobAnnotationsCache.php b/src/Mappers/GlobAnnotationsCache.php index 5b23a6abc6..f8be4cef3a 100644 --- a/src/Mappers/GlobAnnotationsCache.php +++ b/src/Mappers/GlobAnnotationsCache.php @@ -95,9 +95,7 @@ public function getDecorators(): array /** * Register a new input. * - * @param string $name * @param class-string $className - * @param Input $input */ public function registerInput(string $name, string $className, Input $input): void { diff --git a/src/Mappers/GlobTypeMapperCache.php b/src/Mappers/GlobTypeMapperCache.php index 0d96027bf8..8fdd6612ec 100644 --- a/src/Mappers/GlobTypeMapperCache.php +++ b/src/Mappers/GlobTypeMapperCache.php @@ -136,8 +136,6 @@ public function getFactoryByObjectClass(string $className): ?array } /** - * @param string $graphqlTypeName - * * @return array{0: class-string, 1: string|null, 2: bool}|null */ public function getInputByGraphQLInputTypeName(string $graphqlTypeName): ?array @@ -146,8 +144,6 @@ public function getInputByGraphQLInputTypeName(string $graphqlTypeName): ?array } /** - * @param string $className - * * @return array{0: string, 1: string|null, 2: bool}|null */ public function getInputByObjectClass(string $className): ?array diff --git a/src/Mappers/Parameters/TypeHandler.php b/src/Mappers/Parameters/TypeHandler.php index 535da1e686..929d8979aa 100644 --- a/src/Mappers/Parameters/TypeHandler.php +++ b/src/Mappers/Parameters/TypeHandler.php @@ -45,7 +45,13 @@ use function array_unique; use function assert; use function count; +use function explode; +use function in_array; use function iterator_to_array; +use function method_exists; +use function reset; +use function trim; +use const PHP_EOL; use const SORT_REGULAR; class TypeHandler implements ParameterHandlerInterface @@ -112,20 +118,17 @@ private function getDocBlocReturnType(DocBlock $docBlock, ReflectionMethod $refM /** * Gets property type from its dock block. - * - * @param DocBlock $docBlock - * @param ReflectionProperty $refProperty - * - * @return Type|null */ private function getDocBlockPropertyType(DocBlock $docBlock, ReflectionProperty $refProperty): ?Type { /** @var Var_[] $varTags */ $varTags = $docBlock->getTagsByName('var'); - if (!$varTags) { + if (! $varTags) { return null; - } elseif (count($varTags) > 1) { + } + + if (count($varTags) > 1) { throw InvalidDocBlockRuntimeException::tooManyVarTags($refProperty); } @@ -192,11 +195,6 @@ public function mapParameter(ReflectionParameter $parameter, DocBlock $docBlock, /** * Map class property to a GraphQL type. * - * @param ReflectionProperty $refProperty - * @param DocBlock $docBlock - * @param bool $toInput - * @param string|null $argumentName - * * @return (InputType&GraphQLType)|(OutputType&GraphQLType) * * @throws CannotMapTypeException @@ -206,15 +204,20 @@ public function mapPropertyType(ReflectionProperty $refProperty, DocBlock $docBl $propertyType = null; // getType function on property reflection is available only since PHP 7.4 - if (method_exists($refProperty, 'getType') && $propertyType = $refProperty->getType()) { - $phpdocType = $this->reflectionTypeToPhpDocType($propertyType, $refProperty->getDeclaringClass()); + if (method_exists($refProperty, 'getType')) { + $propertyType = $refProperty->getType(); + if ($propertyType !== null) { + $phpdocType = $this->reflectionTypeToPhpDocType($propertyType, $refProperty->getDeclaringClass()); + } else { + $phpdocType = new Mixed_(); + } } else { $phpdocType = new Mixed_(); } $docBlockPropertyType = $this->getDocBlockPropertyType($docBlock, $refProperty); - if (null === $isNullable) { + if ($isNullable === null) { $isNullable = $propertyType ? $propertyType->allowsNull() : false; } @@ -224,14 +227,7 @@ public function mapPropertyType(ReflectionProperty $refProperty, DocBlock $docBl /** * Maps class property into input property. * - * @param ReflectionProperty $refProperty - * @param DocBlock $docBlock - * @param string|null $argumentName * @param mixed $defaultValue - * @param bool|null $isNullable - * @param string|null $inputTypeName - * - * @return InputTypeProperty * * @throws CannotMapTypeException */ @@ -241,27 +237,34 @@ public function mapInputProperty(ReflectionProperty $refProperty, DocBlock $docB /** @var Var_[] $varTags */ $varTags = $docBlock->getTagsByName('var'); - if ($varTag = reset($varTags)) { + $varTag = reset($varTags); + if ($varTag) { $docBlockComment .= PHP_EOL . $varTag->getDescription(); - if (null === $isNullable && $varType = $varTag->getType()) { - $isNullable = in_array('null', explode('|', (string) $varType)); + if ($isNullable === null) { + $varType = $varTag->getType(); + if ($varType !== null) { + $isNullable = in_array('null', explode('|', (string) $varType)); + } } } - if (null === $isNullable) { + if ($isNullable === null) { $isNullable = false; // getType function on property reflection is available only since PHP 7.4 - if (method_exists($refProperty, 'getType') && $refType = $refProperty->getType()) { - $isNullable = $refType->allowsNull(); + if (method_exists($refProperty, 'getType')) { + $refType = $refProperty->getType(); + if ($refType !== null) { + $isNullable = $refType->allowsNull(); + } } } if ($inputTypeName) { $inputType = $this->typeResolver->mapNameToInputType($inputTypeName); } else { - /** @var InputType&GraphQLType $inputType */ $inputType = $this->mapPropertyType($refProperty, $docBlock, true, $argumentName, $isNullable); + assert($inputType instanceof InputType && $inputType instanceof GraphQLType); } $hasDefault = $defaultValue !== null || $isNullable; @@ -274,15 +277,10 @@ public function mapInputProperty(ReflectionProperty $refProperty, DocBlock $docB } /** - * @param Type $type - * @param Type|null $docBlockType - * @param bool $isNullable - * @param bool $mapToInputType * @param ReflectionMethod|ReflectionProperty $reflector - * @param DocBlock $docBlockObj - * @param string|null $argumentName * * @return (InputType&GraphQLType)|(OutputType&GraphQLType) + * * @throws CannotMapTypeException */ private function mapType(Type $type, ?Type $docBlockType, bool $isNullable, bool $mapToInputType, $reflector, DocBlock $docBlockObj, ?string $argumentName = null): GraphQLType diff --git a/src/Mappers/Root/BaseTypeMapper.php b/src/Mappers/Root/BaseTypeMapper.php index 088a405448..208da5f470 100644 --- a/src/Mappers/Root/BaseTypeMapper.php +++ b/src/Mappers/Root/BaseTypeMapper.php @@ -23,6 +23,8 @@ use phpDocumentor\Reflection\Types\Object_; use phpDocumentor\Reflection\Types\String_; use Psr\Http\Message\UploadedFileInterface; +use ReflectionMethod; +use ReflectionProperty; use TheCodingMachine\GraphQLite\Mappers\CannotMapTypeException; use TheCodingMachine\GraphQLite\Mappers\CannotMapTypeExceptionInterface; use TheCodingMachine\GraphQLite\Mappers\RecursiveTypeMapperInterface; @@ -53,6 +55,7 @@ public function __construct(RootTypeMapperInterface $next, RecursiveTypeMapperIn /** * @param (OutputType&GraphQLType)|null $subType + * @param ReflectionMethod|ReflectionProperty $reflector * * @return OutputType&GraphQLType * @@ -84,6 +87,7 @@ public function toGraphQLOutputType(Type $type, ?OutputType $subType, $reflector /** * @param (InputType&GraphQLType)|null $subType + * @param ReflectionMethod|ReflectionProperty $reflector * * @return InputType&GraphQLType */ diff --git a/src/Mappers/Root/CompoundTypeMapper.php b/src/Mappers/Root/CompoundTypeMapper.php index 15cbbc4720..2cf5fd20d4 100644 --- a/src/Mappers/Root/CompoundTypeMapper.php +++ b/src/Mappers/Root/CompoundTypeMapper.php @@ -15,6 +15,8 @@ use phpDocumentor\Reflection\Type; use phpDocumentor\Reflection\Types\Compound; use phpDocumentor\Reflection\Types\Iterable_; +use ReflectionMethod; +use ReflectionProperty; use RuntimeException; use TheCodingMachine\GraphQLite\Mappers\CannotMapTypeException; use TheCodingMachine\GraphQLite\Mappers\RecursiveTypeMapperInterface; @@ -52,6 +54,7 @@ public function __construct(RootTypeMapperInterface $next, RootTypeMapperInterfa /** * @param (OutputType&GraphQLType)|null $subType + * @param ReflectionMethod|ReflectionProperty $reflector * * @return OutputType&GraphQLType */ @@ -92,6 +95,7 @@ public function toGraphQLOutputType(Type $type, ?OutputType $subType, $reflector /** * @param (InputType&GraphQLType)|null $subType + * @param ReflectionMethod|ReflectionProperty $reflector * * @return InputType&GraphQLType */ diff --git a/src/Mappers/Root/FinalRootTypeMapper.php b/src/Mappers/Root/FinalRootTypeMapper.php index db2def2bf8..3c62cc3ce1 100644 --- a/src/Mappers/Root/FinalRootTypeMapper.php +++ b/src/Mappers/Root/FinalRootTypeMapper.php @@ -10,6 +10,8 @@ use GraphQL\Type\Definition\Type as GraphQLType; use phpDocumentor\Reflection\DocBlock; use phpDocumentor\Reflection\Type; +use ReflectionMethod; +use ReflectionProperty; use TheCodingMachine\GraphQLite\Mappers\CannotMapTypeException; use TheCodingMachine\GraphQLite\Mappers\RecursiveTypeMapperInterface; @@ -31,6 +33,7 @@ public function __construct(RecursiveTypeMapperInterface $recursiveTypeMapper) /** * @param (OutputType&GraphQLType)|null $subType + * @param ReflectionMethod|ReflectionProperty $reflector * * @return OutputType&GraphQLType */ @@ -41,6 +44,7 @@ public function toGraphQLOutputType(Type $type, ?OutputType $subType, $reflector /** * @param (InputType&GraphQLType)|null $subType + * @param ReflectionMethod|ReflectionProperty $reflector * * @return InputType&GraphQLType */ diff --git a/src/Mappers/Root/IteratorTypeMapper.php b/src/Mappers/Root/IteratorTypeMapper.php index d935611816..b034290881 100644 --- a/src/Mappers/Root/IteratorTypeMapper.php +++ b/src/Mappers/Root/IteratorTypeMapper.php @@ -19,6 +19,8 @@ use phpDocumentor\Reflection\Types\Compound; use phpDocumentor\Reflection\Types\Object_; use ReflectionClass; +use ReflectionMethod; +use ReflectionProperty; use TheCodingMachine\GraphQLite\Mappers\CannotMapTypeException; use TheCodingMachine\GraphQLite\Mappers\CannotMapTypeExceptionInterface; use Webmozart\Assert\Assert; @@ -45,6 +47,7 @@ public function __construct(RootTypeMapperInterface $next, RootTypeMapperInterfa /** * @param (OutputType&GraphQLType)|null $subType + * @param ReflectionMethod|ReflectionProperty $reflector * * @return OutputType&GraphQLType */ @@ -83,6 +86,7 @@ public function toGraphQLOutputType(Type $type, ?OutputType $subType, $reflector /** * @param (InputType&GraphQLType)|null $subType + * @param ReflectionMethod|ReflectionProperty $reflector * * @return InputType&GraphQLType */ diff --git a/src/Mappers/Root/MyCLabsEnumTypeMapper.php b/src/Mappers/Root/MyCLabsEnumTypeMapper.php index cb26bdd2d9..b7137d1be4 100644 --- a/src/Mappers/Root/MyCLabsEnumTypeMapper.php +++ b/src/Mappers/Root/MyCLabsEnumTypeMapper.php @@ -14,6 +14,8 @@ use phpDocumentor\Reflection\Type; use phpDocumentor\Reflection\Types\Object_; use ReflectionClass; +use ReflectionMethod; +use ReflectionProperty; use Symfony\Contracts\Cache\CacheInterface; use TheCodingMachine\GraphQLite\AnnotationReader; use TheCodingMachine\GraphQLite\Types\MyCLabsEnumType; @@ -53,6 +55,7 @@ public function __construct(RootTypeMapperInterface $next, AnnotationReader $ann /** * @param (OutputType&GraphQLType)|null $subType + * @param ReflectionMethod|ReflectionProperty $reflector * * @return OutputType&GraphQLType */ @@ -68,6 +71,7 @@ public function toGraphQLOutputType(Type $type, ?OutputType $subType, $reflector /** * @param (InputType&GraphQLType)|null $subType + * @param ReflectionMethod|ReflectionProperty $reflector * * @return InputType&GraphQLType */ diff --git a/src/Mappers/Root/NullableTypeMapperAdapter.php b/src/Mappers/Root/NullableTypeMapperAdapter.php index b2077275d0..79720b1e2e 100644 --- a/src/Mappers/Root/NullableTypeMapperAdapter.php +++ b/src/Mappers/Root/NullableTypeMapperAdapter.php @@ -14,6 +14,8 @@ use phpDocumentor\Reflection\Types\Compound; use phpDocumentor\Reflection\Types\Null_; use phpDocumentor\Reflection\Types\Nullable; +use ReflectionMethod; +use ReflectionProperty; use TheCodingMachine\GraphQLite\Mappers\CannotMapTypeException; use TheCodingMachine\GraphQLite\Types\ResolvableMutableInputObjectType; use function array_filter; @@ -38,6 +40,7 @@ public function setNext(RootTypeMapperInterface $next): void /** * @param (OutputType&GraphQLType)|null $subType + * @param ReflectionMethod|ReflectionProperty $reflector * * @return OutputType&GraphQLType */ @@ -65,6 +68,7 @@ public function toGraphQLOutputType(Type $type, ?OutputType $subType, $reflector /** * @param (InputType&GraphQLType)|null $subType + * @param ReflectionMethod|ReflectionProperty $reflector * * @return InputType&GraphQLType */ diff --git a/src/Middlewares/SourcePropertyResolver.php b/src/Middlewares/SourcePropertyResolver.php index 8a3dc90296..fe68e4aaba 100644 --- a/src/Middlewares/SourcePropertyResolver.php +++ b/src/Middlewares/SourcePropertyResolver.php @@ -4,8 +4,8 @@ namespace TheCodingMachine\GraphQLite\Middlewares; -use TheCodingMachine\GraphQLite\Utils\PropertyAccessor; use TheCodingMachine\GraphQLite\GraphQLRuntimeException; +use TheCodingMachine\GraphQLite\Utils\PropertyAccessor; use Webmozart\Assert\Assert; use function get_class; use function is_object; @@ -18,35 +18,22 @@ */ class SourcePropertyResolver implements SourceResolverInterface { - /** - * @var string - */ + /** @var string */ private $propertyName; - /** - * @var object|null - */ + /** @var object|null */ private $object; - /** - * @param string $propertyName - */ public function __construct(string $propertyName) { $this->propertyName = $propertyName; } - /** - * {@inheritdoc} - */ public function setObject(object $object): void { $this->object = $object; } - /** - * {@inheritdoc} - */ public function getObject(): object { Assert::notNull($this->object); @@ -68,9 +55,6 @@ public function __invoke(...$args) return PropertyAccessor::getValue($this->object, $this->propertyName, ...$args); } - /** - * {@inheritdoc} - */ public function toString(): string { $class = $this->getObject(); diff --git a/src/Middlewares/SourceResolverInterface.php b/src/Middlewares/SourceResolverInterface.php index 66d09f16bc..2ee9a62b8a 100644 --- a/src/Middlewares/SourceResolverInterface.php +++ b/src/Middlewares/SourceResolverInterface.php @@ -1,5 +1,7 @@ $className * @param Input|Factory $input - * - * @return string */ public function getInputTypeName(string $className, $input): string; diff --git a/src/Parameters/InputTypeParameter.php b/src/Parameters/InputTypeParameter.php index 18554cda6c..87b18ad26f 100644 --- a/src/Parameters/InputTypeParameter.php +++ b/src/Parameters/InputTypeParameter.php @@ -63,10 +63,8 @@ public function resolve(?object $source, array $args, $context, ResolveInfo $inf throw MissingArgumentException::create($this->name); } - /** - * @return string - */ - public function getName(): string { + public function getName(): string + { return $this->name; } @@ -88,17 +86,13 @@ public function getDefaultValue() return $this->defaultValue; } - /** - * @return string - */ - public function getDescription(): string { + public function getDescription(): string + { return $this->description; } - /** - * @param string $description - */ - public function setDescription(string $description): void { + public function setDescription(string $description): void + { $this->description = $description; } } diff --git a/src/Parameters/InputTypeProperty.php b/src/Parameters/InputTypeProperty.php index 812be5ed42..f458da65f5 100644 --- a/src/Parameters/InputTypeProperty.php +++ b/src/Parameters/InputTypeProperty.php @@ -1,5 +1,7 @@ propertyName = $propertyName; } - /** - * @return string - */ - public function getPropertyName(): string { + public function getPropertyName(): string + { return $this->propertyName; } } diff --git a/src/Reflection/CachedDocBlockFactory.php b/src/Reflection/CachedDocBlockFactory.php index e1d0686a6b..de0944677b 100644 --- a/src/Reflection/CachedDocBlockFactory.php +++ b/src/Reflection/CachedDocBlockFactory.php @@ -15,6 +15,7 @@ use ReflectionProperty; use Webmozart\Assert\Assert; use function filemtime; +use function get_class; use function md5; /** @@ -48,8 +49,6 @@ public function __construct(CacheInterface $cache, ?DocBlockFactory $docBlockFac * * @param ReflectionMethod|ReflectionProperty $reflector * - * @return DocBlock - * * @throws InvalidArgumentException */ public function getDocBlock($reflector): DocBlock @@ -89,8 +88,6 @@ public function getDocBlock($reflector): DocBlock /** * @param ReflectionMethod|ReflectionProperty $reflector - * - * @return DocBlock */ private function doGetDocBlock($reflector): DocBlock { diff --git a/src/Types/InputType.php b/src/Types/InputType.php index 338098e4c5..a7557ed5f4 100644 --- a/src/Types/InputType.php +++ b/src/Types/InputType.php @@ -1,5 +1,7 @@ - */ + /** @var class-string */ private $className; /** * @param class-string $className - * @param string $inputName - * @param string|null $description - * @param bool $isUpdate - * @param FieldsBuilder $fieldsBuilder */ public function __construct(string $className, string $inputName, ?string $description, bool $isUpdate, FieldsBuilder $fieldsBuilder) { $this->fields = $fieldsBuilder->getInputFields($className, $inputName, $isUpdate); - $fields = function() use ($isUpdate) { + $fields = function () use ($isUpdate) { $fields = []; foreach ($this->fields as $name => $field) { $type = $field->getType(); @@ -46,9 +40,11 @@ public function __construct(string $className, string $inputName, ?string $descr 'description' => $field->getDescription(), ]; - if ($field->hasDefaultValue() && !$isUpdate) { - $fields[$name]['defaultValue'] = $field->getDefaultValue(); + if (! $field->hasDefaultValue() || $isUpdate) { + continue; } + + $fields[$name]['defaultValue'] = $field->getDefaultValue(); } return $fields; @@ -64,23 +60,20 @@ public function __construct(string $className, string $inputName, ?string $descr $this->className = $className; } - /** - * @param object|null $source * @param array $args * @param mixed $context - * @param ResolveInfo $resolveInfo - * - * @return object */ public function resolve(?object $source, array $args, $context, ResolveInfo $resolveInfo): object { $mappedValues = []; foreach ($this->fields as $field) { $name = $field->getName(); - if (array_key_exists($name, $args)) { - $mappedValues[$field->getPropertyName()] = $field->resolve($source, $args, $context, $resolveInfo); + if (! array_key_exists($name, $args)) { + continue; } + + $mappedValues[$field->getPropertyName()] = $field->resolve($source, $args, $context, $resolveInfo); } $instance = $this->createInstance($mappedValues); @@ -91,9 +84,6 @@ public function resolve(?object $source, array $args, $context, ResolveInfo $res return $instance; } - /** - * {@inheritdoc} - */ public function decorate(callable $decorator): void { throw FailedResolvingInputType::createForDecorator(); @@ -103,10 +93,8 @@ public function decorate(callable $decorator): void * Creates an instance of the input class. * * @param array $values - * - * @return object */ - private function createInstance(array $values) + private function createInstance(array $values): object { $refClass = new ReflectionClass($this->className); $constructor = $refClass->getConstructor(); @@ -115,7 +103,7 @@ private function createInstance(array $values) $parameters = []; foreach ($constructorParameters as $parameter) { $name = $parameter->getName(); - if (!$parameter->isDefaultValueAvailable() && empty($values[$name])) { + if (! $parameter->isDefaultValueAvailable() && empty($values[$name])) { throw FailedResolvingInputType::createForMissingConstructorParameter($refClass->getName(), $name); } diff --git a/src/Utils/PropertyAccessor.php b/src/Utils/PropertyAccessor.php index 5002e5c74c..a81058a882 100644 --- a/src/Utils/PropertyAccessor.php +++ b/src/Utils/PropertyAccessor.php @@ -1,27 +1,27 @@ $method(...$args); } @@ -67,13 +61,12 @@ public static function getValue(object $object, string $propertyName, ...$args) } /** - * @param object $instance - * @param string $propertyName * @param mixed $value */ public static function setValue(object $instance, string $propertyName, $value): void { - if ($setter = self::findSetter(get_class($instance), $propertyName)) { + $setter = self::findSetter(get_class($instance), $propertyName); + if ($setter) { $instance->$setter($value); } else { $instance->$propertyName = $value; diff --git a/tests/Mappers/Root/VoidRootTypeMapper.php b/tests/Mappers/Root/VoidRootTypeMapper.php index 5c06ee5a9e..ddaa28a2b3 100644 --- a/tests/Mappers/Root/VoidRootTypeMapper.php +++ b/tests/Mappers/Root/VoidRootTypeMapper.php @@ -10,6 +10,8 @@ use GraphQL\Type\Definition\Type as GraphQLType; use phpDocumentor\Reflection\DocBlock; use phpDocumentor\Reflection\Type; +use ReflectionMethod; +use ReflectionProperty; class VoidRootTypeMapper implements RootTypeMapperInterface { @@ -25,6 +27,7 @@ public function __construct(RootTypeMapperInterface $next) /** * @param (OutputType&GraphQLType)|null $subType + * @param ReflectionMethod|ReflectionProperty $reflector * * @return OutputType&GraphQLType */ @@ -35,6 +38,7 @@ public function toGraphQLOutputType(Type $type, ?OutputType $subType, $reflector /** * @param (InputType&GraphQLType)|null $subType + * @param ReflectionMethod|ReflectionProperty $reflector * * @return InputType&GraphQLType */ From bb69621e6d7e9851e5cf6b82b94e25df5b54a136 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Fri, 26 Jun 2020 16:03:17 +0200 Subject: [PATCH 11/41] Adding a integration test to test @Field annotation in properties. --- tests/Fixtures/Integration/Models/Contact.php | 19 ++++ tests/Integration/EndToEndTest.php | 97 +++++++++++++------ 2 files changed, 88 insertions(+), 28 deletions(-) diff --git a/tests/Fixtures/Integration/Models/Contact.php b/tests/Fixtures/Integration/Models/Contact.php index 297756e6de..9defaeae41 100644 --- a/tests/Fixtures/Integration/Models/Contact.php +++ b/tests/Fixtures/Integration/Models/Contact.php @@ -4,6 +4,7 @@ namespace TheCodingMachine\GraphQLite\Fixtures\Integration\Models; +use DateTimeImmutable; use TheCodingMachine\GraphQLite\Annotations\MagicField; use function array_search; use DateTimeInterface; @@ -46,6 +47,16 @@ class Contact * @var string */ private $company; + /** + * @Field() + * @var int + */ + private $age = 42; + /** + * @Field() + * @var string + */ + public $nickName = 'foo'; public function __construct(string $name) { @@ -182,6 +193,14 @@ public function secret(): string return 'you can see this only if you have the good right'; } + /** + * @return int + */ + public function getAge(): int + { + return $this->age; + } + /** * @Field() * @Autowire(for="testService", identifier="testService") diff --git a/tests/Integration/EndToEndTest.php b/tests/Integration/EndToEndTest.php index 8bceb71600..a13751742a 100644 --- a/tests/Integration/EndToEndTest.php +++ b/tests/Integration/EndToEndTest.php @@ -4,6 +4,7 @@ use Doctrine\Common\Annotations\AnnotationReader as DoctrineAnnotationReader; use GraphQL\Error\Debug; +use GraphQL\Executor\ExecutionResult; use GraphQL\GraphQL; use Mouf\Picotainer\Picotainer; use PHPUnit\Framework\TestCase; @@ -66,8 +67,10 @@ use TheCodingMachine\GraphQLite\Types\ArgumentResolver; use TheCodingMachine\GraphQLite\Types\TypeResolver; use TheCodingMachine\GraphQLite\Utils\Namespaces\NamespaceFactory; +use function json_encode; use function var_dump; use function var_export; +use const JSON_PRETTY_PRINT; class EndToEndTest extends TestCase { @@ -275,6 +278,17 @@ public function createContainer(array $overloadedServices = []): ContainerInterf return $container; } + /** + * @return mixed + */ + private function getSuccessResult(ExecutionResult $result, int $debugFlag = Debug::RETHROW_INTERNAL_EXCEPTIONS) { + $array = $result->toArray($debugFlag); + if (isset($array['errors']) || !isset($array['data'])) { + $this->fail('Expected a successful answer. Got '.json_encode($array, JSON_PRETTY_PRINT)); + } + return $array['data']; + } + public function testEndToEnd(): void { /** @@ -325,7 +339,7 @@ public function testEndToEnd(): void ] ] - ], $result->toArray(Debug::RETHROW_INTERNAL_EXCEPTIONS)['data']); + ], $this->getSuccessResult($result)); // Let's redo this to test cache. $result = GraphQL::executeQuery( @@ -354,7 +368,7 @@ public function testEndToEnd(): void ] ] - ], $result->toArray(Debug::RETHROW_INTERNAL_EXCEPTIONS)['data']); + ], $this->getSuccessResult($result)); } public function testDeprecatedField(): void @@ -400,7 +414,7 @@ public function testDeprecatedField(): void ] ] - ], $result->toArray(Debug::RETHROW_INTERNAL_EXCEPTIONS)['data']); + ], $this->getSuccessResult($result)); // Let's introspect to see if the field is marked as deprecated // in the resulting GraphQL schema @@ -423,7 +437,7 @@ public function testDeprecatedField(): void new Context() ); - $fields = $result->toArray(Debug::RETHROW_INTERNAL_EXCEPTIONS)['data']['__type']['fields']; + $fields = $this->getSuccessResult($result)['__type']['fields']; $deprecatedFields = [ 'deprecatedUppercaseName', 'deprecatedName' @@ -511,7 +525,7 @@ public function testEndToEndInputTypeDate() 'name' => 'Bill', 'birthDate' => '1942-12-24T00:00:00+00:00', ] - ], $result->toArray(Debug::RETHROW_INTERNAL_EXCEPTIONS)['data']); + ], $this->getSuccessResult($result)); } public function testEndToEndInputTypeDateAsParam() @@ -544,7 +558,7 @@ public function testEndToEndInputTypeDateAsParam() 'name' => 'Bill', 'birthDate' => '1942-12-24T00:00:00+00:00', ] - ], $result->toArray(Debug::RETHROW_INTERNAL_EXCEPTIONS)['data']); + ], $this->getSuccessResult($result)); } public function testEndToEndInputType() @@ -590,7 +604,7 @@ public function testEndToEndInputType() ] ] ] - ], $result->toArray(Debug::RETHROW_INTERNAL_EXCEPTIONS)['data']); + ], $this->getSuccessResult($result)); } public function testEndToEndPorpaginas(): void @@ -631,7 +645,7 @@ public function testEndToEndPorpaginas(): void ], 'count' => 2 ] - ], $result->toArray(Debug::RETHROW_INTERNAL_EXCEPTIONS)['data']); + ], $this->getSuccessResult($result)); // Let's redo this to test cache. $result = GraphQL::executeQuery( @@ -650,7 +664,7 @@ public function testEndToEndPorpaginas(): void ], 'count' => 2 ] - ], $result->toArray(Debug::RETHROW_INTERNAL_EXCEPTIONS)['data']); + ], $this->getSuccessResult($result)); // Let's run a query with no limit but an offset $invalidQueryString = ' @@ -708,7 +722,7 @@ public function testEndToEndPorpaginas(): void ], 'count' => 2 ] - ], $result->toArray(Debug::RETHROW_INTERNAL_EXCEPTIONS)['data']); + ], $this->getSuccessResult($result)); } public function testEndToEndPorpaginasOnScalarType(): void @@ -737,7 +751,7 @@ public function testEndToEndPorpaginasOnScalarType(): void 'items' => ['Bill'], 'count' => 2 ] - ], $result->toArray(Debug::RETHROW_INTERNAL_EXCEPTIONS)['data']); + ], $this->getSuccessResult($result)); } /** @@ -800,7 +814,7 @@ public function testEndToEnd2Iterators(): void ], 'count' => 1 ] - ], $result->toArray(Debug::RETHROW_INTERNAL_EXCEPTIONS)['data']); + ], $this->getSuccessResult($result)); } @@ -824,7 +838,7 @@ public function testEndToEndStaticFactories(): void $this->assertSame([ 'echoFilters' => [ "foo", "bar", "12", "42", "62" ] - ], $result->toArray(Debug::RETHROW_INTERNAL_EXCEPTIONS)['data']); + ], $this->getSuccessResult($result)); // Call again to test GlobTypeMapper cache $result = GraphQL::executeQuery( @@ -834,7 +848,7 @@ public function testEndToEndStaticFactories(): void $this->assertSame([ 'echoFilters' => [ "foo", "bar", "12", "42", "62" ] - ], $result->toArray(Debug::RETHROW_INTERNAL_EXCEPTIONS)['data']); + ], $this->getSuccessResult($result)); } public function testNonNullableTypesWithOptionnalFactoryArguments(): void @@ -857,7 +871,7 @@ public function testNonNullableTypesWithOptionnalFactoryArguments(): void $this->assertSame([ 'echoFilters' => [] - ], $result->toArray(Debug::RETHROW_INTERNAL_EXCEPTIONS)['data']); + ], $this->getSuccessResult($result)); } public function testNullableTypesWithOptionnalFactoryArguments(): void @@ -880,7 +894,7 @@ public function testNullableTypesWithOptionnalFactoryArguments(): void $this->assertSame([ 'echoNullableFilters' => null - ], $result->toArray(Debug::RETHROW_INTERNAL_EXCEPTIONS)['data']); + ], $this->getSuccessResult($result)); } public function testEndToEndResolveInfo(): void @@ -903,7 +917,7 @@ public function testEndToEndResolveInfo(): void $this->assertSame([ 'echoResolveInfo' => 'echoResolveInfo' - ], $result->toArray(Debug::RETHROW_INTERNAL_EXCEPTIONS)['data']); + ], $this->getSuccessResult($result)); } public function testEndToEndRightIssues(): void @@ -976,7 +990,7 @@ public function testAutowireService(): void ] ] - ], $result->toArray(Debug::RETHROW_INTERNAL_EXCEPTIONS)['data']); + ], $this->getSuccessResult($result)); } public function testParameterAnnotationsInSourceField(): void @@ -1009,7 +1023,7 @@ public function testParameterAnnotationsInSourceField(): void ] ] - ], $result->toArray(Debug::RETHROW_INTERNAL_EXCEPTIONS)['data']); + ], $this->getSuccessResult($result)); } public function testEndToEndEnums(): void @@ -1032,7 +1046,7 @@ public function testEndToEndEnums(): void $this->assertSame([ 'echoProductType' => 'NON_FOOD' - ], $result->toArray(Debug::RETHROW_INTERNAL_EXCEPTIONS)['data']); + ], $this->getSuccessResult($result)); } public function testEndToEndEnums2(): void @@ -1055,7 +1069,7 @@ public function testEndToEndEnums2(): void $this->assertSame([ 'echoSomeProductType' => 'FOOD' - ], $result->toArray(Debug::RETHROW_INTERNAL_EXCEPTIONS)['data']); + ], $this->getSuccessResult($result)); } public function testEndToEndEnums3(): void @@ -1085,7 +1099,7 @@ public function testEndToEndEnums3(): void $this->assertSame([ 'echoProductType' => 'NON_FOOD' - ], $result->toArray(Debug::RETHROW_INTERNAL_EXCEPTIONS)['data']); + ], $this->getSuccessResult($result)); } public function testEndToEndDateTime(): void @@ -1108,7 +1122,7 @@ public function testEndToEndDateTime(): void $this->assertSame([ 'echoDate' => '2019-05-05T01:02:03+00:00' - ], $result->toArray(Debug::RETHROW_INTERNAL_EXCEPTIONS)['data']); + ], $this->getSuccessResult($result)); } public function testEndToEndErrorHandlingOfInconstentTypesInArrays(): void @@ -1164,7 +1178,7 @@ public function testEndToEndNonDefaultOutputType(): void 'fullName' => 'JOE', 'phone' => '0123456789' ] - ], $result->toArray(Debug::RETHROW_INTERNAL_EXCEPTIONS)['data']); + ], $this->getSuccessResult($result)); } public function testEndToEndSecurityAnnotation(): void @@ -1187,7 +1201,7 @@ public function testEndToEndSecurityAnnotation(): void $this->assertSame([ 'secretPhrase' => 'you can see this secret only if passed parameter is "foo"' - ], $result->toArray(Debug::RETHROW_INTERNAL_EXCEPTIONS)['data']); + ], $this->getSuccessResult($result)); $queryString = ' query { @@ -1226,7 +1240,7 @@ public function testEndToEndSecurityFailWithAnnotation(): void $this->assertSame([ 'nullableSecretPhrase' => null - ], $result->toArray(Debug::RETHROW_INTERNAL_EXCEPTIONS)['data']); + ], $this->getSuccessResult($result)); // Test with @FailWith annotation $queryString = ' @@ -1242,7 +1256,7 @@ public function testEndToEndSecurityFailWithAnnotation(): void $this->assertSame([ 'nullableSecretPhrase2' => null - ], $result->toArray(Debug::RETHROW_INTERNAL_EXCEPTIONS)['data']); + ], $this->getSuccessResult($result)); } public function testEndToEndSecurityWithUser(): void @@ -1487,7 +1501,7 @@ public function testEndToEndMagicFieldWithPhpType(): void ] ], ] - ], $result->toArray(Debug::RETHROW_INTERNAL_EXCEPTIONS)['data']); + ], $this->getSuccessResult($result)); } public function testEndToEndInjectUser(): void @@ -1545,4 +1559,31 @@ public function testInputOutputNameConflict(): void $schema->validate(); } + + public function testEndToEndFieldAnnotationInProperty(): void + { + /** + * @var Schema $schema + */ + $schema = $this->mainContainer->get(Schema::class); + + $queryString = ' + query { + contacts { + age + nickName + } + } + '; + + $result = GraphQL::executeQuery( + $schema, + $queryString + ); + + $data = $this->getSuccessResult($result); + + $this->assertSame(42, $data['contacts'][0]['age']); + $this->assertSame('foo', $data['contacts'][0]['nickName']); + } } From ecc7386ccbfe58a6c5ef6a6bd3d3617b2a237de7 Mon Sep 17 00:00:00 2001 From: Andrew Maslov Date: Sat, 27 Jun 2020 18:27:36 +0300 Subject: [PATCH 12/41] Fixed the bug about breaking access to child fields --- src/FieldsBuilder.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/FieldsBuilder.php b/src/FieldsBuilder.php index 0bd0def917..58f7b20aaf 100644 --- a/src/FieldsBuilder.php +++ b/src/FieldsBuilder.php @@ -287,7 +287,7 @@ private function getFieldsByAnnotations($controller, string $annotationName, boo if ($closestMatchingTypeClass !== null && $closestMatchingTypeClass === $reflector->getDeclaringClass()->getName()) { // Optimisation: no need to fetch annotations from parent classes that are ALREADY GraphQL types. // We will merge the fields anyway. - break; + continue; } if ($reflector instanceof ReflectionMethod) { From b7df51be38c35947a80c15afd40e8c88621e755b Mon Sep 17 00:00:00 2001 From: Andrew Maslov Date: Sat, 27 Jun 2020 19:18:12 +0300 Subject: [PATCH 13/41] Adjusted Field::description for output type --- src/FieldsBuilder.php | 46 +++++++++++++++++++++++++++---------------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/src/FieldsBuilder.php b/src/FieldsBuilder.php index 58f7b20aaf..6455c5e231 100644 --- a/src/FieldsBuilder.php +++ b/src/FieldsBuilder.php @@ -330,26 +330,32 @@ private function getFieldsByMethodAnnotations($controller, ReflectionClass $refC $annotations = $this->annotationReader->getMethodAnnotations($refMethod, $annotationName); foreach ($annotations as $queryAnnotation) { - if ($typeName && $queryAnnotation instanceof Field) { + $description = null; + + if ($queryAnnotation instanceof Field) { $for = $queryAnnotation->getFor(); - if ($for && ! in_array($typeName, $for)) { + if ($typeName && $for && ! in_array($typeName, $for)) { continue; } + + $description = $queryAnnotation->getDescription(); } $fieldDescriptor = new QueryFieldDescriptor(); $fieldDescriptor->setRefMethod($refMethod); $docBlockObj = $this->cachedDocBlockFactory->getDocBlock($refMethod); - $docBlockComment = $docBlockObj->getSummary() . "\n" . $docBlockObj->getDescription()->render(); - $fieldDescriptor->setDeprecationReason($this->getDeprecationReason($docBlockObj)); $methodName = $refMethod->getName(); $name = $queryAnnotation->getName() ?: $this->namingStrategy->getFieldNameFromMethodName($methodName); + if (!$description) { + $description = $docBlockObj->getSummary() . "\n" . $docBlockObj->getDescription()->render(); + } + $fieldDescriptor->setName($name); - $fieldDescriptor->setComment($docBlockComment); + $fieldDescriptor->setComment($description); [$prefetchMethodName, $prefetchArgs, $prefetchRefMethod] = $this->getPrefetchMethodInfo($refClass, $refMethod, $queryAnnotation); if ($prefetchMethodName) { @@ -433,31 +439,37 @@ private function getFieldsByPropertyAnnotations($controller, ReflectionClass $re $annotations = $this->annotationReader->getPropertyAnnotations($refProperty, $annotationName); foreach ($annotations as $queryAnnotation) { - if ($typeName && $queryAnnotation instanceof Field) { + $description = null; + + if ($queryAnnotation instanceof Field) { $for = $queryAnnotation->getFor(); - if ($for && ! in_array($typeName, $for)) { + if ($typeName && $for && ! in_array($typeName, $for)) { continue; } + + $description = $queryAnnotation->getDescription(); } $fieldDescriptor = new QueryFieldDescriptor(); $fieldDescriptor->setRefProperty($refProperty); $docBlock = $this->cachedDocBlockFactory->getDocBlock($refProperty); - $docBlockComment = $docBlock->getSummary() . PHP_EOL . $docBlock->getDescription()->render(); + $fieldDescriptor->setDeprecationReason($this->getDeprecationReason($docBlock)); + $name = $queryAnnotation->getName() ?: $refProperty->getName(); - /** @var Var_[] $varTags */ - $varTags = $docBlock->getTagsByName('var'); - $varTag = reset($varTags); - if ($varTag) { - $docBlockComment .= PHP_EOL . $varTag->getDescription(); - } + if (!$description) { + $description = $docBlock->getSummary() . PHP_EOL . $docBlock->getDescription()->render(); - $fieldDescriptor->setDeprecationReason($this->getDeprecationReason($docBlock)); + /** @var Var_[] $varTags */ + $varTags = $docBlock->getTagsByName('var'); + $varTag = reset($varTags); + if ($varTag) { + $description .= PHP_EOL . $varTag->getDescription(); + } + } - $name = $queryAnnotation->getName() ?: $refProperty->getName(); $fieldDescriptor->setName($name); - $fieldDescriptor->setComment($docBlockComment); + $fieldDescriptor->setComment($description); [$prefetchMethodName, $prefetchArgs] = $this->getPrefetchMethodInfo($refClass, $refProperty, $queryAnnotation); if ($prefetchMethodName) { From 8f22ee68b7aff31b1c0aae997368551fa3797d72 Mon Sep 17 00:00:00 2001 From: Andrew Maslov Date: Sat, 27 Jun 2020 19:24:33 +0300 Subject: [PATCH 14/41] Applying cs fix --- src/FieldsBuilder.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/FieldsBuilder.php b/src/FieldsBuilder.php index 6455c5e231..234a0c0fc9 100644 --- a/src/FieldsBuilder.php +++ b/src/FieldsBuilder.php @@ -350,7 +350,7 @@ private function getFieldsByMethodAnnotations($controller, ReflectionClass $refC $methodName = $refMethod->getName(); $name = $queryAnnotation->getName() ?: $this->namingStrategy->getFieldNameFromMethodName($methodName); - if (!$description) { + if (! $description) { $description = $docBlockObj->getSummary() . "\n" . $docBlockObj->getDescription()->render(); } @@ -457,7 +457,7 @@ private function getFieldsByPropertyAnnotations($controller, ReflectionClass $re $fieldDescriptor->setDeprecationReason($this->getDeprecationReason($docBlock)); $name = $queryAnnotation->getName() ?: $refProperty->getName(); - if (!$description) { + if (! $description) { $description = $docBlock->getSummary() . PHP_EOL . $docBlock->getDescription()->render(); /** @var Var_[] $varTags */ From 0548019118ef22d4774fbe44e46ef02b0cecc41b Mon Sep 17 00:00:00 2001 From: Andrew Maslov Date: Sun, 13 Dec 2020 00:41:22 +0200 Subject: [PATCH 15/41] Fixed Fields constructor param typehint --- src/Annotations/Field.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Annotations/Field.php b/src/Annotations/Field.php index a6612f218a..41ac80d0a9 100644 --- a/src/Annotations/Field.php +++ b/src/Annotations/Field.php @@ -39,6 +39,7 @@ class Field extends AbstractRequest /** * @param mixed[] $attributes + * @param string|string[] $for */ public function __construct(array $attributes = [], ?string $name = null, ?string $outputType = null, ?string $prefetchMethod = null, $for = null, ?string $description = null, ?string $inputType = null) { From 794f6f382289da416d33007227d81824ab20bfb2 Mon Sep 17 00:00:00 2001 From: Andrew Maslov Date: Sat, 23 Jan 2021 16:13:37 +0200 Subject: [PATCH 16/41] Throw errors when field cannot be accessed --- src/Utils/AccessPropertyException.php | 22 +++++++++ src/Utils/PropertyAccessor.php | 49 ++++++++++++++++--- tests/Fixtures/Integration/Models/Contact.php | 31 ++++++++++++ tests/Integration/EndToEndTest.php | 22 +++++++++ 4 files changed, 118 insertions(+), 6 deletions(-) create mode 100644 src/Utils/AccessPropertyException.php diff --git a/src/Utils/AccessPropertyException.php b/src/Utils/AccessPropertyException.php new file mode 100644 index 0000000000..089f916288 --- /dev/null +++ b/src/Utils/AccessPropertyException.php @@ -0,0 +1,22 @@ +$method(...$args); } - return $object->$propertyName; + if (self::publicPropertyExists($class, $propertyName)) { + return $object->$propertyName; + } + + throw AccessPropertyException::createForUnreadableProperty($class, $propertyName); } /** @@ -65,11 +73,40 @@ public static function getValue(object $object, string $propertyName, ...$args) */ public static function setValue(object $instance, string $propertyName, $value): void { - $setter = self::findSetter(get_class($instance), $propertyName); + $class = get_class($instance); + + $setter = self::findSetter($class, $propertyName); if ($setter) { $instance->$setter($value); - } else { + return; + } + + if (self::publicPropertyExists($class, $propertyName)) { $instance->$propertyName = $value; } + + throw AccessPropertyException::createForUnwritableProperty($class, $propertyName); + } + + private static function publicPropertyExists(string $class, string $propertyName): bool + { + if (!property_exists($class, $propertyName)) { + return false; + } + + $reflection = new ReflectionProperty($class, $propertyName); + + return $reflection->isPublic(); + } + + private static function publicMethodExists(string $class, string $methodName): bool + { + if (!method_exists($class, $methodName)) { + return false; + } + + $reflection = new ReflectionMethod($class, $methodName); + + return $reflection->isPublic(); } } diff --git a/tests/Fixtures/Integration/Models/Contact.php b/tests/Fixtures/Integration/Models/Contact.php index 9defaeae41..306ebe2bf7 100644 --- a/tests/Fixtures/Integration/Models/Contact.php +++ b/tests/Fixtures/Integration/Models/Contact.php @@ -57,6 +57,21 @@ class Contact * @var string */ public $nickName = 'foo'; + /** + * @Field() + * @var string + */ + public $status = 'foo'; + /** + * @Field() + * @var string + */ + public $address = 'foo'; + /** + * @Field() + * @var bool + */ + private $private = true; public function __construct(string $name) { @@ -201,6 +216,22 @@ public function getAge(): int return $this->age; } + /** + * @return string + */ + public function getStatus(): string + { + return 'bar'; + } + + /** + * @return string + */ + private function getAddress(): string + { + return $this->address; + } + /** * @Field() * @Autowire(for="testService", identifier="testService") diff --git a/tests/Integration/EndToEndTest.php b/tests/Integration/EndToEndTest.php index 26369b72f6..dbaa0bb855 100644 --- a/tests/Integration/EndToEndTest.php +++ b/tests/Integration/EndToEndTest.php @@ -66,6 +66,7 @@ use TheCodingMachine\GraphQLite\TypeRegistry; use TheCodingMachine\GraphQLite\Types\ArgumentResolver; use TheCodingMachine\GraphQLite\Types\TypeResolver; +use TheCodingMachine\GraphQLite\Utils\AccessPropertyException; use TheCodingMachine\GraphQLite\Utils\Namespaces\NamespaceFactory; use function json_encode; use function var_dump; @@ -1597,6 +1598,8 @@ public function testEndToEndFieldAnnotationInProperty(): void contacts { age nickName + status + address } } '; @@ -1610,5 +1613,24 @@ public function testEndToEndFieldAnnotationInProperty(): void $this->assertSame(42, $data['contacts'][0]['age']); $this->assertSame('foo', $data['contacts'][0]['nickName']); + $this->assertSame('bar', $data['contacts'][0]['status']); + $this->assertSame('foo', $data['contacts'][0]['address']); + + $queryString = ' + query { + contacts { + private + } + } + '; + + $result = GraphQL::executeQuery( + $schema, + $queryString + ); + + $this->expectException(AccessPropertyException::class); + $this->expectExceptionMessage("Could not get value from property 'TheCodingMachine\GraphQLite\Fixtures\Integration\Models\Contact::private'. Either make the property public or add a public getter for it like this: 'getPrivate' or 'isPrivate'"); + $result->toArray(Debug::RETHROW_INTERNAL_EXCEPTIONS); } } From 0797af760fcb4dd96e4ee1e7cb9a33d14148ff63 Mon Sep 17 00:00:00 2001 From: Andrew Maslov Date: Sat, 23 Jan 2021 18:07:09 +0200 Subject: [PATCH 17/41] Added test for FailWith annotation used on property --- src/Annotations/FailWith.php | 2 +- tests/Fixtures/Integration/Models/Contact.php | 8 ++++++++ tests/Integration/EndToEndTest.php | 17 +++++++++++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/Annotations/FailWith.php b/src/Annotations/FailWith.php index 75f4ed666e..bb1aa6f0cc 100644 --- a/src/Annotations/FailWith.php +++ b/src/Annotations/FailWith.php @@ -18,7 +18,7 @@ * @Attribute("mode", type = "string") * }) */ -#[Attribute(Attribute::TARGET_METHOD)] +#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_METHOD)] class FailWith implements MiddlewareAnnotationInterface { /** diff --git a/tests/Fixtures/Integration/Models/Contact.php b/tests/Fixtures/Integration/Models/Contact.php index 306ebe2bf7..fdb73392e0 100644 --- a/tests/Fixtures/Integration/Models/Contact.php +++ b/tests/Fixtures/Integration/Models/Contact.php @@ -5,6 +5,7 @@ use DateTimeImmutable; +use TheCodingMachine\GraphQLite\Annotations\FailWith; use TheCodingMachine\GraphQLite\Annotations\MagicField; use function array_search; use DateTimeInterface; @@ -72,6 +73,13 @@ class Contact * @var bool */ private $private = true; + /** + * @Field() + * @Right("NO_ACCESS") + * @FailWith(null) + * @var string + */ + public $failWithNull = 'This should fail with NULL!'; public function __construct(string $name) { diff --git a/tests/Integration/EndToEndTest.php b/tests/Integration/EndToEndTest.php index dbaa0bb855..5da69f6fae 100644 --- a/tests/Integration/EndToEndTest.php +++ b/tests/Integration/EndToEndTest.php @@ -1258,6 +1258,23 @@ public function testEndToEndSecurityFailWithAnnotation(): void $this->assertSame([ 'nullableSecretPhrase2' => null ], $this->getSuccessResult($result)); + + // Test with @FailWith annotation on property + $queryString = ' + query { + contacts { + failWithNull + } + } + '; + + $result = GraphQL::executeQuery( + $schema, + $queryString + ); + + $data = $this->getSuccessResult($result); + $this->assertSame(null, $data['contacts'][0]['failWithNull']); } public function testEndToEndSecurityWithUser(): void From 697a2967f09f50ea9bdbc43f45540e108b2490e4 Mon Sep 17 00:00:00 2001 From: Andrew Maslov Date: Sat, 23 Jan 2021 19:08:05 +0200 Subject: [PATCH 18/41] Added security tests on properties --- src/Annotations/HideIfUnauthorized.php | 2 +- src/Annotations/Logged.php | 2 +- src/Annotations/Right.php | 2 +- src/Annotations/Security.php | 2 +- tests/Fixtures/Integration/Models/Contact.php | 27 ++++++++ tests/Integration/EndToEndTest.php | 62 +++++++++++++++++++ 6 files changed, 93 insertions(+), 4 deletions(-) diff --git a/src/Annotations/HideIfUnauthorized.php b/src/Annotations/HideIfUnauthorized.php index 7138d05996..98727c7655 100644 --- a/src/Annotations/HideIfUnauthorized.php +++ b/src/Annotations/HideIfUnauthorized.php @@ -13,7 +13,7 @@ * @Annotation * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) */ -#[Attribute(Attribute::TARGET_METHOD)] +#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_METHOD)] class HideIfUnauthorized implements MiddlewareAnnotationInterface { } diff --git a/src/Annotations/Logged.php b/src/Annotations/Logged.php index ab96ee2257..e802dcd29d 100644 --- a/src/Annotations/Logged.php +++ b/src/Annotations/Logged.php @@ -10,7 +10,7 @@ * @Annotation * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) */ -#[Attribute(Attribute::TARGET_METHOD)] +#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_METHOD)] class Logged implements MiddlewareAnnotationInterface { } diff --git a/src/Annotations/Right.php b/src/Annotations/Right.php index 1313105df5..f4562ba423 100644 --- a/src/Annotations/Right.php +++ b/src/Annotations/Right.php @@ -16,7 +16,7 @@ * @Attribute("name", type = "string"), * }) */ -#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] +#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] class Right implements MiddlewareAnnotationInterface { /** @var string */ diff --git a/src/Annotations/Security.php b/src/Annotations/Security.php index 26c46d4582..59eda63d60 100644 --- a/src/Annotations/Security.php +++ b/src/Annotations/Security.php @@ -24,7 +24,7 @@ * @Attribute("message", type = "string"), * }) */ -#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] +#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] class Security implements MiddlewareAnnotationInterface { /** @var string */ diff --git a/tests/Fixtures/Integration/Models/Contact.php b/tests/Fixtures/Integration/Models/Contact.php index fdb73392e0..0566858d97 100644 --- a/tests/Fixtures/Integration/Models/Contact.php +++ b/tests/Fixtures/Integration/Models/Contact.php @@ -6,7 +6,9 @@ use DateTimeImmutable; use TheCodingMachine\GraphQLite\Annotations\FailWith; +use TheCodingMachine\GraphQLite\Annotations\HideIfUnauthorized; use TheCodingMachine\GraphQLite\Annotations\MagicField; +use TheCodingMachine\GraphQLite\Annotations\Security; use function array_search; use DateTimeInterface; use Psr\Http\Message\UploadedFileInterface; @@ -80,6 +82,31 @@ class Contact * @var string */ public $failWithNull = 'This should fail with NULL!'; + /** + * @Field() + * @Right("NO_ACCESS") + * @HideIfUnauthorized() + * @var string + */ + public $hidden = 'you can see the property only if you have access'; + /** + * @Field() + * @Logged() + * @var string + */ + public $forLogged = 'you can see this only if you are logged'; + /** + * @Field() + * @Right("NO_ACCESS") + * @var string + */ + public $withRight = 'you can see this only if you have sufficient right'; + /** + * @Field() + * @Security("is_granted('NO_ACCESS')") + * @var string + */ + public $secured = 'you can see this only if access granted'; public function __construct(string $name) { diff --git a/tests/Integration/EndToEndTest.php b/tests/Integration/EndToEndTest.php index 5da69f6fae..f60cd2e7f6 100644 --- a/tests/Integration/EndToEndTest.php +++ b/tests/Integration/EndToEndTest.php @@ -945,6 +945,22 @@ public function testEndToEndRightIssues(): void $this->assertSame('You need to be logged to access this field', $result->toArray(Debug::RETHROW_UNSAFE_EXCEPTIONS)['errors'][0]['message']); + $queryString = ' + query { + contacts { + name + forLogged + } + } + '; + + $result = GraphQL::executeQuery( + $schema, + $queryString + ); + + $this->assertSame('You need to be logged to access this field', $result->toArray(Debug::RETHROW_UNSAFE_EXCEPTIONS)['errors'][0]['message']); + $queryString = ' query { contacts { @@ -959,6 +975,37 @@ public function testEndToEndRightIssues(): void ); $this->assertSame('You do not have sufficient rights to access this field', $result->toArray(Debug::RETHROW_UNSAFE_EXCEPTIONS)['errors'][0]['message']); + + $queryString = ' + query { + contacts { + withRight + } + } + '; + + $result = GraphQL::executeQuery( + $schema, + $queryString + ); + + $this->assertSame('You do not have sufficient rights to access this field', $result->toArray(Debug::RETHROW_UNSAFE_EXCEPTIONS)['errors'][0]['message']); + + $queryString = ' + query { + contacts { + name + hidden + } + } + '; + + $result = GraphQL::executeQuery( + $schema, + $queryString + ); + + $this->assertSame('Cannot query field "hidden" on type "ContactInterface".', $result->toArray(Debug::RETHROW_UNSAFE_EXCEPTIONS)['errors'][0]['message']); } public function testAutowireService(): void @@ -1424,6 +1471,21 @@ public function testEndToEndSecurityInField(): void ); $this->assertSame('Access denied.', $result->toArray(Debug::RETHROW_UNSAFE_EXCEPTIONS)['errors'][0]['message']); + + $queryString = ' + query { + contacts { + secured + } + } + '; + + $result = GraphQL::executeQuery( + $schema, + $queryString + ); + + $this->assertSame('Access denied.', $result->toArray(Debug::RETHROW_UNSAFE_EXCEPTIONS)['errors'][0]['message']); } public function testEndToEndUnions(){ From 5a7b9790dfd54191cf8cb6c654248b853f3ba572 Mon Sep 17 00:00:00 2001 From: Andrew Maslov Date: Sat, 23 Jan 2021 19:47:30 +0200 Subject: [PATCH 19/41] Added test for required param in getter --- src/Utils/AccessPropertyException.php | 2 +- src/Utils/PropertyAccessor.php | 20 +++++++-- tests/Fixtures/Integration/Models/Contact.php | 13 ++++++ tests/Integration/EndToEndTest.php | 42 +++++++++++++------ 4 files changed, 59 insertions(+), 18 deletions(-) diff --git a/src/Utils/AccessPropertyException.php b/src/Utils/AccessPropertyException.php index 089f916288..079d05154a 100644 --- a/src/Utils/AccessPropertyException.php +++ b/src/Utils/AccessPropertyException.php @@ -10,7 +10,7 @@ public static function createForUnreadableProperty(string $class, string $proper { $name = ucfirst($propertyName); - return new self("Could not get value from property '$class::$propertyName'. Either make the property public or add a public getter for it like this: 'get$name' or 'is$name'"); + return new self("Could not get value from property '$class::$propertyName'. Either make the property public or add a public getter for it like 'get$name' or 'is$name' with no required parameters"); } public static function createForUnwritableProperty(string $class, string $propertyName): self diff --git a/src/Utils/PropertyAccessor.php b/src/Utils/PropertyAccessor.php index c631922cfb..a085473d40 100644 --- a/src/Utils/PropertyAccessor.php +++ b/src/Utils/PropertyAccessor.php @@ -24,7 +24,7 @@ public static function findGetter(string $class, string $propertyName): ?string foreach (['get', 'is'] as $prefix) { $methodName = $prefix . $name; - if (self::publicMethodExists($class, $methodName)) { + if (self::isPublicMethod($class, $methodName)) { return $methodName; } } @@ -40,7 +40,7 @@ public static function findSetter(string $class, string $propertyName): ?string $name = ucfirst($propertyName); $methodName = 'set' . $name; - if (self::publicMethodExists($class, $methodName)) { + if (self::isPublicMethod($class, $methodName)) { return $methodName; } @@ -57,7 +57,7 @@ public static function getValue(object $object, string $propertyName, ...$args) $class = get_class($object); $method = self::findGetter($class, $propertyName); - if ($method) { + if ($method && self::isValidGetter($class, $method)) { return $object->$method(...$args); } @@ -99,7 +99,7 @@ private static function publicPropertyExists(string $class, string $propertyName return $reflection->isPublic(); } - private static function publicMethodExists(string $class, string $methodName): bool + private static function isPublicMethod(string $class, string $methodName): bool { if (!method_exists($class, $methodName)) { return false; @@ -109,4 +109,16 @@ private static function publicMethodExists(string $class, string $methodName): b return $reflection->isPublic(); } + + private static function isValidGetter(string $class, string $methodName): bool + { + $reflection = new ReflectionMethod($class, $methodName); + foreach ($reflection->getParameters() as $parameter) { + if (!$parameter->isDefaultValueAvailable()) { + return false; + } + } + + return true; + } } diff --git a/tests/Fixtures/Integration/Models/Contact.php b/tests/Fixtures/Integration/Models/Contact.php index 0566858d97..d6877f584c 100644 --- a/tests/Fixtures/Integration/Models/Contact.php +++ b/tests/Fixtures/Integration/Models/Contact.php @@ -75,6 +75,11 @@ class Contact * @var bool */ private $private = true; + /** + * @Field() + * @var string + */ + private $zipcode = '5555'; /** * @Field() * @Right("NO_ACCESS") @@ -259,6 +264,14 @@ public function getStatus(): string return 'bar'; } + /** + * @return string + */ + public function getZipcode(string $foo): string + { + return $this->zipcode; + } + /** * @return string */ diff --git a/tests/Integration/EndToEndTest.php b/tests/Integration/EndToEndTest.php index f60cd2e7f6..ae964fe1d9 100644 --- a/tests/Integration/EndToEndTest.php +++ b/tests/Integration/EndToEndTest.php @@ -955,8 +955,8 @@ public function testEndToEndRightIssues(): void '; $result = GraphQL::executeQuery( - $schema, - $queryString + $schema, + $queryString ); $this->assertSame('You need to be logged to access this field', $result->toArray(Debug::RETHROW_UNSAFE_EXCEPTIONS)['errors'][0]['message']); @@ -985,8 +985,8 @@ public function testEndToEndRightIssues(): void '; $result = GraphQL::executeQuery( - $schema, - $queryString + $schema, + $queryString ); $this->assertSame('You do not have sufficient rights to access this field', $result->toArray(Debug::RETHROW_UNSAFE_EXCEPTIONS)['errors'][0]['message']); @@ -1001,8 +1001,8 @@ public function testEndToEndRightIssues(): void '; $result = GraphQL::executeQuery( - $schema, - $queryString + $schema, + $queryString ); $this->assertSame('Cannot query field "hidden" on type "ContactInterface".', $result->toArray(Debug::RETHROW_UNSAFE_EXCEPTIONS)['errors'][0]['message']); @@ -1316,8 +1316,8 @@ public function testEndToEndSecurityFailWithAnnotation(): void '; $result = GraphQL::executeQuery( - $schema, - $queryString + $schema, + $queryString ); $data = $this->getSuccessResult($result); @@ -1481,8 +1481,8 @@ public function testEndToEndSecurityInField(): void '; $result = GraphQL::executeQuery( - $schema, - $queryString + $schema, + $queryString ); $this->assertSame('Access denied.', $result->toArray(Debug::RETHROW_UNSAFE_EXCEPTIONS)['errors'][0]['message']); @@ -1703,13 +1703,29 @@ public function testEndToEndFieldAnnotationInProperty(): void } '; + GraphQL::executeQuery( + $schema, + $queryString + ); + + $this->expectException(AccessPropertyException::class); + $this->expectExceptionMessage("Could not get value from property 'TheCodingMachine\GraphQLite\Fixtures\Integration\Models\Contact::private'. Either make the property public or add a public getter for it like 'getPrivate' or 'isPrivate' with no required parameters"); + + $queryString = ' + query { + contacts { + zipcode + } + } + '; + $result = GraphQL::executeQuery( - $schema, - $queryString + $schema, + $queryString ); $this->expectException(AccessPropertyException::class); - $this->expectExceptionMessage("Could not get value from property 'TheCodingMachine\GraphQLite\Fixtures\Integration\Models\Contact::private'. Either make the property public or add a public getter for it like this: 'getPrivate' or 'isPrivate'"); + $this->expectExceptionMessage("Could not get value from property 'TheCodingMachine\GraphQLite\Fixtures\Integration\Models\Contact::zipcode'. Either make the property public or add a public getter for it like 'getZipcode' or 'isZipcode' with no required parameters"); $result->toArray(Debug::RETHROW_INTERNAL_EXCEPTIONS); } } From 540a36c2c2856f128784c6dc5f67ed2865cdb875 Mon Sep 17 00:00:00 2001 From: Andrew Maslov Date: Sun, 24 Jan 2021 20:16:47 +0200 Subject: [PATCH 20/41] Added unit end to end tests for input annotations usage --- src/Annotations/Field.php | 2 +- src/Utils/PropertyAccessor.php | 7 +- .../Controllers/PostController.php | 39 ++++++ tests/Fixtures/Integration/Models/Post.php | 101 +++++++++++++++ tests/Integration/EndToEndTest.php | 122 ++++++++++++++++++ 5 files changed, 267 insertions(+), 4 deletions(-) create mode 100644 tests/Fixtures/Integration/Controllers/PostController.php create mode 100644 tests/Fixtures/Integration/Models/Post.php diff --git a/src/Annotations/Field.php b/src/Annotations/Field.php index 41ac80d0a9..6b8294cc54 100644 --- a/src/Annotations/Field.php +++ b/src/Annotations/Field.php @@ -50,7 +50,7 @@ public function __construct(array $attributes = [], ?string $name = null, ?strin $forValue = $for ?? $attributes['for'] ?? null; if ($forValue) { - $this->for = (array) $for; + $this->for = (array) $forValue; } } diff --git a/src/Utils/PropertyAccessor.php b/src/Utils/PropertyAccessor.php index a085473d40..b69861a90c 100644 --- a/src/Utils/PropertyAccessor.php +++ b/src/Utils/PropertyAccessor.php @@ -61,7 +61,7 @@ public static function getValue(object $object, string $propertyName, ...$args) return $object->$method(...$args); } - if (self::publicPropertyExists($class, $propertyName)) { + if (self::isPublicProperty($class, $propertyName)) { return $object->$propertyName; } @@ -81,14 +81,15 @@ public static function setValue(object $instance, string $propertyName, $value): return; } - if (self::publicPropertyExists($class, $propertyName)) { + if (self::isPublicProperty($class, $propertyName)) { $instance->$propertyName = $value; + return; } throw AccessPropertyException::createForUnwritableProperty($class, $propertyName); } - private static function publicPropertyExists(string $class, string $propertyName): bool + private static function isPublicProperty(string $class, string $propertyName): bool { if (!property_exists($class, $propertyName)) { return false; diff --git a/tests/Fixtures/Integration/Controllers/PostController.php b/tests/Fixtures/Integration/Controllers/PostController.php new file mode 100644 index 0000000000..1390069187 --- /dev/null +++ b/tests/Fixtures/Integration/Controllers/PostController.php @@ -0,0 +1,39 @@ +id = $id; + + return $post; + } +} diff --git a/tests/Fixtures/Integration/Models/Post.php b/tests/Fixtures/Integration/Models/Post.php new file mode 100644 index 0000000000..69af7522af --- /dev/null +++ b/tests/Fixtures/Integration/Models/Post.php @@ -0,0 +1,101 @@ +title = $title; + $this->description = 'bar'; + } + + /** + * @return string|null + */ + public function getDescription(): ?string + { + return $this->description; + } + + /** + * @param string|null $description + */ + public function setDescription(?string $description): void + { + $this->description = $description; + } + + /** + * @param string|null $summary + */ + public function setSummary(?string $summary): void + { + $this->summary = $summary; + } + + /** + * @param string $inaccessible + */ + private function setInaccessible(string $inaccessible): void + { + $this->inaccessible = $inaccessible; + } +} diff --git a/tests/Integration/EndToEndTest.php b/tests/Integration/EndToEndTest.php index ae964fe1d9..2ebec7f05e 100644 --- a/tests/Integration/EndToEndTest.php +++ b/tests/Integration/EndToEndTest.php @@ -1728,4 +1728,126 @@ public function testEndToEndFieldAnnotationInProperty(): void $this->expectExceptionMessage("Could not get value from property 'TheCodingMachine\GraphQLite\Fixtures\Integration\Models\Contact::zipcode'. Either make the property public or add a public getter for it like 'getZipcode' or 'isZipcode' with no required parameters"); $result->toArray(Debug::RETHROW_INTERNAL_EXCEPTIONS); } + + public function testEndToEndInputAnnotations(): void + { + /** + * @var Schema $schema + */ + $schema = $this->mainContainer->get(Schema::class); + $queryString = ' + mutation { + createPost( + post: { + title: "foo", + publishedAt: "2021-01-24T00:00:00+00:00" + author: { + name: "foo", + birthDate: "1942-12-24T00:00:00+00:00", + relations: [ + { + name: "bar" + } + ] + } + } + ) { + id + title + publishedAt + description + summary + author { + name + } + } + updatePost( + id: 100, + post: { + title: "bar" + } + ) { + id + title + description + summary + } + } + '; + + $result = GraphQL::executeQuery( + $schema, + $queryString + ); + + $this->assertSame([ + 'createPost' => [ + 'id' => 1, + 'title' => 'foo', + 'publishedAt' => '2021-01-24T00:00:00+00:00', + 'description' => 'foo', + 'summary' => 'foo', + 'author' => [ + 'name' => 'foo', + ], + ], + 'updatePost' => [ + 'id' => 100, + 'title' => 'bar', + 'description' => 'bar', + 'summary' => 'foo', + ], + ], $this->getSuccessResult($result)); + } + + public function testEndToEndInputAnnotationIssues(): void + { + /** + * @var Schema $schema + */ + $schema = $this->mainContainer->get(Schema::class); + $queryString = ' + mutation { + createPost( + post: { + id: 20, + } + ) { + id + } + } + '; + + $result = GraphQL::executeQuery( + $schema, + $queryString + ); + + $this->assertSame('Field PostInput.title of required type String! was not provided.', $result->toArray(Debug::RETHROW_UNSAFE_EXCEPTIONS)['errors'][0]['message']); + $this->assertSame('Field PostInput.publishedAt of required type DateTime! was not provided.', $result->toArray(Debug::RETHROW_UNSAFE_EXCEPTIONS)['errors'][1]['message']); + $this->assertSame('Field "id" is not defined by type PostInput.', $result->toArray(Debug::RETHROW_UNSAFE_EXCEPTIONS)['errors'][2]['message']); + + $queryString = ' + mutation { + updatePost( + id: 100, + post: { + title: "foo", + inaccessible: "foo" + } + ) { + id + } + } + '; + + $result = GraphQL::executeQuery( + $schema, + $queryString + ); + + $this->expectException(AccessPropertyException::class); + $this->expectExceptionMessage("Could not set value for property 'TheCodingMachine\GraphQLite\Fixtures\Integration\Models\Post::inaccessible'. Either make the property public or add a public setter for it like this: 'setInaccessible'"); + $result->toArray(Debug::RETHROW_INTERNAL_EXCEPTIONS); + } } From 62d3f4a744c8c2bb9b5c4af368abd10d295dd817 Mon Sep 17 00:00:00 2001 From: Andrew Maslov Date: Sun, 24 Jan 2021 23:29:13 +0200 Subject: [PATCH 21/41] Removed redundant "class" property from Input annotation --- src/Annotations/Input.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Annotations/Input.php b/src/Annotations/Input.php index 08388ea8d6..fed3d465bb 100644 --- a/src/Annotations/Input.php +++ b/src/Annotations/Input.php @@ -40,11 +40,9 @@ class Input /** * @param mixed[] $attributes - * @param class-string|null $class */ - public function __construct(array $attributes = [], ?string $class = null, ?string $name = null, ?bool $default = null, ?string $description = null, ?bool $update = null) + public function __construct(array $attributes = [], ?string $name = null, ?bool $default = null, ?string $description = null, ?bool $update = null) { - $this->class = $class ?? $attributes['class'] ?? null; $this->name = $name ?? $attributes['name'] ?? null; $this->default = $default ?? $attributes['default'] ?? $this->name === null; $this->description = $description ?? $attributes['description'] ?? null; From 5ac1d4df4eb0c4c8f1b2a2be30f5d17cf6e84daf Mon Sep 17 00:00:00 2001 From: Andrew Maslov Date: Mon, 25 Jan 2021 00:23:48 +0200 Subject: [PATCH 22/41] Added "duplicated" exception for inputs and unit test for it --- src/Mappers/AbstractTypeMapper.php | 8 ++++++++ src/Mappers/DuplicateMappingException.php | 13 ++++++++++-- src/Mappers/GlobTypeMapperCache.php | 2 +- tests/Fixtures/DuplicateInputs/Test.php | 12 +++++++++++ .../DuplicateInputs/TestDuplicate.php | 13 ++++++++++++ tests/Fixtures/TestInput.php | 20 +++++++++++++++++++ tests/Mappers/GlobTypeMapperTest.php | 17 ++++++++++++++++ 7 files changed, 82 insertions(+), 3 deletions(-) create mode 100644 tests/Fixtures/DuplicateInputs/Test.php create mode 100644 tests/Fixtures/DuplicateInputs/TestDuplicate.php create mode 100644 tests/Fixtures/TestInput.php diff --git a/src/Mappers/AbstractTypeMapper.php b/src/Mappers/AbstractTypeMapper.php index 864f8ef7c0..e95d306e2f 100644 --- a/src/Mappers/AbstractTypeMapper.php +++ b/src/Mappers/AbstractTypeMapper.php @@ -78,6 +78,8 @@ abstract class AbstractTypeMapper implements TypeMapperInterface private $globTypeMapperCache; /** @var GlobExtendTypeMapperCache */ private $globExtendTypeMapperCache; + /** @var array> */ + private array $registeredInputs; public function __construct(string $cachePrefix, TypeGenerator $typeGenerator, InputTypeGenerator $inputTypeGenerator, InputTypeUtils $inputTypeUtils, ContainerInterface $container, AnnotationReader $annotationReader, NamingStrategyInterface $namingStrategy, RecursiveTypeMapperInterface $recursiveTypeMapper, CacheInterface $cache, ?int $globTTL = 2, ?int $mapTTL = null) { @@ -135,6 +137,7 @@ private function buildMap(): GlobTypeMapperCache /** @var array,ReflectionClass> $classes */ $classes = $this->getClassList(); + foreach ($classes as $className => $refClass) { $annotationsCache = $this->mapClassToAnnotationsCache->get($refClass, function () use ($refClass, $className) { $annotationsCache = new GlobAnnotationsCache(); @@ -151,6 +154,11 @@ private function buildMap(): GlobTypeMapperCache $inputs = $this->annotationReader->getInputAnnotations($refClass); foreach ($inputs as $input) { $inputName = $this->namingStrategy->getInputTypeName($className, $input); + if (isset($this->registeredInputs[$inputName])) { + throw DuplicateMappingException::createForTwoInputs($inputName, $this->registeredInputs[$inputName], $refClass->getName()); + } + + $this->registeredInputs[$inputName] = $refClass->getName(); $annotationsCache->registerInput($inputName, $className, $input); $containsAnnotations = true; } diff --git a/src/Mappers/DuplicateMappingException.php b/src/Mappers/DuplicateMappingException.php index 00140c27de..989b5945ce 100644 --- a/src/Mappers/DuplicateMappingException.php +++ b/src/Mappers/DuplicateMappingException.php @@ -71,8 +71,17 @@ public static function createForQueryInOneProperty(string $queryName, Reflection /** * @return static */ - public static function createForInput(string $sourceClass): self + public static function createForDefaultInput(string $sourceClass): self { - throw new self(sprintf("The class '%s' should be mapped to only one GraphQL Input type. Two default inputs are declared as default via @Input annotation.", $sourceClass)); + throw new self(sprintf("The class '%s' should be mapped to only one GraphQL Input type as default. Two default inputs are declared as default via @Input annotation.", $sourceClass)); + } + + public static function createForTwoInputs(string $typeName, string $firstClass, string $secondClass): self + { + if ($firstClass === $secondClass) { + throw new self(sprintf("The input type '%s' is created 2 times in '%s'", $typeName, $firstClass)); + } + + throw new self(sprintf("The input type '%s' is created by 2 different classes: '%s' and '%s'", $typeName, $firstClass, $secondClass)); } } diff --git a/src/Mappers/GlobTypeMapperCache.php b/src/Mappers/GlobTypeMapperCache.php index 38e63231b0..4a0e0ba65e 100644 --- a/src/Mappers/GlobTypeMapperCache.php +++ b/src/Mappers/GlobTypeMapperCache.php @@ -72,7 +72,7 @@ public function registerAnnotations(ReflectionClass $refClass, GlobAnnotationsCa foreach ($globAnnotationsCache->getInputs() as $inputName => [$inputClassName, $isDefault, $description, $isUpdate]) { if ($isDefault) { if (isset($this->mapClassToInput[$inputClassName])) { - throw DuplicateMappingException::createForInput($refClass->getName()); + throw DuplicateMappingException::createForDefaultInput($refClass->getName()); } $this->mapClassToInput[$inputClassName] = [$inputName, $description, $isUpdate]; diff --git a/tests/Fixtures/DuplicateInputs/Test.php b/tests/Fixtures/DuplicateInputs/Test.php new file mode 100644 index 0000000000..f4a906c1c2 --- /dev/null +++ b/tests/Fixtures/DuplicateInputs/Test.php @@ -0,0 +1,12 @@ +canMapClassToType(TestType::class); } + public function testGlobTypeMapperDuplicateInputsException(): void + { + $container = new Picotainer([ + TestInput::class => function () { + return new TestInput(); + } + ]); + + $typeGenerator = $this->getTypeGenerator(); + + $mapper = new GlobTypeMapper($this->getNamespaceFactory()->createNamespace('TheCodingMachine\GraphQLite\Fixtures\DuplicateInputs'), $typeGenerator, $this->getInputTypeGenerator(), $this->getInputTypeUtils(), $container, new \TheCodingMachine\GraphQLite\AnnotationReader(new AnnotationReader()), new NamingStrategy(), $this->getTypeMapper(), new Psr16Cache(new NullAdapter())); + + $this->expectException(DuplicateMappingException::class); + $mapper->canMapClassToInputType(TestInput::class); + } + public function testGlobTypeMapperDuplicateInputTypesException(): void { $container = new Picotainer([ From b83ac16a541e1bafd425e8c66757e952f41e1041 Mon Sep 17 00:00:00 2001 From: Andrew Maslov Date: Thu, 28 Jan 2021 00:16:15 +0200 Subject: [PATCH 23/41] Fixed inherited Fields via private properties and covered input type inheritance in unit tests --- src/AnnotationReader.php | 7 ++- src/FieldsBuilder.php | 6 ++ .../Controllers/ArticleController.php | 21 +++++++ tests/Fixtures/Integration/Models/Article.php | 27 +++++++++ tests/Integration/EndToEndTest.php | 58 +++++++++++++++++++ 5 files changed, 116 insertions(+), 3 deletions(-) create mode 100644 tests/Fixtures/Integration/Controllers/ArticleController.php create mode 100644 tests/Fixtures/Integration/Models/Article.php diff --git a/src/AnnotationReader.php b/src/AnnotationReader.php index 2acb6ddcb4..be02aa6691 100644 --- a/src/AnnotationReader.php +++ b/src/AnnotationReader.php @@ -125,7 +125,7 @@ public function getInputAnnotations(ReflectionClass $refClass): array { try { /** @var Input[] $inputs */ - $inputs = $this->getClassAnnotations($refClass, Input::class); + $inputs = $this->getClassAnnotations($refClass, Input::class, false); foreach ($inputs as $input) { $input->setClass($refClass->getName()); } @@ -398,6 +398,7 @@ private function isErrorImportant(string $annotationClass, string $docComment, s * * @param ReflectionClass $refClass * @param class-string $annotationClass + * @param bool $inherited * * @return A[] * @@ -406,7 +407,7 @@ private function isErrorImportant(string $annotationClass, string $docComment, s * @template T of object * @template A of object */ - public function getClassAnnotations(ReflectionClass $refClass, string $annotationClass): array + public function getClassAnnotations(ReflectionClass $refClass, string $annotationClass, bool $inherited = true): array { $toAddAnnotations = []; do { @@ -438,7 +439,7 @@ static function ($attribute) { } } $refClass = $refClass->getParentClass(); - } while ($refClass); + } while ($inherited && $refClass); if (! empty($toAddAnnotations)) { return array_merge(...$toAddAnnotations); diff --git a/src/FieldsBuilder.php b/src/FieldsBuilder.php index 63a594574d..b36b102668 100644 --- a/src/FieldsBuilder.php +++ b/src/FieldsBuilder.php @@ -186,6 +186,12 @@ public function getInputFields(string $className, string $inputName, bool $isUpd } } + // Make sure @Field annotations applied to parent's private properties are taken into account as well. + if ($parent = $refClass->getParentClass()) { + $parentFields = $this->getInputFields($parent->getName(), $inputName, $isUpdate); + $fields = array_merge($fields, array_diff_key($parentFields, $fields)); + } + return $fields; } diff --git a/tests/Fixtures/Integration/Controllers/ArticleController.php b/tests/Fixtures/Integration/Controllers/ArticleController.php new file mode 100644 index 0000000000..f682ca585b --- /dev/null +++ b/tests/Fixtures/Integration/Controllers/ArticleController.php @@ -0,0 +1,21 @@ + 'bar', 'summary' => 'foo', ], + 'createArticle' => [ + 'id' => 2, + 'title' => 'foo', + 'description' => 'some description', + 'summary' => 'foo', + 'magazine' => 'bar', + 'author' => [ + 'name' => 'foo', + ], + ], ], $this->getSuccessResult($result)); } @@ -1827,6 +1862,29 @@ public function testEndToEndInputAnnotationIssues(): void $this->assertSame('Field PostInput.publishedAt of required type DateTime! was not provided.', $result->toArray(Debug::RETHROW_UNSAFE_EXCEPTIONS)['errors'][1]['message']); $this->assertSame('Field "id" is not defined by type PostInput.', $result->toArray(Debug::RETHROW_UNSAFE_EXCEPTIONS)['errors'][2]['message']); + $queryString = ' + mutation { + createArticle( + article: { + id: 20, + publishedAt: "2021-01-24T00:00:00+00:00" + } + ) { + id + publishedAt + } + } + '; + + $result = GraphQL::executeQuery( + $schema, + $queryString + ); + + $this->assertSame('Field ArticleInput.title of required type String! was not provided.', $result->toArray(Debug::RETHROW_UNSAFE_EXCEPTIONS)['errors'][0]['message']); + $this->assertSame('Field "id" is not defined by type ArticleInput.', $result->toArray(Debug::RETHROW_UNSAFE_EXCEPTIONS)['errors'][1]['message']); + $this->assertSame('Field "publishedAt" is not defined by type ArticleInput.', $result->toArray(Debug::RETHROW_UNSAFE_EXCEPTIONS)['errors'][2]['message']); + $queryString = ' mutation { updatePost( From 68604fa90d5bbd55f0ed7a9e4eb8df696aa41eb5 Mon Sep 17 00:00:00 2001 From: Andrew Maslov Date: Sat, 30 Jan 2021 15:08:49 +0200 Subject: [PATCH 24/41] Added verification for non instantiable input classes --- src/FailedResolvingInputType.php | 5 +++++ src/Parameters/InputTypeProperty.php | 4 ++-- src/Types/InputType.php | 5 +++++ .../NonInstantiableInput/AbstractFoo.php | 18 ++++++++++++++++++ tests/Mappers/GlobTypeMapperTest.php | 17 +++++++++++++++++ 5 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 tests/Fixtures/NonInstantiableInput/AbstractFoo.php diff --git a/src/FailedResolvingInputType.php b/src/FailedResolvingInputType.php index e914bb545e..a3d1b1c221 100644 --- a/src/FailedResolvingInputType.php +++ b/src/FailedResolvingInputType.php @@ -18,4 +18,9 @@ public static function createForDecorator(): self { return new self('Input type cannot be a decorator'); } + + public static function createForNotInstantiableClass(string $class): self + { + return new self("Class '$class' annotated with @Input must be instantiable."); + } } diff --git a/src/Parameters/InputTypeProperty.php b/src/Parameters/InputTypeProperty.php index f458da65f5..7e1750f7d1 100644 --- a/src/Parameters/InputTypeProperty.php +++ b/src/Parameters/InputTypeProperty.php @@ -14,8 +14,8 @@ class InputTypeProperty extends InputTypeParameter private $propertyName; /** - * @param InputType&Type $type - * @param mixed $defaultValue + * @param InputType&Type $type + * @param mixed $defaultValue */ public function __construct(string $propertyName, string $fieldName, InputType $type, bool $hasDefaultValue, $defaultValue, ArgumentResolver $argumentResolver) { diff --git a/src/Types/InputType.php b/src/Types/InputType.php index a7557ed5f4..30e58c8e6c 100644 --- a/src/Types/InputType.php +++ b/src/Types/InputType.php @@ -28,6 +28,11 @@ class InputType extends MutableInputObjectType implements ResolvableMutableInput */ public function __construct(string $className, string $inputName, ?string $description, bool $isUpdate, FieldsBuilder $fieldsBuilder) { + $reflection = new ReflectionClass($className); + if (!$reflection->isInstantiable()) { + throw FailedResolvingInputType::createForNotInstantiableClass($className); + } + $this->fields = $fieldsBuilder->getInputFields($className, $inputName, $isUpdate); $fields = function () use ($isUpdate) { diff --git a/tests/Fixtures/NonInstantiableInput/AbstractFoo.php b/tests/Fixtures/NonInstantiableInput/AbstractFoo.php new file mode 100644 index 0000000000..419e6dc90d --- /dev/null +++ b/tests/Fixtures/NonInstantiableInput/AbstractFoo.php @@ -0,0 +1,18 @@ +expectExceptionMessage('Class "TheCodingMachine\GraphQLite\Fixtures\NonInstantiableType\AbstractFooType" annotated with @Type(class="TheCodingMachine\GraphQLite\Fixtures\TestObject") must be instantiable.'); $mapper->mapClassToType(TestObject::class, null); } + + public function testNonInstantiableInput(): void + { + $container = new Picotainer([]); + + $typeGenerator = $this->getTypeGenerator(); + $inputTypeGenerator = $this->getInputTypeGenerator(); + + $cache = new Psr16Cache(new ArrayAdapter()); + $mapper = new GlobTypeMapper($this->getNamespaceFactory()->createNamespace('TheCodingMachine\GraphQLite\Fixtures\NonInstantiableInput'), $typeGenerator, $inputTypeGenerator, $this->getInputTypeUtils(), $container, new \TheCodingMachine\GraphQLite\AnnotationReader(new AnnotationReader()), new NamingStrategy(), $this->getTypeMapper(), $cache); + + $this->expectException(FailedResolvingInputType::class); + $this->expectExceptionMessage("Class 'TheCodingMachine\GraphQLite\Fixtures\NonInstantiableInput\AbstractFoo' annotated with @Input must be instantiable."); + $mapper->mapClassToInputType(AbstractFoo::class); + } } From 42e449eaa54a0eca4adca77f86a9da19b7901ebf Mon Sep 17 00:00:00 2001 From: Andrew Maslov Date: Sat, 30 Jan 2021 15:20:29 +0200 Subject: [PATCH 25/41] Added unit test for QueryFieldDescriptor for target property on source --- tests/QueryFieldDescriptorTest.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/QueryFieldDescriptorTest.php b/tests/QueryFieldDescriptorTest.php index 880b999bcf..796487406a 100644 --- a/tests/QueryFieldDescriptorTest.php +++ b/tests/QueryFieldDescriptorTest.php @@ -26,6 +26,16 @@ public function testExceptionInSetTargetMethodOnSource(): void $descriptor->setTargetMethodOnSource('test'); } + public function testExceptionInSetTargetPropertyOnSource(): void + { + $descriptor = new QueryFieldDescriptor(); + $descriptor->setTargetPropertyOnSource('test'); + $descriptor->getResolver(); + + $this->expectException(GraphQLRuntimeException::class); + $descriptor->setTargetPropertyOnSource('test'); + } + public function testExceptionInSetMagicProperty(): void { $descriptor = new QueryFieldDescriptor(); From dc4605cfc40910cf2b8978eb6be8fe847d486602 Mon Sep 17 00:00:00 2001 From: Andrew Maslov Date: Sun, 31 Jan 2021 13:53:25 +0200 Subject: [PATCH 26/41] Added unit tests for InputType --- src/FailedResolvingInputType.php | 4 +- src/Types/InputType.php | 2 +- tests/Fixtures/Inputs/FooBar.php | 45 +++++++++ tests/Fixtures/Inputs/InputInterface.php | 12 +++ tests/Fixtures/Integration/Models/Post.php | 6 +- tests/Integration/EndToEndTest.php | 14 +-- tests/Types/InputTypeTest.php | 101 +++++++++++++++++++++ 7 files changed, 171 insertions(+), 13 deletions(-) create mode 100644 tests/Fixtures/Inputs/FooBar.php create mode 100644 tests/Fixtures/Inputs/InputInterface.php create mode 100644 tests/Types/InputTypeTest.php diff --git a/src/FailedResolvingInputType.php b/src/FailedResolvingInputType.php index a3d1b1c221..e09db75f2a 100644 --- a/src/FailedResolvingInputType.php +++ b/src/FailedResolvingInputType.php @@ -14,9 +14,9 @@ public static function createForMissingConstructorParameter(string $class, strin return new self(sprintf("Parameter '%s' is missing for class '%s' constructor. It should be mapped as required field.", $parameter, $class)); } - public static function createForDecorator(): self + public static function createForDecorator(string $class): self { - return new self('Input type cannot be a decorator'); + return new self("Input type '$class' cannot be a decorator."); } public static function createForNotInstantiableClass(string $class): self diff --git a/src/Types/InputType.php b/src/Types/InputType.php index 30e58c8e6c..f7522b61db 100644 --- a/src/Types/InputType.php +++ b/src/Types/InputType.php @@ -91,7 +91,7 @@ public function resolve(?object $source, array $args, $context, ResolveInfo $res public function decorate(callable $decorator): void { - throw FailedResolvingInputType::createForDecorator(); + throw FailedResolvingInputType::createForDecorator($this->className); } /** diff --git a/tests/Fixtures/Inputs/FooBar.php b/tests/Fixtures/Inputs/FooBar.php new file mode 100644 index 0000000000..43951b1c34 --- /dev/null +++ b/tests/Fixtures/Inputs/FooBar.php @@ -0,0 +1,45 @@ +foo = $foo; + $this->bar = $bar; + } +} diff --git a/tests/Fixtures/Inputs/InputInterface.php b/tests/Fixtures/Inputs/InputInterface.php new file mode 100644 index 0000000000..744299c5f5 --- /dev/null +++ b/tests/Fixtures/Inputs/InputInterface.php @@ -0,0 +1,12 @@ + 1, 'title' => 'foo', 'publishedAt' => '2021-01-24T00:00:00+00:00', - 'description' => 'foo', + 'comment' => 'foo', 'summary' => 'foo', 'author' => [ 'name' => 'foo', @@ -1819,13 +1819,13 @@ public function testEndToEndInputAnnotations(): void 'updatePost' => [ 'id' => 100, 'title' => 'bar', - 'description' => 'bar', + 'comment' => 'bar', 'summary' => 'foo', ], 'createArticle' => [ 'id' => 2, 'title' => 'foo', - 'description' => 'some description', + 'comment' => 'some description', 'summary' => 'foo', 'magazine' => 'bar', 'author' => [ diff --git a/tests/Types/InputTypeTest.php b/tests/Types/InputTypeTest.php new file mode 100644 index 0000000000..ba1cb3cec9 --- /dev/null +++ b/tests/Types/InputTypeTest.php @@ -0,0 +1,101 @@ +getFieldsBuilder()); + $input->freeze(); + + $this->assertEquals('FooBarInput', $input->config['name']); + $this->assertEquals('Test', $input->config['description']); + + $fields = $input->getFields(); + $this->assertCount(2, $fields); + + $this->assertEquals('foo', $fields['foo']->config['name']); + $this->assertEquals('Foo description.', $fields['foo']->config['description']); + $this->assertArrayNotHasKey('defaultValue', $fields['foo']->config); + + $this->assertEquals('bar', $fields['bar']->config['name']); + $this->assertEquals('Bar comment.', $fields['bar']->config['description']); + $this->assertEquals('bar', $fields['bar']->config['defaultValue']); + } + + public function testUpdateInputConfiguredCorrectly(): void + { + $input = new InputType(FooBar::class, 'FooBarUpdateInput', 'Test', true, $this->getFieldsBuilder()); + $input->freeze(); + + $this->assertEquals('FooBarUpdateInput', $input->config['name']); + $this->assertEquals('Test', $input->config['description']); + + $fields = $input->getFields(); + $this->assertCount(3, $fields); + + $this->assertEquals('foo', $fields['foo']->config['name']); + $this->assertEquals('Foo description.', $fields['foo']->config['description']); + $this->assertArrayNotHasKey('defaultValue', $fields['foo']->config); + + $this->assertEquals('bar', $fields['bar']->config['name']); + $this->assertEquals('Bar comment.', $fields['bar']->config['description']); + $this->assertArrayNotHasKey('defaultValue', $fields['foo']->config); + + $this->assertEquals('timestamp', $fields['timestamp']->config['name']); + $this->assertEquals('', $fields['timestamp']->config['description']); + $this->assertArrayNotHasKey('defaultValue', $fields['timestamp']->config); + } + + public function testPassingInterfaceName(): void + { + $this->expectException(FailedResolvingInputType::class); + $this->expectExceptionMessage("Class 'TheCodingMachine\GraphQLite\Fixtures\Inputs\InputInterface' annotated with @Input must be instantiable."); + + new InputType(InputInterface::class, 'TestInput', null, false, $this->getFieldsBuilder()); + } + + public function testInputCannotBeDecorator(): void + { + $this->expectException(FailedResolvingInputType::class); + $this->expectExceptionMessage("Input type 'TheCodingMachine\GraphQLite\Fixtures\Inputs\FooBar' cannot be a decorator."); + + $input = new InputType(FooBar::class, 'FooBarInput', null, false, $this->getFieldsBuilder()); + $input->decorate(function () {}); + } + + public function testResolvesCorrectlyWithRequiredConstructParam(): void + { + $input = new InputType(FooBar::class, 'FooBarInput', null, false, $this->getFieldsBuilder()); + + $args = ['foo' => 'Foo']; + $resolveInfo = $this->createMock(ResolveInfo::class); + $result = $input->resolve(null, $args, [], $resolveInfo); + + $this->assertSame([ + 'foo' => 'Foo', + 'bar' => 'test', + ], (array) $result); + } + + public function testFailsResolvingFieldWithoutRequiredConstructParam(): void + { + $input = new InputType(FooBar::class, 'FooBarInput', null, false, $this->getFieldsBuilder()); + + $args = ['bar' => 'Bar']; + $resolveInfo = $this->createMock(ResolveInfo::class); + + $this->expectException(FailedResolvingInputType::class); + $this->expectExceptionMessage("Parameter 'foo' is missing for class 'TheCodingMachine\GraphQLite\Fixtures\Inputs\FooBar' constructor. It should be mapped as required field."); + + $input->resolve(null, $args, [], $resolveInfo); + } +} From 675cd0277e12ffc3b40e16a81640bce528c81c72 Mon Sep 17 00:00:00 2001 From: Andrew Maslov Date: Sun, 31 Jan 2021 14:47:25 +0200 Subject: [PATCH 27/41] Adjusted tests for lower PHP versions --- src/Mappers/AbstractTypeMapper.php | 2 +- tests/Fixtures/Inputs/FooBar.php | 12 ++++++-- tests/Fixtures/Inputs/TypedFooBar.php | 23 +++++++++++++++ tests/Fixtures/Integration/Models/Article.php | 6 ++-- tests/Fixtures/Integration/Models/Post.php | 10 +++---- .../NonInstantiableInput/AbstractFoo.php | 3 +- tests/Fixtures/TestInput.php | 2 +- tests/Types/InputTypeTest.php | 29 +++++++++++++++++++ 8 files changed, 73 insertions(+), 14 deletions(-) create mode 100644 tests/Fixtures/Inputs/TypedFooBar.php diff --git a/src/Mappers/AbstractTypeMapper.php b/src/Mappers/AbstractTypeMapper.php index e95d306e2f..9aaffae63d 100644 --- a/src/Mappers/AbstractTypeMapper.php +++ b/src/Mappers/AbstractTypeMapper.php @@ -79,7 +79,7 @@ abstract class AbstractTypeMapper implements TypeMapperInterface /** @var GlobExtendTypeMapperCache */ private $globExtendTypeMapperCache; /** @var array> */ - private array $registeredInputs; + private $registeredInputs; public function __construct(string $cachePrefix, TypeGenerator $typeGenerator, InputTypeGenerator $inputTypeGenerator, InputTypeUtils $inputTypeUtils, ContainerInterface $container, AnnotationReader $annotationReader, NamingStrategyInterface $namingStrategy, RecursiveTypeMapperInterface $recursiveTypeMapper, CacheInterface $cache, ?int $globTTL = 2, ?int $mapTTL = null) { diff --git a/tests/Fixtures/Inputs/FooBar.php b/tests/Fixtures/Inputs/FooBar.php index 43951b1c34..03dd67ab8f 100644 --- a/tests/Fixtures/Inputs/FooBar.php +++ b/tests/Fixtures/Inputs/FooBar.php @@ -16,20 +16,26 @@ class FooBar * Foo comment. * * @Field(description="Foo description.") + * + * @var string */ - public string $foo; + public $foo; /** * Bar comment. * * @Field() + * + * @var string|null */ - public ?string $bar = 'bar'; + public $bar = 'bar'; /** * @Field(for="FooBarUpdateInput", name="timestamp") + * + * @var string|null */ - public ?string $date; + public $date; /** * FooBar constructor. diff --git a/tests/Fixtures/Inputs/TypedFooBar.php b/tests/Fixtures/Inputs/TypedFooBar.php new file mode 100644 index 0000000000..770eb24fd9 --- /dev/null +++ b/tests/Fixtures/Inputs/TypedFooBar.php @@ -0,0 +1,23 @@ +assertEquals('foo', $fields['foo']->config['name']); $this->assertEquals('Foo description.', $fields['foo']->config['description']); $this->assertArrayNotHasKey('defaultValue', $fields['foo']->config); + $this->assertInstanceOf(NonNull::class, $fields['foo']->getType()); + $this->assertInstanceOf(StringType::class, $fields['foo']->getType()->getWrappedType()); $this->assertEquals('bar', $fields['bar']->config['name']); $this->assertEquals('Bar comment.', $fields['bar']->config['description']); $this->assertEquals('bar', $fields['bar']->config['defaultValue']); + $this->assertNotInstanceOf(NonNull::class, $fields['bar']->getType()); + } + + /** + * @requires PHP >= 7.4 + */ + public function testInputConfiguredCorrectlyWithTypedProperties(): void + { + $input = new InputType(TypedFooBar::class, 'TypedFooBarInput', 'Test', false, $this->getFieldsBuilder()); + $input->freeze(); + + $fields = $input->getFields(); + $this->assertCount(2, $fields); + + $this->assertEquals('foo', $fields['foo']->config['name']); + $this->assertArrayNotHasKey('defaultValue', $fields['foo']->config); + $this->assertInstanceOf(NonNull::class, $fields['foo']->getType()); + $this->assertInstanceOf(StringType::class, $fields['foo']->getType()->getWrappedType()); + + $this->assertEquals('bar', $fields['bar']->config['name']); + $this->assertEquals(10, $fields['bar']->config['defaultValue']); + $this->assertInstanceOf(IntType::class, $fields['bar']->getType()); } public function testUpdateInputConfiguredCorrectly(): void @@ -83,6 +111,7 @@ public function testResolvesCorrectlyWithRequiredConstructParam(): void $this->assertSame([ 'foo' => 'Foo', 'bar' => 'test', + 'date' => null, ], (array) $result); } From 0f4026e5b0737547561b36755a0e2e30bf5ed473 Mon Sep 17 00:00:00 2001 From: Andrew Maslov Date: Sun, 31 Jan 2021 17:44:27 +0200 Subject: [PATCH 28/41] Skip adding 'Input' suffix for classes which names ends with it already --- src/NamingStrategy.php | 4 ++++ tests/Fixtures/DuplicateInputs/{Test.php => Foo.php} | 4 ++-- .../DuplicateInputs/{TestDuplicate.php => FooInput.php} | 5 ++--- 3 files changed, 8 insertions(+), 5 deletions(-) rename tests/Fixtures/DuplicateInputs/{Test.php => Foo.php} (78%) rename tests/Fixtures/DuplicateInputs/{TestDuplicate.php => FooInput.php} (73%) diff --git a/src/NamingStrategy.php b/src/NamingStrategy.php index a69909061b..e99e18e9d5 100644 --- a/src/NamingStrategy.php +++ b/src/NamingStrategy.php @@ -75,6 +75,10 @@ public function getInputTypeName(string $className, $input): string $className = substr($className, $prevPos + 1); } + if (substr($className, -5) === 'Input') { + return $className; + } + return $className . 'Input'; } diff --git a/tests/Fixtures/DuplicateInputs/Test.php b/tests/Fixtures/DuplicateInputs/Foo.php similarity index 78% rename from tests/Fixtures/DuplicateInputs/Test.php rename to tests/Fixtures/DuplicateInputs/Foo.php index f4a906c1c2..8c5acc9356 100644 --- a/tests/Fixtures/DuplicateInputs/Test.php +++ b/tests/Fixtures/DuplicateInputs/Foo.php @@ -5,8 +5,8 @@ use TheCodingMachine\GraphQLite\Annotations\Input; /** - * @Input(name="FooInput") + * @Input() */ -class Test +class Foo { } diff --git a/tests/Fixtures/DuplicateInputs/TestDuplicate.php b/tests/Fixtures/DuplicateInputs/FooInput.php similarity index 73% rename from tests/Fixtures/DuplicateInputs/TestDuplicate.php rename to tests/Fixtures/DuplicateInputs/FooInput.php index a7fa3b97dd..2427d3be08 100644 --- a/tests/Fixtures/DuplicateInputs/TestDuplicate.php +++ b/tests/Fixtures/DuplicateInputs/FooInput.php @@ -5,9 +5,8 @@ use TheCodingMachine\GraphQLite\Annotations\Input; /** - * @Input(name="FooInput") + * @Input() */ -class TestDuplicate +class FooInput { - } From b31f2f52097f8d6e775c8e238d3c34d8e177ed29 Mon Sep 17 00:00:00 2001 From: Andrew Maslov Date: Sun, 31 Jan 2021 20:39:40 +0200 Subject: [PATCH 29/41] Stop using setter for input properties mentioned in the constructor --- src/Types/InputType.php | 21 ++++++++- tests/Fixtures/Inputs/FooBar.php | 3 -- tests/Fixtures/Inputs/TestOnlyConstruct.php | 52 +++++++++++++++++++++ tests/Types/InputTypeTest.php | 19 ++++++++ 4 files changed, 91 insertions(+), 4 deletions(-) create mode 100644 tests/Fixtures/Inputs/TestOnlyConstruct.php diff --git a/src/Types/InputType.php b/src/Types/InputType.php index f7522b61db..eb27c66f7e 100644 --- a/src/Types/InputType.php +++ b/src/Types/InputType.php @@ -82,7 +82,9 @@ public function resolve(?object $source, array $args, $context, ResolveInfo $res } $instance = $this->createInstance($mappedValues); - foreach ($mappedValues as $property => $value) { + $values = array_diff_key($mappedValues, array_flip($this->getClassConstructParameterNames())); + + foreach ($values as $property => $value) { PropertyAccessor::setValue($instance, $property, $value); } @@ -117,4 +119,21 @@ private function createInstance(array $values): object return $refClass->newInstanceArgs($parameters); } + + private function getClassConstructParameterNames(): array + { + $refClass = new ReflectionClass($this->className); + $constructor = $refClass->getConstructor(); + + if (!$constructor) { + return []; + } + + $names = []; + foreach ($constructor->getParameters() as $parameter) { + $names[] = $parameter->getName(); + } + + return $names; + } } diff --git a/tests/Fixtures/Inputs/FooBar.php b/tests/Fixtures/Inputs/FooBar.php index 03dd67ab8f..ff76408377 100644 --- a/tests/Fixtures/Inputs/FooBar.php +++ b/tests/Fixtures/Inputs/FooBar.php @@ -16,7 +16,6 @@ class FooBar * Foo comment. * * @Field(description="Foo description.") - * * @var string */ public $foo; @@ -25,14 +24,12 @@ class FooBar * Bar comment. * * @Field() - * * @var string|null */ public $bar = 'bar'; /** * @Field(for="FooBarUpdateInput", name="timestamp") - * * @var string|null */ public $date; diff --git a/tests/Fixtures/Inputs/TestOnlyConstruct.php b/tests/Fixtures/Inputs/TestOnlyConstruct.php new file mode 100644 index 0000000000..f78ed8efdf --- /dev/null +++ b/tests/Fixtures/Inputs/TestOnlyConstruct.php @@ -0,0 +1,52 @@ +foo = $foo; + $this->bar = $bar; + } + + public function setFoo(string $foo): void + { + throw new Exception('This should not be called!'); + } + + public function setBar(int $bar): void + { + throw new Exception('This should not be called!'); + } + + public function getFoo(): string + { + return $this->foo; + } + + public function getBar(): int + { + return $this->bar; + } +} diff --git a/tests/Types/InputTypeTest.php b/tests/Types/InputTypeTest.php index 73962d31f1..57174337fe 100644 --- a/tests/Types/InputTypeTest.php +++ b/tests/Types/InputTypeTest.php @@ -10,6 +10,7 @@ use TheCodingMachine\GraphQLite\FailedResolvingInputType; use TheCodingMachine\GraphQLite\Fixtures\Inputs\FooBar; use TheCodingMachine\GraphQLite\Fixtures\Inputs\InputInterface; +use TheCodingMachine\GraphQLite\Fixtures\Inputs\TestOnlyConstruct; use TheCodingMachine\GraphQLite\Fixtures\Inputs\TypedFooBar; class InputTypeTest extends AbstractQueryProviderTest @@ -115,6 +116,24 @@ public function testResolvesCorrectlyWithRequiredConstructParam(): void ], (array) $result); } + public function testResolvesCorrectlyWithOnlyConstruct(): void + { + $input = new InputType(TestOnlyConstruct::class, 'TestOnlyConstructInput', null, false, $this->getFieldsBuilder()); + + $args = [ + 'foo' => 'Foo', + 'bar' => 200, + ]; + + $resolveInfo = $this->createMock(ResolveInfo::class); + + /** @var TestOnlyConstruct $result */ + $result = $input->resolve(null, $args, [], $resolveInfo); + + $this->assertEquals('Foo', $result->getFoo()); + $this->assertEquals(200, $result->getBar()); + } + public function testFailsResolvingFieldWithoutRequiredConstructParam(): void { $input = new InputType(FooBar::class, 'FooBarInput', null, false, $this->getFieldsBuilder()); From 2dcc4309925f953b580899588f0b7ac026c8b421 Mon Sep 17 00:00:00 2001 From: Andrew Maslov Date: Sun, 31 Jan 2021 20:41:18 +0200 Subject: [PATCH 30/41] Fixed return type for getClassConstructParameterNames() --- src/Types/InputType.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Types/InputType.php b/src/Types/InputType.php index eb27c66f7e..34c216a04d 100644 --- a/src/Types/InputType.php +++ b/src/Types/InputType.php @@ -120,6 +120,9 @@ private function createInstance(array $values): object return $refClass->newInstanceArgs($parameters); } + /** + * @return string[] + */ private function getClassConstructParameterNames(): array { $refClass = new ReflectionClass($this->className); From c9765638ab198aa7d7e6e3b035aa264551a52858 Mon Sep 17 00:00:00 2001 From: Andrew Maslov Date: Sun, 31 Jan 2021 21:21:24 +0200 Subject: [PATCH 31/41] Added documentation for annotation references --- docs/annotations_reference.md | 39 ++++++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/docs/annotations_reference.md b/docs/annotations_reference.md index 8a966e6833..e04edaf92a 100644 --- a/docs/annotations_reference.md +++ b/docs/annotations_reference.md @@ -55,16 +55,35 @@ name | see below | string | The targeted GraphQL output type. One and only one of "class" and "name" parameter can be passed at the same time. +## @Input annotation + +The `@Input` annotation is used to declare a GraphQL input type. + +**Applies on**: classes. + +Attribute | Compulsory | Type | Definition +---------------|------------|--------|-------- +name | *no* | string | The name of the GraphQL input type generated. If not passed, the name of the class with suffix "Input" is used. If the class ends with "Input", the "Input" suffix is not added. +description | *no* | string | Description of the input type in the documentation. If not passed, PHP doc comment is used. +default | *no* | bool | Defaults to *true* if name is not specified. Whether the annotated PHP class should be mapped by default to this type. +update | *no* | bool | Determines if the the input represents a partial update. When set to *true* input fields won't have default values thus won't be set on resolve if they are not specified in the query/mutation. + + ## @Field annotation The `@Field` annotation is used to declare a GraphQL field. -**Applies on**: methods of classes annotated with `@Type` or `@ExtendType`. +**Applies on**: methods or properties of classes annotated with `@Type`, `@ExtendType` or `@Input`. +When it's applied on private or protected property, public getter or/and setter method is expected in the class accordingly +whether it's used for output type or input type. For example if property name is `foo` then getter should be `getFoo()` or setter should be `setFoo($foo)`. Setter can be omitted if property related to the field is present in the constructor with the same name. -Attribute | Compulsory | Type | Definition ----------------|------------|------|-------- -name | *no* | string | The name of the field. If skipped, the name of the method is used instead. -[outputType](custom_types.md) | *no* | string | Forces the GraphQL output type of a query. +Attribute | Compulsory | Type | Definition +------------------------------|------------|---------------|-------- +name | *no* | string | The name of the field. If skipped, the name of the method is used instead. +for | *no* | string, array | Forces the field to be used only for specific output or input type(s). By default field is used for all possible declared types. +description | *no* | string | Field description displayed in the GraphQL docs. If it's empty PHP doc comment is used instead. +[outputType](custom_types.md) | *no* | string | Forces the GraphQL output type of a query. +[inputType](input_types.md) | *no* | string | Forces the GraphQL input type of a query. ## @SourceField annotation @@ -100,7 +119,7 @@ annotations | *no* | array | A set of annotations that ap The `@Logged` annotation is used to declare a Query/Mutation/Field is only visible to logged users. -**Applies on**: methods annotated with `@Query`, `@Mutation` or `@Field`. +**Applies on**: methods or properties annotated with `@Query`, `@Mutation` or `@Field`. This annotation allows no attributes. @@ -108,7 +127,7 @@ This annotation allows no attributes. The `@Right` annotation is used to declare a Query/Mutation/Field is only visible to users with a specific right. -**Applies on**: methods annotated with `@Query`, `@Mutation` or `@Field`. +**Applies on**: methods or properties annotated with `@Query`, `@Mutation` or `@Field`. Attribute | Compulsory | Type | Definition ---------------|------------|------|-------- @@ -119,7 +138,7 @@ name | *yes* | string | The name of the right. The `@FailWith` annotation is used to declare a default value to return in the user is not authorized to see a specific query / mutation / field (according to the `@Logged` and `@Right` annotations). -**Applies on**: methods annotated with `@Query`, `@Mutation` or `@Field` and one of `@Logged` or `@Right` annotations. +**Applies on**: methods or properties annotated with `@Query`, `@Mutation` or `@Field` and one of `@Logged` or `@Right` annotations. Attribute | Compulsory | Type | Definition ---------------|------------|------|-------- @@ -130,7 +149,7 @@ value | *yes* | mixed | The value to return if the user is not au The `@HideIfUnauthorized` annotation is used to completely hide the query / mutation / field if the user is not authorized to access it (according to the `@Logged` and `@Right` annotations). -**Applies on**: methods annotated with `@Query`, `@Mutation` or `@Field` and one of `@Logged` or `@Right` annotations. +**Applies on**: methods or properties annotated with `@Query`, `@Mutation` or `@Field` and one of `@Logged` or `@Right` annotations. `@HideIfUnauthorized` and `@FailWith` are mutually exclusive. @@ -152,7 +171,7 @@ It is very flexible: it allows you to pass an expression that can contains custo See [the fine grained security page](fine-grained-security.md) for more details. -**Applies on**: methods annotated with `@Query`, `@Mutation` or `@Field`. +**Applies on**: methods or properties annotated with `@Query`, `@Mutation` or `@Field`. Attribute | Compulsory | Type | Definition ---------------|------------|--------|-------- From fecf4fb49c4fb8aeb4bba221b6b31fcfa4d5252c Mon Sep 17 00:00:00 2001 From: Andrew Maslov Date: Sun, 31 Jan 2021 21:22:12 +0200 Subject: [PATCH 32/41] Partially covered documentation for input annotation --- docs/input_types.md | 90 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 88 insertions(+), 2 deletions(-) diff --git a/docs/input_types.md b/docs/input_types.md index 41fd64703c..4dccee5bad 100644 --- a/docs/input_types.md +++ b/docs/input_types.md @@ -95,7 +95,9 @@ You are running into this error because GraphQLite does not know how to handle t In GraphQL, an object passed in parameter of a query or mutation (or any field) is called an **Input Type**. -In order to declare that type, in GraphQLite, we will declare a **Factory**. +There are two ways for declaring that type, in GraphQLite: using **Factory** or annotating the class with `@Input`. + +## Factory A **Factory** is a method that takes in parameter all the fields of the input type and return an object. @@ -136,7 +138,7 @@ class MyFactory and now, you can run query like this: ``` -mutation { +query { getCities(location: { latitude: 45.0, longitude: 0.0, @@ -387,3 +389,87 @@ public function getProductById(string $id, bool $lazyLoad = true): Product With the `@HideParameter` annotation, you can choose to remove from the GraphQL schema any argument. To be able to hide an argument, the argument must have a default value. + +## @Input Annotation + +Let's transform `Location` class into an input type by adding `@Input` annotation to it and `@Field` annotation to corresponding properties: + + + +```php +#[Input] +class Location +{ + + #[Field] + private float $latitude; + + #[Field] + private float $longitude; + + public function __construct(float $latitude, float $longitude) + { + $this->latitude = $latitude; + $this->longitude = $longitude; + } + + public function getLatitude(): float + { + return $this->latitude; + } + + public function getLongitude(): float + { + return $this->longitude; + } +} +``` + +```php +/** + * @Input + */ +class Location +{ + + /** + * @Field + * @var float + */ + private $latitude; + + /** + * @Field + * @var float + */ + private $longitude; + + public function __construct(float $latitude, float $longitude) + { + $this->latitude = $latitude; + $this->longitude = $longitude; + } + + public function getLatitude(): float + { + return $this->latitude; + } + + public function getLongitude(): float + { + return $this->longitude; + } +} +``` + + +Now if you call `getCities()` query you can pass the location input in the same way as with factories. +The `Location` object will be automatically instantiated with provided `latitude` / `longitude` and passed to the controller as a parameter. + +There are some important things to notice: + +- `@Field` annotation is recognized only on properties for Input Type. +- There are 3 ways for fields to be resolved: + - Via constructor if corresponding properties are mentioned as parameters with the same names - exactly as in the example above. + - If properties are public, they will be just set without any additional effort. + - For private or protected properties implemented public setter is required (if they are not set via constructor). For example `setLatitude(float $latitude)`. From 991d39028dcfa65f8d338634a068318d9d71a131 Mon Sep 17 00:00:00 2001 From: Andrew Maslov Date: Mon, 1 Feb 2021 23:29:25 +0200 Subject: [PATCH 33/41] Added documentation for input type --- docs/input_types.md | 78 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/docs/input_types.md b/docs/input_types.md index 4dccee5bad..77ef9339ed 100644 --- a/docs/input_types.md +++ b/docs/input_types.md @@ -473,3 +473,81 @@ There are some important things to notice: - Via constructor if corresponding properties are mentioned as parameters with the same names - exactly as in the example above. - If properties are public, they will be just set without any additional effort. - For private or protected properties implemented public setter is required (if they are not set via constructor). For example `setLatitude(float $latitude)`. + +### Multiple input types per one class + +Simple usage of `@Input` annotation on a class creates an GraphQl input named by class name + "Input" suffix if a class name does not end with it already. +You can add multiple `@Input` annotations to the same class, give them different names and link different fields. +Consider the following example: + + + +```php +#[Input(name: 'CreateUserInput', default: true)] +#[Input(name: 'UpdateUserInput', update: true)] +class UserInput +{ + + #[Field] + public string $username; + + #[Field(for: 'CreateUserInput')] + public string $email; + + #[Field(for: 'CreateUserInput', inputType: 'String!')] + #[Field(for: 'UpdateUserInput', inputType: 'String')] + public string $password; + + #[Field] + public ?int $age; +} +``` + +```php +/** + * @Input(name="CreateUserInput", default=true) + * @Input(name="UpdateUserInput", update=true) + */ +class UserInput +{ + + /** + * @Field() + * @var string + */ + public $username; + + /** + * @Field(for="CreateUserInput") + * @var string + */ + public string $email; + + /** + * @Field(for="CreateUserInput", inputType="String!") + * @Field(for="UpdateUserInput", inputType="String") + * @var string|null + */ + public $password; + + /** + * @Field() + * @var int|null + */ + public $age; +} +``` + + +There are 2 input types created for just one class: `CreateUserInput` and `UpdateUserInput`. A few notes: +- `CreateUserInput` input will be used by default for this class. +- Field `username` is created for both input types, and it is required because the property type is not nullable. +- Field `email` will appear only for `CreateUserInput` input. +- Field `password` will appear for both. For `CreateUserInput` it'll be the required field and for `UpdateUserInput` optional. +- Field `age` is optional for both input types. + +Note that `update: true` argument for `UpdateUserInput`. It should be used when input type is used for a partial update, +It removes all default values from all fields thus prevents setting default values via setters or directly to public properties. +In example above if you use the class as `UpdateUserInput` and set only `username` the other ones will be ignored. +In PHP 7 they will be set to `null`, while in PHP 8 they will be in not initialized state - this can be used as a trick +to check if user actually passed a value for a certain field. From 4c797fd1e3f8d1254512fc6dbec60c10f28a23f7 Mon Sep 17 00:00:00 2001 From: Andrew Maslov Date: Tue, 2 Feb 2021 23:58:12 +0200 Subject: [PATCH 34/41] Force fields to be optional with "update" input --- docs/annotations_reference.md | 2 +- docs/input_types.md | 2 +- src/Types/InputType.php | 5 +++++ tests/Fixtures/Integration/Models/Post.php | 6 ++++++ 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/docs/annotations_reference.md b/docs/annotations_reference.md index e04edaf92a..3d3eaedcc5 100644 --- a/docs/annotations_reference.md +++ b/docs/annotations_reference.md @@ -66,7 +66,7 @@ Attribute | Compulsory | Type | Definition name | *no* | string | The name of the GraphQL input type generated. If not passed, the name of the class with suffix "Input" is used. If the class ends with "Input", the "Input" suffix is not added. description | *no* | string | Description of the input type in the documentation. If not passed, PHP doc comment is used. default | *no* | bool | Defaults to *true* if name is not specified. Whether the annotated PHP class should be mapped by default to this type. -update | *no* | bool | Determines if the the input represents a partial update. When set to *true* input fields won't have default values thus won't be set on resolve if they are not specified in the query/mutation. +update | *no* | bool | Determines if the the input represents a partial update. When set to *true* all input fields will become optional and won't have default values thus won't be set on resolve if they are not specified in the query/mutation. ## @Field annotation diff --git a/docs/input_types.md b/docs/input_types.md index 77ef9339ed..d60ed3c98a 100644 --- a/docs/input_types.md +++ b/docs/input_types.md @@ -547,7 +547,7 @@ There are 2 input types created for just one class: `CreateUserInput` and `Updat - Field `age` is optional for both input types. Note that `update: true` argument for `UpdateUserInput`. It should be used when input type is used for a partial update, -It removes all default values from all fields thus prevents setting default values via setters or directly to public properties. +It makes all fields optional and removes all default values from thus prevents setting default values via setters or directly to public properties. In example above if you use the class as `UpdateUserInput` and set only `username` the other ones will be ignored. In PHP 7 they will be set to `null`, while in PHP 8 they will be in not initialized state - this can be used as a trick to check if user actually passed a value for a certain field. diff --git a/src/Types/InputType.php b/src/Types/InputType.php index 34c216a04d..75bc95066a 100644 --- a/src/Types/InputType.php +++ b/src/Types/InputType.php @@ -4,6 +4,7 @@ namespace TheCodingMachine\GraphQLite\Types; +use GraphQL\Type\Definition\NonNull; use GraphQL\Type\Definition\ResolveInfo; use ReflectionClass; use TheCodingMachine\GraphQLite\FailedResolvingInputType; @@ -40,6 +41,10 @@ public function __construct(string $className, string $inputName, ?string $descr foreach ($this->fields as $name => $field) { $type = $field->getType(); + if ($isUpdate && $type instanceof NonNull) { + $type = $type->getWrappedType(); + } + $fields[$name] = [ 'type' => $type, 'description' => $field->getDescription(), diff --git a/tests/Fixtures/Integration/Models/Post.php b/tests/Fixtures/Integration/Models/Post.php index eb0553c5df..31b5270780 100644 --- a/tests/Fixtures/Integration/Models/Post.php +++ b/tests/Fixtures/Integration/Models/Post.php @@ -52,6 +52,12 @@ class Post */ public $author = null; + /** + * @Field(for="UpdatePostInput") + * @var int + */ + public $views; + /** * @Field(for="UpdatePostInput") * @var string|null From 2ca6f24bd76890bf8b3eab22b30eef3f100433b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Mon, 8 Feb 2021 09:48:06 +0100 Subject: [PATCH 35/41] Attempting to change code coverage driver to see if this fixes core dumped error --- .github/workflows/continuous_integration.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml index 38803d54df..5ec5bc42d6 100644 --- a/.github/workflows/continuous_integration.yml +++ b/.github/workflows/continuous_integration.yml @@ -31,7 +31,7 @@ jobs: - name: "Install PHP with extensions" uses: "shivammathur/setup-php@v2" with: - coverage: "pcov" + coverage: "xdebug" php-version: "${{ matrix.php-version }}" tools: composer:v2 From a3c1aec2cbc6b7007871becac9e513b846d9cf37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Mon, 8 Feb 2021 09:48:18 +0100 Subject: [PATCH 36/41] adding XSD to PHPUnit file --- phpunit.xml.dist | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index c7b363d680..68da55ad03 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,6 +1,9 @@ - - - - ./tests/ - ./tests/dependencies/ - ./tests/Bootstrap.php - - + + + ./tests/ + ./tests/dependencies/ + ./tests/Bootstrap.php + + - - - src/ - - - - - - + + + src/ + + + + + + From 627516a940afce0026a747ceb696b6eb3acd3a78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Mon, 8 Feb 2021 09:54:11 +0100 Subject: [PATCH 37/41] Fixing PHPStan issue --- src/Http/WebonyxGraphqlMiddleware.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Http/WebonyxGraphqlMiddleware.php b/src/Http/WebonyxGraphqlMiddleware.php index 9d55df807f..67f66d63e8 100644 --- a/src/Http/WebonyxGraphqlMiddleware.php +++ b/src/Http/WebonyxGraphqlMiddleware.php @@ -137,7 +137,7 @@ private function decideHttpCode($result): int return $this->httpCodeDecider->decideHttpStatusCode($executionResult); }, $result); - return max($codes); + return (int) max($codes); } // @codeCoverageIgnoreStart From 8ab5cf34167192cce37c3242eb6acbf6e8d4ab57 Mon Sep 17 00:00:00 2001 From: Andrew Maslov Date: Thu, 11 Feb 2021 22:46:57 +0200 Subject: [PATCH 38/41] Code style fixes --- src/AnnotationReader.php | 13 ++++++------- src/Annotations/Field.php | 6 ++++-- src/FailedResolvingInputType.php | 5 +++-- src/FieldsBuilder.php | 5 ++++- src/Mappers/AbstractTypeMapper.php | 4 ++-- src/Middlewares/SourcePropertyResolver.php | 1 + src/QueryFieldDescriptor.php | 7 ++----- src/SchemaFactory.php | 1 + src/Types/InputType.php | 9 ++++++--- src/Utils/AccessPropertyException.php | 9 +++++++-- src/Utils/PropertyAccessor.php | 8 +++++--- 11 files changed, 41 insertions(+), 27 deletions(-) diff --git a/src/AnnotationReader.php b/src/AnnotationReader.php index be02aa6691..0297ab41bd 100644 --- a/src/AnnotationReader.php +++ b/src/AnnotationReader.php @@ -398,7 +398,6 @@ private function isErrorImportant(string $annotationClass, string $docComment, s * * @param ReflectionClass $refClass * @param class-string $annotationClass - * @param bool $inherited * * @return A[] * @@ -530,12 +529,12 @@ public function getPropertyAnnotations(ReflectionProperty $refProperty, string $ if (PHP_MAJOR_VERSION >= 8) { $attributes = $refProperty->getAttributes(); $toAddAnnotations = array_merge($toAddAnnotations, array_map( - static function ($attribute) { - return $attribute->newInstance(); - }, - array_filter($attributes, static function ($annotation) use ($annotationClass): bool { - return is_a($annotation->getName(), $annotationClass, true); - }) + static function ($attribute) { + return $attribute->newInstance(); + }, + array_filter($attributes, static function ($annotation) use ($annotationClass): bool { + return is_a($annotation->getName(), $annotationClass, true); + }) )); } } catch (AnnotationException $e) { diff --git a/src/Annotations/Field.php b/src/Annotations/Field.php index 6b8294cc54..fff273401c 100644 --- a/src/Annotations/Field.php +++ b/src/Annotations/Field.php @@ -49,9 +49,11 @@ public function __construct(array $attributes = [], ?string $name = null, ?strin $this->inputType = $inputType ?? $attributes['inputType'] ?? null; $forValue = $for ?? $attributes['for'] ?? null; - if ($forValue) { - $this->for = (array) $forValue; + if (! $forValue) { + return; } + + $this->for = (array) $forValue; } /** diff --git a/src/FailedResolvingInputType.php b/src/FailedResolvingInputType.php index e09db75f2a..ce69807244 100644 --- a/src/FailedResolvingInputType.php +++ b/src/FailedResolvingInputType.php @@ -5,6 +5,7 @@ namespace TheCodingMachine\GraphQLite; use RuntimeException; + use function sprintf; class FailedResolvingInputType extends RuntimeException @@ -16,11 +17,11 @@ public static function createForMissingConstructorParameter(string $class, strin public static function createForDecorator(string $class): self { - return new self("Input type '$class' cannot be a decorator."); + return new self(sprintf("Input type '%s' cannot be a decorator.", $class)); } public static function createForNotInstantiableClass(string $class): self { - return new self("Class '$class' annotated with @Input must be instantiable."); + return new self(sprintf("Class '%s' annotated with @Input must be instantiable.", $class)); } } diff --git a/src/FieldsBuilder.php b/src/FieldsBuilder.php index b36b102668..6e105218de 100644 --- a/src/FieldsBuilder.php +++ b/src/FieldsBuilder.php @@ -41,6 +41,7 @@ use TheCodingMachine\GraphQLite\Utils\PropertyAccessor; use Webmozart\Assert\Assert; +use function array_diff_key; use function array_fill_keys; use function array_intersect_key; use function array_keys; @@ -56,6 +57,7 @@ use function reset; use function rtrim; use function trim; + use const PHP_EOL; /** @@ -187,7 +189,8 @@ public function getInputFields(string $className, string $inputName, bool $isUpd } // Make sure @Field annotations applied to parent's private properties are taken into account as well. - if ($parent = $refClass->getParentClass()) { + $parent = $refClass->getParentClass(); + if ($parent) { $parentFields = $this->getInputFields($parent->getName(), $inputName, $isUpdate); $fields = array_merge($fields, array_diff_key($parentFields, $fields)); } diff --git a/src/Mappers/AbstractTypeMapper.php b/src/Mappers/AbstractTypeMapper.php index 9aaffae63d..03d6eec4e2 100644 --- a/src/Mappers/AbstractTypeMapper.php +++ b/src/Mappers/AbstractTypeMapper.php @@ -325,7 +325,7 @@ public function mapClassToInputType(string $className): ResolvableMutableInputIn $input = $this->getMaps()->getInputByObjectClass($className); if ($input !== null) { - [ $typeName, $description, $isUpdate ] = $input; + [$typeName, $description, $isUpdate] = $input; return $this->inputTypeGenerator->mapInput($className, $typeName, $description, $isUpdate); } @@ -357,7 +357,7 @@ public function mapNameToType(string $typeName): Type $input = $this->getMaps()->getInputByGraphQLInputTypeName($typeName); if ($input !== null) { - [ $className, $description, $isUpdate ] = $input; + [$className, $description, $isUpdate] = $input; return $this->inputTypeGenerator->mapInput($className, $typeName, $description, $isUpdate); } diff --git a/src/Middlewares/SourcePropertyResolver.php b/src/Middlewares/SourcePropertyResolver.php index fe68e4aaba..fb33748058 100644 --- a/src/Middlewares/SourcePropertyResolver.php +++ b/src/Middlewares/SourcePropertyResolver.php @@ -7,6 +7,7 @@ use TheCodingMachine\GraphQLite\GraphQLRuntimeException; use TheCodingMachine\GraphQLite\Utils\PropertyAccessor; use Webmozart\Assert\Assert; + use function get_class; use function is_object; diff --git a/src/QueryFieldDescriptor.php b/src/QueryFieldDescriptor.php index d10ff532f1..cd45f7cac1 100644 --- a/src/QueryFieldDescriptor.php +++ b/src/QueryFieldDescriptor.php @@ -16,6 +16,8 @@ use TheCodingMachine\GraphQLite\Middlewares\SourceResolver; use TheCodingMachine\GraphQLite\Parameters\ParameterInterface; +use function is_array; + /** * A class that describes a field to be created. * To contains getters and setters to alter the field behaviour. @@ -134,8 +136,6 @@ public function setPrefetchMethodName(?string $prefetchMethodName): void * Sets the callable targeting the resolver function if the resolver function is part of a service. * This should not be used in the context of a field middleware. * Use getResolver/setResolver if you want to wrap the resolver in another method. - * - * @param callable $callable */ public function setCallable(callable $callable): void { @@ -159,9 +159,6 @@ public function setTargetMethodOnSource(string $targetMethodOnSource): void $this->magicProperty = null; } - /** - * @param string|null $targetPropertyOnSource - */ public function setTargetPropertyOnSource(?string $targetPropertyOnSource): void { if ($this->originalResolver !== null) { diff --git a/src/SchemaFactory.php b/src/SchemaFactory.php index f1c6011bb0..220033087e 100644 --- a/src/SchemaFactory.php +++ b/src/SchemaFactory.php @@ -52,6 +52,7 @@ use TheCodingMachine\GraphQLite\Utils\NamespacedCache; use TheCodingMachine\GraphQLite\Utils\Namespaces\NamespaceFactory; +use function apcu_enabled; use function array_map; use function array_reverse; use function class_exists; diff --git a/src/Types/InputType.php b/src/Types/InputType.php index 75bc95066a..4189c0b79a 100644 --- a/src/Types/InputType.php +++ b/src/Types/InputType.php @@ -11,6 +11,9 @@ use TheCodingMachine\GraphQLite\FieldsBuilder; use TheCodingMachine\GraphQLite\Parameters\InputTypeProperty; use TheCodingMachine\GraphQLite\Utils\PropertyAccessor; + +use function array_diff_key; +use function array_flip; use function array_key_exists; /** @@ -30,8 +33,8 @@ class InputType extends MutableInputObjectType implements ResolvableMutableInput public function __construct(string $className, string $inputName, ?string $description, bool $isUpdate, FieldsBuilder $fieldsBuilder) { $reflection = new ReflectionClass($className); - if (!$reflection->isInstantiable()) { - throw FailedResolvingInputType::createForNotInstantiableClass($className); + if (! $reflection->isInstantiable()) { + throw FailedResolvingInputType::createForNotInstantiableClass($className); } $this->fields = $fieldsBuilder->getInputFields($className, $inputName, $isUpdate); @@ -133,7 +136,7 @@ private function getClassConstructParameterNames(): array $refClass = new ReflectionClass($this->className); $constructor = $refClass->getConstructor(); - if (!$constructor) { + if (! $constructor) { return []; } diff --git a/src/Utils/AccessPropertyException.php b/src/Utils/AccessPropertyException.php index 079d05154a..87d2f1fbfc 100644 --- a/src/Utils/AccessPropertyException.php +++ b/src/Utils/AccessPropertyException.php @@ -1,22 +1,27 @@ getParameters() as $parameter) { - if (!$parameter->isDefaultValueAvailable()) { + if (! $parameter->isDefaultValueAvailable()) { return false; } } From 8cdacaf41fc8594d58a5a5989ba32d0ac051f77c Mon Sep 17 00:00:00 2001 From: Andrew Maslov Date: Thu, 11 Feb 2021 23:08:57 +0200 Subject: [PATCH 39/41] Fixed phpstan error ignoring for ReflectionMethod in PHP8 --- phpstan.neon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpstan.neon b/phpstan.neon index e1d9bb2e30..098c58184f 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -4,7 +4,7 @@ parameters: - "#PHPDoc tag \\@throws with type Psr\\\\Container\\\\ContainerExceptionInterface is not subtype of Throwable#" #- "#Property TheCodingMachine\\\\GraphQLite\\\\Types\\\\ResolvableInputObjectType::\\$resolve \\(array&callable\\) does not accept array#" #- "#Parameter \\#2 \\$type of class TheCodingMachine\\\\GraphQLite\\\\Parameters\\\\InputTypeParameter constructor expects GraphQL\\\\Type\\\\Definition\\\\InputType&GraphQL\\\\Type\\\\Definition\\\\Type, GraphQL\\\\Type\\\\Definition\\\\InputType\\|GraphQL\\\\Type\\\\Definition\\\\Type given.#" - - "#Parameter .* of class ReflectionMethod constructor expects string, object\\|string given.#" + - "#Parameter .* of class ReflectionMethod constructor expects string(\\|null)?, object\\|string given.#" - message: '#Method TheCodingMachine\\GraphQLite\\Types\\Mutable(Interface|Object)Type::getFields\(\) should return array but returns array\|float\|int#' path: src/Types/MutableTrait.php From 0ff74b5c7632a45d1a1bb465b9416ba5356c50a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Sat, 27 Mar 2021 19:05:28 +0100 Subject: [PATCH 40/41] Updating GraphQLite to 4.2 release --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index db976321fe..833c93788d 100644 --- a/composer.json +++ b/composer.json @@ -66,7 +66,7 @@ }, "extra": { "branch-alias": { - "dev-master": "4.1.x-dev" + "dev-master": "4.2.x-dev" } } } From 1860b20523fa4f92a2b78ca3674d69a90caa4f99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Sat, 27 Mar 2021 19:21:10 +0100 Subject: [PATCH 41/41] Updating PHPStan version and checks --- composer.json | 2 +- phpstan.neon | 9 +++++++-- src/AnnotationReader.php | 3 +++ src/Mappers/Proxys/MutableInterfaceTypeAdapter.php | 2 +- src/Mappers/Proxys/MutableObjectTypeAdapter.php | 2 +- 5 files changed, 13 insertions(+), 5 deletions(-) diff --git a/composer.json b/composer.json index 833c93788d..03304feb0f 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,7 @@ "phpunit/phpunit": "^8.2.4||^9.4", "php-coveralls/php-coveralls": "^2.1", "mouf/picotainer": "^1.1", - "phpstan/phpstan": "^0.12.25", + "phpstan/phpstan": "^0.12.82", "beberlei/porpaginas": "^1.2", "myclabs/php-enum": "^1.6.6", "doctrine/coding-standard": "^8.2", diff --git a/phpstan.neon b/phpstan.neon index 098c58184f..3d08891dbd 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -33,9 +33,14 @@ parameters: message: '#Property TheCodingMachine\\GraphQLite\\Annotations\\Type::\$class \(class-string\\|null\) does not accept string.#' path: src/Annotations/Type.php - - message: '#Method TheCodingMachine\\GraphQLite\\AnnotationReader::getMethodAnnotations\(\) should return array but returns array.#' + message: '#Method TheCodingMachine\\GraphQLite\\AnnotationReader::(getMethodAnnotations|getPropertyAnnotations)\(\) should return array but returns array.#' + path: src/AnnotationReader.php + - + message: '#Method TheCodingMachine\\GraphQLite\\AnnotationReader::getClassAnnotations\(\) should return array but returns array.#' + path: src/AnnotationReader.php + - + message: '#Parameter \#1 \$annotations of class TheCodingMachine\\GraphQLite\\Annotations\\ParameterAnnotations constructor expects array, array given.#' path: src/AnnotationReader.php - - '#Call to an undefined method GraphQL\\Error\\ClientAware::getMessage\(\)#' #- diff --git a/src/AnnotationReader.php b/src/AnnotationReader.php index 0297ab41bd..e89471c77e 100644 --- a/src/AnnotationReader.php +++ b/src/AnnotationReader.php @@ -408,6 +408,9 @@ private function isErrorImportant(string $annotationClass, string $docComment, s */ public function getClassAnnotations(ReflectionClass $refClass, string $annotationClass, bool $inherited = true): array { + /** + * @var array> + */ $toAddAnnotations = []; do { try { diff --git a/src/Mappers/Proxys/MutableInterfaceTypeAdapter.php b/src/Mappers/Proxys/MutableInterfaceTypeAdapter.php index fbf39b440d..4d5ccc7985 100644 --- a/src/Mappers/Proxys/MutableInterfaceTypeAdapter.php +++ b/src/Mappers/Proxys/MutableInterfaceTypeAdapter.php @@ -27,7 +27,7 @@ */ class MutableInterfaceTypeAdapter extends MutableInterfaceType implements MutableInterface { - /** @use MutableAdapterTrait */ + /** @use MutableAdapterTrait */ use MutableAdapterTrait; public function __construct(InterfaceType $type, ?string $className = null) diff --git a/src/Mappers/Proxys/MutableObjectTypeAdapter.php b/src/Mappers/Proxys/MutableObjectTypeAdapter.php index e1cc2d4311..1878924c10 100644 --- a/src/Mappers/Proxys/MutableObjectTypeAdapter.php +++ b/src/Mappers/Proxys/MutableObjectTypeAdapter.php @@ -28,7 +28,7 @@ */ class MutableObjectTypeAdapter extends MutableObjectType implements MutableInterface { - /** @use MutableAdapterTrait */ + /** @use MutableAdapterTrait */ use MutableAdapterTrait; public function __construct(ObjectType $type, ?string $className = null)