diff --git a/phpstan.neon b/phpstan.neon index b496200bf0..803977c572 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -17,10 +17,7 @@ parameters: - "#Parameter .* of class ReflectionMethod constructor expects string(\\|null)?, object\\|string given.#" - "#PHPDoc tag @throws with type Psr\\\\SimpleCache\\\\InvalidArgumentException is not subtype of Throwable#" - '#Variable \$context might not be defined.#' - # TODO: fix these in the resolver refactor PR that follows; it'll be initialized in the constructor - - '#Class TheCodingMachine\\GraphQLite\\(QueryFieldDescriptor|InputFieldDescriptor) has an uninitialized readonly property \$(originalResolver|resolver)\. Assign it in the constructor.#' - - '#Readonly property TheCodingMachine\\GraphQLite\\(QueryFieldDescriptor|InputFieldDescriptor)::\$(originalResolver|resolver) is assigned outside of the constructor\.#' - - '#Property TheCodingMachine\\GraphQLite\\(QueryFieldDescriptor|InputFieldDescriptor)::\$resolver \(callable\) in isset\(\) is not nullable\.#' + - '#Parameter \#1 \$callable of class TheCodingMachine\\GraphQLite\\Middlewares\\ServiceResolver constructor expects array{object, string}&callable\(\): mixed, array{object, non-empty-string} given.#' - message: '#Parameter .* of class GraphQL\\Error\\Error constructor expects#' path: src/Exceptions/WebonyxErrorHandler.php diff --git a/src/FailedResolvingInputType.php b/src/FailedResolvingInputType.php index ce69807244..3ae93cff10 100644 --- a/src/FailedResolvingInputType.php +++ b/src/FailedResolvingInputType.php @@ -10,9 +10,9 @@ class FailedResolvingInputType extends RuntimeException { - public static function createForMissingConstructorParameter(string $class, string $parameter): self + public static function createForMissingConstructorParameter(\ArgumentCountError $original): self { - return new self(sprintf("Parameter '%s' is missing for class '%s' constructor. It should be mapped as required field.", $parameter, $class)); + return new self(sprintf("%s. It should be mapped as required field.", $original->getMessage()), previous: $original); } public static function createForDecorator(string $class): self diff --git a/src/FieldsBuilder.php b/src/FieldsBuilder.php index 5cd1d75709..f9c7572559 100644 --- a/src/FieldsBuilder.php +++ b/src/FieldsBuilder.php @@ -38,7 +38,13 @@ use TheCodingMachine\GraphQLite\Middlewares\FieldMiddlewareInterface; use TheCodingMachine\GraphQLite\Middlewares\InputFieldHandlerInterface; use TheCodingMachine\GraphQLite\Middlewares\InputFieldMiddlewareInterface; +use TheCodingMachine\GraphQLite\Middlewares\MagicPropertyResolver; use TheCodingMachine\GraphQLite\Middlewares\MissingMagicGetException; +use TheCodingMachine\GraphQLite\Middlewares\ServiceResolver; +use TheCodingMachine\GraphQLite\Middlewares\SourceConstructorParameterResolver; +use TheCodingMachine\GraphQLite\Middlewares\SourceInputPropertyResolver; +use TheCodingMachine\GraphQLite\Middlewares\SourceMethodResolver; +use TheCodingMachine\GraphQLite\Middlewares\SourcePropertyResolver; use TheCodingMachine\GraphQLite\Parameters\InputTypeParameterInterface; use TheCodingMachine\GraphQLite\Parameters\ParameterInterface; use TheCodingMachine\GraphQLite\Parameters\PrefetchDataParameter; @@ -370,14 +376,6 @@ private function getFieldsByMethodAnnotations(string|object $controller, Reflect $type = $this->typeMapper->mapReturnType($refMethod, $docBlockObj); } - $fieldDescriptor = new QueryFieldDescriptor( - name: $name, - type: $type, - comment: trim($description), - deprecationReason: $this->getDeprecationReason($docBlockObj), - refMethod: $refMethod, - ); - $parameters = $refMethod->getParameters(); if ($injectSource === true) { $firstParameter = array_shift($parameters); @@ -398,19 +396,21 @@ private function getFieldsByMethodAnnotations(string|object $controller, Reflect $args = ['__graphqlite_prefectData' => $prefetchDataParameter, ...$args]; } - $fieldDescriptor = $fieldDescriptor->withParameters($args); + $resolver = is_string($controller) ? + new SourceMethodResolver($refMethod) : + new ServiceResolver([$controller, $methodName]); - if (is_string($controller)) { - $fieldDescriptor = $fieldDescriptor->withTargetMethodOnSource($refMethod->getDeclaringClass()->getName(), $methodName); - } else { - $callable = [$controller, $methodName]; - assert(is_callable($callable)); - $fieldDescriptor = $fieldDescriptor->withCallable($callable); - } - - $fieldDescriptor = $fieldDescriptor - ->withInjectSource($injectSource) - ->withMiddlewareAnnotations($this->annotationReader->getMiddlewareAnnotations($refMethod)); + $fieldDescriptor = new QueryFieldDescriptor( + name: $name, + type: $type, + resolver: $resolver, + originalResolver: $resolver, + parameters: $args, + injectSource: $injectSource, + comment: trim($description), + deprecationReason: $this->getDeprecationReason($docBlockObj), + middlewareAnnotations: $this->annotationReader->getMiddlewareAnnotations($refMethod), + ); $field = $this->fieldMiddleware->process($fieldDescriptor, new class implements FieldHandlerInterface { public function handle(QueryFieldDescriptor $fieldDescriptor): FieldDefinition|null @@ -480,26 +480,22 @@ private function getFieldsByPropertyAnnotations(string|object $controller, Refle assert($type instanceof OutputType); } + $originalResolver = new SourcePropertyResolver($refProperty); + $resolver = is_string($controller) ? + $originalResolver : + fn () => PropertyAccessor::getValue($controller, $refProperty->getName()); + $fieldDescriptor = new QueryFieldDescriptor( name: $name, type: $type, + resolver: $resolver, + originalResolver: $originalResolver, + injectSource: false, comment: trim($description), deprecationReason: $this->getDeprecationReason($docBlock), - refProperty: $refProperty, + middlewareAnnotations: $this->annotationReader->getMiddlewareAnnotations($refProperty), ); - if (is_string($controller)) { - $fieldDescriptor = $fieldDescriptor->withTargetPropertyOnSource($refProperty->getDeclaringClass()->getName(), $refProperty->getName()); - } else { - $fieldDescriptor = $fieldDescriptor->withCallable(static function () use ($controller, $refProperty) { - return PropertyAccessor::getValue($controller, $refProperty->getName()); - }); - } - - $fieldDescriptor = $fieldDescriptor - ->withInjectSource(false) - ->withMiddlewareAnnotations($this->annotationReader->getMiddlewareAnnotations($refProperty)); - $field = $this->fieldMiddleware->process($fieldDescriptor, new class implements FieldHandlerInterface { public function handle(QueryFieldDescriptor $fieldDescriptor): FieldDefinition|null { @@ -597,15 +593,16 @@ private function getQueryFieldsFromSourceFields(array $sourceFields, ReflectionC $type = $this->typeMapper->mapReturnType($refMethod, $docBlockObj); } + $resolver = new SourceMethodResolver($refMethod); + $fieldDescriptor = new QueryFieldDescriptor( name: $sourceField->getName(), type: $type, + resolver: $resolver, + originalResolver: $resolver, parameters: $args, - targetClass: $refMethod->getDeclaringClass()->getName(), - targetMethodOnSource: $methodName, comment: $description, deprecationReason: $deprecationReason ?? null, - refMethod: $refMethod, ); } else { $outputType = $sourceField->getOutputType(); @@ -619,11 +616,13 @@ private function getQueryFieldsFromSourceFields(array $sourceFields, ReflectionC $type = $this->resolvePhpType($phpTypeStr, $refClass, $magicGefRefMethod); } + $resolver = new MagicPropertyResolver($refClass->getName(), $sourceField->getSourceName() ?? $sourceField->getName()); + $fieldDescriptor = new QueryFieldDescriptor( name: $sourceField->getName(), type: $type, - targetClass: $refClass->getName(), - magicProperty: $sourceField->getSourceName() ?? $sourceField->getName(), + resolver: $resolver, + originalResolver: $resolver, comment: $sourceField->getDescription(), ); } @@ -889,27 +888,22 @@ private function getInputFieldsByMethodAnnotations(string|object $controller, Re assert($type instanceof InputType); + $resolver = new SourceMethodResolver($refMethod); + $inputFieldDescriptor = new InputFieldDescriptor( name: $name, type: $type, + resolver: $resolver, + originalResolver: $resolver, parameters: $args, + injectSource: $injectSource, comment: trim($description), - refMethod: $refMethod, + middlewareAnnotations: $this->annotationReader->getMiddlewareAnnotations($refMethod), isUpdate: $isUpdate, + hasDefaultValue: $isUpdate, + defaultValue: $args[$name]->getDefaultValue() ); - $inputFieldDescriptor = $inputFieldDescriptor - ->withHasDefaultValue($isUpdate) - ->withDefaultValue($args[$name]->getDefaultValue()); - $constructerParameters = $this->getClassConstructParameterNames($refClass); - if (!in_array($name, $constructerParameters)) { - $inputFieldDescriptor = $inputFieldDescriptor->withTargetMethodOnSource($refMethod->getDeclaringClass()->getName(), $methodName); - } - - $inputFieldDescriptor = $inputFieldDescriptor - ->withInjectSource($injectSource) - ->withMiddlewareAnnotations($this->annotationReader->getMiddlewareAnnotations($refMethod)); - $field = $this->inputFieldMiddleware->process($inputFieldDescriptor, new class implements InputFieldHandlerInterface { public function handle(InputFieldDescriptor $inputFieldDescriptor): InputField|null { @@ -965,53 +959,38 @@ private function getInputFieldsByPropertyAnnotations(string|object $controller, $description = $inputProperty->getDescription(); } - if (in_array($name, $constructerParameters)) { - $middlewareAnnotations = $this->annotationReader->getPropertyAnnotations($refProperty, MiddlewareAnnotationInterface::class); - if ($middlewareAnnotations !== []) { - throw IncompatibleAnnotationsException::middlewareAnnotationsUnsupported(); - } - // constructor hydrated - $field = new InputField( - $name, - $inputProperty->getType(), - [$inputProperty->getName() => $inputProperty], - null, - null, - trim($description), - $isUpdate, - $inputProperty->hasDefaultValue(), - $inputProperty->getDefaultValue(), - ); - } else { - $type = $inputProperty->getType(); - if (!$inputType && $isUpdate && $type instanceof NonNull) { - $type = $type->getWrappedType(); - } - assert($type instanceof InputType); + $type = $inputProperty->getType(); + if (!$inputType && $isUpdate && $type instanceof NonNull) { + $type = $type->getWrappedType(); + } + assert($type instanceof InputType); + $forConstructorHydration = in_array($name, $constructerParameters); + $resolver = $forConstructorHydration ? + new SourceConstructorParameterResolver($refProperty->getDeclaringClass()->getName(), $refProperty->getName()) : + new SourceInputPropertyResolver($refProperty); - // setters and properties - $inputFieldDescriptor = new InputFieldDescriptor( - name: $inputProperty->getName(), - type: $type, - parameters: [$inputProperty->getName() => $inputProperty], - targetClass: $refProperty->getDeclaringClass()->getName(), - targetPropertyOnSource: $refProperty->getName(), - injectSource: false, - comment: trim($description), - middlewareAnnotations: $this->annotationReader->getMiddlewareAnnotations($refProperty), - refProperty: $refProperty, - isUpdate: $isUpdate, - hasDefaultValue: $inputProperty->hasDefaultValue(), - defaultValue: $inputProperty->getDefaultValue(), - ); + // setters and properties + $inputFieldDescriptor = new InputFieldDescriptor( + name: $inputProperty->getName(), + type: $type, + resolver: $resolver, + originalResolver: $resolver, + parameters: [$inputProperty->getName() => $inputProperty], + injectSource: false, + forConstructorHydration: $forConstructorHydration, + comment: trim($description), + middlewareAnnotations: $this->annotationReader->getMiddlewareAnnotations($refProperty), + isUpdate: $isUpdate, + hasDefaultValue: $inputProperty->hasDefaultValue(), + defaultValue: $inputProperty->getDefaultValue(), + ); - $field = $this->inputFieldMiddleware->process($inputFieldDescriptor, new class implements InputFieldHandlerInterface { - public function handle(InputFieldDescriptor $inputFieldDescriptor): InputField|null - { - return InputField::fromFieldDescriptor($inputFieldDescriptor); - } - }); - } + $field = $this->inputFieldMiddleware->process($inputFieldDescriptor, new class implements InputFieldHandlerInterface { + public function handle(InputFieldDescriptor $inputFieldDescriptor): InputField|null + { + return InputField::fromFieldDescriptor($inputFieldDescriptor); + } + }); if ($field === null) { continue; diff --git a/src/InputField.php b/src/InputField.php index 16ad51cfc6..f2778c47c7 100644 --- a/src/InputField.php +++ b/src/InputField.php @@ -32,16 +32,25 @@ final class InputField extends InputObjectField /** @var callable */ private $resolve; - private bool $forConstructorHydration = false; - /** * @param (Type&InputType) $type * @param array $arguments Indexed by argument name. * @param mixed|null $defaultValue the default value set for this field * @param array{defaultValue?: mixed,description?: string|null,astNode?: InputValueDefinitionNode|null}|null $additionalConfig */ - public function __construct(string $name, InputType $type, array $arguments, ResolverInterface|null $originalResolver, callable|null $resolver, string|null $comment, bool $isUpdate, bool $hasDefaultValue, mixed $defaultValue, array|null $additionalConfig = null) - { + public function __construct( + string $name, + InputType $type, + array $arguments, + ResolverInterface $originalResolver, + callable $resolver, + private bool $forConstructorHydration, + string|null $comment, + bool $isUpdate, + bool $hasDefaultValue, + mixed $defaultValue, + array|null $additionalConfig = null + ) { $config = [ 'name' => $name, 'type' => $type, @@ -52,28 +61,27 @@ public function __construct(string $name, InputType $type, array $arguments, Res $config['defaultValue'] = $defaultValue; } - if ($originalResolver !== null && $resolver !== null) { - $this->resolve = function (object $source, array $args, $context, ResolveInfo $info) use ($arguments, $originalResolver, $resolver) { + $this->resolve = function (object|null $source, array $args, $context, ResolveInfo $info) use ($arguments, $originalResolver, $resolver) { + if ($this->forConstructorHydration) { + $toPassArgs = [ + $arguments[$this->name]->resolve($source, $args, $context, $info) + ]; + } else { $toPassArgs = $this->paramsToArguments($arguments, $source, $args, $context, $info, $resolver); - $result = $resolver($source, ...$toPassArgs); - - try { - $this->assertInputType($result); - } catch (TypeMismatchRuntimeException $e) { - $e->addInfo($this->name, $originalResolver->toString()); - throw $e; - } - - return $result; - }; - } else { - $this->forConstructorHydration = true; - $this->resolve = function (object|null $source, array $args, $context, ResolveInfo $info) use ($arguments) { - $result = $arguments[$this->name]->resolve($source, $args, $context, $info); + } + + $result = $resolver($source, ...$toPassArgs); + + try { $this->assertInputType($result); - return $result; - }; - } + } catch (TypeMismatchRuntimeException $e) { + $e->addInfo($this->name, $originalResolver->toString()); + throw $e; + } + + return $result; + }; + if ($additionalConfig !== null) { if (isset($additionalConfig['astNode'])) { $config['astNode'] = $additionalConfig['astNode']; @@ -126,6 +134,7 @@ private static function fromDescriptor(InputFieldDescriptor $fieldDescriptor): s $fieldDescriptor->getParameters(), $fieldDescriptor->getOriginalResolver(), $fieldDescriptor->getResolver(), + $fieldDescriptor->isForConstructorHydration(), $fieldDescriptor->getComment(), $fieldDescriptor->isUpdate(), $fieldDescriptor->hasDefaultValue(), diff --git a/src/InputFieldDescriptor.php b/src/InputFieldDescriptor.php index a3eff9abd0..5dd436994d 100644 --- a/src/InputFieldDescriptor.php +++ b/src/InputFieldDescriptor.php @@ -9,10 +9,13 @@ 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\SourceConstructorParameterResolver; use TheCodingMachine\GraphQLite\Middlewares\SourceInputPropertyResolver; use TheCodingMachine\GraphQLite\Middlewares\SourceMethodResolver; +use TheCodingMachine\GraphQLite\Middlewares\SourcePropertyResolver; use TheCodingMachine\GraphQLite\Parameters\ParameterInterface; use TheCodingMachine\GraphQLite\Utils\Cloneable; @@ -28,28 +31,21 @@ class InputFieldDescriptor { use Cloneable; - private readonly ResolverInterface $originalResolver; - /** @var callable */ - private readonly mixed $resolver; - /** * @param array $parameters - * @param callable|null $callable + * @param callable $resolver * @param bool $injectSource Whether we should inject the source as the first parameter or not. */ public function __construct( private readonly string $name, private readonly InputType&Type $type, + private readonly mixed $resolver, + private readonly SourceInputPropertyResolver|SourceConstructorParameterResolver|SourceMethodResolver|ServiceResolver $originalResolver, private readonly array $parameters = [], - private readonly mixed $callable = null, - private readonly string|null $targetClass = null, - private readonly string|null $targetMethodOnSource = null, - private readonly string|null $targetPropertyOnSource = null, private readonly bool $injectSource = false, + private readonly bool $forConstructorHydration = false, private readonly string|null $comment = null, private readonly MiddlewareAnnotations $middlewareAnnotations = new MiddlewareAnnotations([]), - private readonly ReflectionMethod|null $refMethod = null, - private readonly ReflectionProperty|null $refProperty = null, private readonly bool $isUpdate = false, private readonly bool $hasDefaultValue = false, private readonly mixed $defaultValue = null, @@ -119,67 +115,24 @@ public function withParameters(array $parameters): self return $this->with(parameters: $parameters); } - /** - * 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. - */ - public function withCallable(callable $callable): self - { - if (isset($this->originalResolver)) { - throw new GraphQLRuntimeException('You cannot modify the target method via withCallable because it was already used. You can still wrap the callable using getResolver/withResolver'); - } - - // To be enabled in a future PR - // $this->magicProperty = null; - return $this->with( - callable: $callable, - targetClass: null, - targetMethodOnSource: null, - targetPropertyOnSource: null, - ); - } - - public function withTargetMethodOnSource(string $className, string $targetMethodOnSource): self + public function isInjectSource(): bool { - if (isset($this->originalResolver)) { - throw new GraphQLRuntimeException('You cannot modify the target method via withTargetMethodOnSource because it was already used. You can still wrap the callable using getResolver/withResolver'); - } - - // To be enabled in a future PR - // $this->magicProperty = null; - return $this->with( - callable: null, - targetClass: $className, - targetMethodOnSource: $targetMethodOnSource, - targetPropertyOnSource: null, - ); + return $this->injectSource; } - public function withTargetPropertyOnSource(string $className, string|null $targetPropertyOnSource): self + public function withInjectSource(bool $injectSource): self { - if (isset($this->originalResolver)) { - throw new GraphQLRuntimeException('You cannot modify the target method via withTargetMethodOnSource because it was already used. You can still wrap the callable using getResolver/withResolver'); - } - - // To be enabled in a future PR - // $this->magicProperty = null; - return $this->with( - callable: null, - targetClass: $className, - targetMethodOnSource: null, - targetPropertyOnSource: $targetPropertyOnSource, - ); + return $this->with(injectSource: $injectSource); } - public function isInjectSource(): bool + public function isForConstructorHydration(): bool { - return $this->injectSource; + return $this->forConstructorHydration; } - public function withInjectSource(bool $injectSource): self + public function withForConstructorHydration(bool $forConstructorHydration): self { - return $this->with(injectSource: $injectSource); + return $this->with(forConstructorHydration: $forConstructorHydration); } public function getComment(): string|null @@ -202,54 +155,11 @@ public function withMiddlewareAnnotations(MiddlewareAnnotations $middlewareAnnot return $this->with(middlewareAnnotations: $middlewareAnnotations); } - public function getRefMethod(): ReflectionMethod|null - { - return $this->refMethod; - } - - public function withRefMethod(ReflectionMethod $refMethod): self - { - return $this->with(refMethod: $refMethod); - } - - public function getRefProperty(): ReflectionProperty|null - { - return $this->refProperty; - } - - public function withRefProperty(ReflectionProperty $refProperty): self - { - return $this->with(refProperty: $refProperty); - } - /** * Returns the original callable that will be used to resolve the field. */ - public function getOriginalResolver(): ResolverInterface + public function getOriginalResolver(): SourceInputPropertyResolver|SourceConstructorParameterResolver|SourceMethodResolver|ServiceResolver { - if (isset($this->originalResolver)) { - return $this->originalResolver; - } - - if (is_callable($this->callable)) { - /** @var callable&array{0:object, 1:string} $callable */ - $callable = $this->callable; - $this->originalResolver = new ServiceResolver($callable); - } elseif ($this->targetMethodOnSource !== null) { - assert($this->targetClass !== null); - - $this->originalResolver = new SourceMethodResolver($this->targetClass, $this->targetMethodOnSource); - } elseif ($this->targetPropertyOnSource !== null) { - assert($this->targetClass !== null); - - $this->originalResolver = new SourceInputPropertyResolver($this->targetClass, $this->targetPropertyOnSource); - // } elseif ($this->magicProperty !== null) { - // Enable magic properties in a future PR - // $this->originalResolver = new MagicInputPropertyResolver($this->magicProperty); - } else { - throw new GraphQLRuntimeException('The InputFieldDescriptor should be passed either a resolve method (via withCallable) or a target method on source object (via withTargetMethodOnSource).'); - } - return $this->originalResolver; } @@ -259,10 +169,6 @@ public function getOriginalResolver(): ResolverInterface */ public function getResolver(): callable { - if (! isset($this->resolver)) { - $this->resolver = $this->getOriginalResolver(); - } - return $this->resolver; } @@ -270,20 +176,4 @@ public function withResolver(callable $resolver): self { return $this->with(resolver: $resolver); } - - /* - * Set the magic property - * - * @todo enable this in a future PR - * - * public function setMagicProperty(string $magicProperty): void - * { - * if ($this->originalResolver !== null) { - * throw new GraphQLRuntimeException('You cannot modify the target method via withMagicProperty because it was already used. You can still wrap the callable using getResolver/withResolver'); - * } - * $this->targetMethodOnSource = null; - * $this->targetPropertyOnSource = null; - * return $this->with(magicProperty: $magicProperty); - * } - */ } diff --git a/src/Middlewares/BadExpressionInSecurityException.php b/src/Middlewares/BadExpressionInSecurityException.php index 1a1fda25bb..31964d0d0c 100644 --- a/src/Middlewares/BadExpressionInSecurityException.php +++ b/src/Middlewares/BadExpressionInSecurityException.php @@ -16,8 +16,8 @@ class BadExpressionInSecurityException extends Exception { public static function wrapException(Throwable $e, QueryFieldDescriptor|InputFieldDescriptor $fieldDescriptor): self { - $refMethod = $fieldDescriptor->getRefMethod(); - $message = 'An error occurred while evaluating expression in @Security annotation of method "' . $refMethod?->getDeclaringClass()?->getName() . '::' . $refMethod?->getName() . '": ' . $e->getMessage(); + $originalResolver = $fieldDescriptor->getOriginalResolver(); + $message = 'An error occurred while evaluating expression in @Security annotation of method "' . $originalResolver->toString() . '": ' . $e->getMessage(); return new self($message, $e->getCode(), $e); } diff --git a/src/Middlewares/MagicPropertyResolver.php b/src/Middlewares/MagicPropertyResolver.php index 43cbe498ce..2338442396 100644 --- a/src/Middlewares/MagicPropertyResolver.php +++ b/src/Middlewares/MagicPropertyResolver.php @@ -15,18 +15,30 @@ */ final class MagicPropertyResolver implements ResolverInterface { + /** + * @param class-string $className + */ public function __construct( private readonly string $className, private readonly string $propertyName, ) { } - public function executionSource(object|null $source): object + /** + * @return class-string + */ + public function className(): string { - if ($source === null) { - throw new GraphQLRuntimeException('You must provide a source for MagicPropertyResolver.'); - } + return $this->className; + } + public function propertyName(): string + { + return $this->propertyName; + } + + public function executionSource(object|null $source): object|null + { return $source; } diff --git a/src/Middlewares/ResolverInterface.php b/src/Middlewares/ResolverInterface.php index 6b025f9e15..5971ad1678 100644 --- a/src/Middlewares/ResolverInterface.php +++ b/src/Middlewares/ResolverInterface.php @@ -18,7 +18,7 @@ public function toString(): string; * the {@see ExtendedContactType::uppercaseName()} field, the source is a {@see Contact} * object, but execution source will be an instance of {@see ExtendedContactType}. */ - public function executionSource(object|null $source): object; + public function executionSource(object|null $source): object|null; public function __invoke(object|null $source, mixed ...$args): mixed; } diff --git a/src/Middlewares/SecurityFieldMiddleware.php b/src/Middlewares/SecurityFieldMiddleware.php index c3182dd962..d9fabaa0c1 100644 --- a/src/Middlewares/SecurityFieldMiddleware.php +++ b/src/Middlewares/SecurityFieldMiddleware.php @@ -104,7 +104,7 @@ public function process(QueryFieldDescriptor $queryFieldDescriptor, FieldHandler * * @return array */ - private function getVariables(array $args, array $parameters, object $source): array + private function getVariables(array $args, array $parameters, object|null $source): array { $variables = [ // If a user is not logged, we provide an empty user object to make usage easier diff --git a/src/Middlewares/SecurityInputFieldMiddleware.php b/src/Middlewares/SecurityInputFieldMiddleware.php index d930a08f37..9eaba0b626 100644 --- a/src/Middlewares/SecurityInputFieldMiddleware.php +++ b/src/Middlewares/SecurityInputFieldMiddleware.php @@ -73,7 +73,7 @@ public function process(InputFieldDescriptor $inputFieldDescriptor, InputFieldHa * * @return array */ - private function getVariables(array $args, array $parameters, object $source): array + private function getVariables(array $args, array $parameters, object|null $source): array { $variables = [ // If a user is not logged, we provide an empty user object to make usage easier diff --git a/src/Middlewares/ServiceResolver.php b/src/Middlewares/ServiceResolver.php index 873ced5729..fb4027ca3b 100644 --- a/src/Middlewares/ServiceResolver.php +++ b/src/Middlewares/ServiceResolver.php @@ -22,6 +22,12 @@ public function __construct(callable $callable) $this->callable = $callable; } + /** @return callable&array{0:object, 1:string} */ + public function callable(): callable + { + return $this->callable; + } + public function executionSource(object|null $source): object { return $this->callable[0]; diff --git a/src/Middlewares/SourceConstructorParameterResolver.php b/src/Middlewares/SourceConstructorParameterResolver.php new file mode 100644 index 0000000000..4611b423d8 --- /dev/null +++ b/src/Middlewares/SourceConstructorParameterResolver.php @@ -0,0 +1,50 @@ +className; + } + + public function parameterName(): string + { + return $this->parameterName; + } + + public function executionSource(object|null $source): object|null + { + return $source; + } + + public function __invoke(object|null $source, mixed ...$args): mixed + { + return $args[0]; + } + + public function toString(): string + { + return $this->className . '::__construct($' . $this->parameterName . ')'; + } +} \ No newline at end of file diff --git a/src/Middlewares/SourceInputPropertyResolver.php b/src/Middlewares/SourceInputPropertyResolver.php index a8405d52a5..b41232c83c 100644 --- a/src/Middlewares/SourceInputPropertyResolver.php +++ b/src/Middlewares/SourceInputPropertyResolver.php @@ -4,6 +4,7 @@ namespace TheCodingMachine\GraphQLite\Middlewares; +use ReflectionProperty; use TheCodingMachine\GraphQLite\GraphQLRuntimeException; use TheCodingMachine\GraphQLite\Utils\PropertyAccessor; @@ -15,18 +16,18 @@ final class SourceInputPropertyResolver implements ResolverInterface { public function __construct( - private readonly string $className, - private readonly string $propertyName, + private readonly ReflectionProperty $propertyReflection, ) { } - public function executionSource(object|null $source): object + public function propertyReflection(): ReflectionProperty { - if ($source === null) { - throw new GraphQLRuntimeException('You must provide a source for SourceInputPropertyResolver.'); - } + return $this->propertyReflection; + } + public function executionSource(object|null $source): object|null + { return $source; } @@ -36,13 +37,13 @@ public function __invoke(object|null $source, mixed ...$args): mixed throw new GraphQLRuntimeException('You must provide a source for SourceInputPropertyResolver.'); } - PropertyAccessor::setValue($source, $this->propertyName, ...$args); + PropertyAccessor::setValue($source, $this->propertyReflection->getName(), ...$args); return $args[0]; } public function toString(): string { - return $this->className . '::' . $this->propertyName; + return $this->propertyReflection->getDeclaringClass()->getName() . '::' . $this->propertyReflection->getName(); } } diff --git a/src/Middlewares/SourceMethodResolver.php b/src/Middlewares/SourceMethodResolver.php index 2aedcfa61b..6fc862fe7a 100644 --- a/src/Middlewares/SourceMethodResolver.php +++ b/src/Middlewares/SourceMethodResolver.php @@ -17,18 +17,18 @@ final class SourceMethodResolver implements ResolverInterface { public function __construct( - private readonly string $className, - private readonly string $methodName, + private readonly \ReflectionMethod $methodReflection, ) { } - public function executionSource(object|null $source): object + public function methodReflection(): \ReflectionMethod { - if ($source === null) { - throw new GraphQLRuntimeException('You must provide a source for SourceMethodResolver.'); - } + return $this->methodReflection; + } + public function executionSource(object|null $source): object|null + { return $source; } @@ -38,7 +38,7 @@ public function __invoke(object|null $source, mixed ...$args): mixed throw new GraphQLRuntimeException('You must provide a source for SourceMethodResolver.'); } - $callable = [$source, $this->methodName]; + $callable = [$source, $this->methodReflection->getName()]; assert(is_callable($callable)); return $callable(...$args); @@ -46,6 +46,6 @@ public function __invoke(object|null $source, mixed ...$args): mixed public function toString(): string { - return $this->className . '::' . $this->methodName . '()'; + return $this->methodReflection->getDeclaringClass()->getName() . '::' . $this->methodReflection->getName() . '()'; } } diff --git a/src/Middlewares/SourcePropertyResolver.php b/src/Middlewares/SourcePropertyResolver.php index 72fa720aab..3448deacb6 100644 --- a/src/Middlewares/SourcePropertyResolver.php +++ b/src/Middlewares/SourcePropertyResolver.php @@ -4,6 +4,7 @@ namespace TheCodingMachine\GraphQLite\Middlewares; +use ReflectionProperty; use TheCodingMachine\GraphQLite\GraphQLRuntimeException; use TheCodingMachine\GraphQLite\Utils\PropertyAccessor; @@ -15,18 +16,18 @@ final class SourcePropertyResolver implements ResolverInterface { public function __construct( - private readonly string $className, - private readonly string $propertyName, + private readonly ReflectionProperty $propertyReflection, ) { } - public function executionSource(object|null $source): object + public function propertyReflection(): ReflectionProperty { - if ($source === null) { - throw new GraphQLRuntimeException('You must provide a source for SourcePropertyResolver.'); - } + return $this->propertyReflection; + } + public function executionSource(object|null $source): object|null + { return $source; } @@ -36,11 +37,11 @@ public function __invoke(object|null $source, mixed ...$args): mixed throw new GraphQLRuntimeException('You must provide a source for SourcePropertyResolver.'); } - return PropertyAccessor::getValue($source, $this->propertyName, ...$args); + return PropertyAccessor::getValue($source, $this->propertyReflection->getName(), ...$args); } public function toString(): string { - return $this->className . '::' . $this->propertyName; + return $this->propertyReflection->getDeclaringClass()->getName() . '::' . $this->propertyReflection->getName(); } } diff --git a/src/QueryFieldDescriptor.php b/src/QueryFieldDescriptor.php index e12b88a2ee..13bdc491f0 100644 --- a/src/QueryFieldDescriptor.php +++ b/src/QueryFieldDescriptor.php @@ -12,6 +12,7 @@ use TheCodingMachine\GraphQLite\Middlewares\MagicPropertyResolver; use TheCodingMachine\GraphQLite\Middlewares\ResolverInterface; use TheCodingMachine\GraphQLite\Middlewares\ServiceResolver; +use TheCodingMachine\GraphQLite\Middlewares\SourceInputPropertyResolver; use TheCodingMachine\GraphQLite\Middlewares\SourceMethodResolver; use TheCodingMachine\GraphQLite\Middlewares\SourcePropertyResolver; use TheCodingMachine\GraphQLite\Parameters\ParameterInterface; @@ -29,30 +30,21 @@ class QueryFieldDescriptor { use Cloneable; - private readonly ResolverInterface $originalResolver; - /** @var callable */ - private readonly mixed $resolver; - /** * @param array $parameters - * @param callable $callable + * @param callable $resolver * @param bool $injectSource Whether we should inject the source as the first parameter or not. */ public function __construct( private readonly string $name, private readonly OutputType&Type $type, + private readonly mixed $resolver, + private readonly SourcePropertyResolver|MagicPropertyResolver|SourceMethodResolver|ServiceResolver $originalResolver, private readonly array $parameters = [], - private readonly mixed $callable = null, - private readonly string|null $targetClass = null, - private readonly string|null $targetMethodOnSource = null, - private readonly string|null $targetPropertyOnSource = null, - private readonly string|null $magicProperty = null, private readonly bool $injectSource = false, private readonly string|null $comment = null, private readonly string|null $deprecationReason = null, private readonly MiddlewareAnnotations $middlewareAnnotations = new MiddlewareAnnotations([]), - private readonly ReflectionMethod|null $refMethod = null, - private readonly ReflectionProperty|null $refProperty = null, ) { } @@ -89,71 +81,6 @@ public function withParameters(array $parameters): self return $this->with(parameters: $parameters); } - /** - * 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. - */ - public function withCallable(callable $callable): self - { - if (isset($this->originalResolver)) { - throw new GraphQLRuntimeException('You cannot modify the callable via withCallable because it was already used. You can still wrap the callable using getResolver/withResolver'); - } - - return $this->with( - callable: $callable, - targetClass: null, - targetMethodOnSource: null, - targetPropertyOnSource: null, - magicProperty: null, - ); - } - - public function withTargetMethodOnSource(string $className, string $targetMethodOnSource): self - { - if (isset($this->originalResolver)) { - throw new GraphQLRuntimeException('You cannot modify the target method via withTargetMethodOnSource because it was already used. You can still wrap the callable using getResolver/withResolver'); - } - - return $this->with( - callable: null, - targetClass: $className, - targetMethodOnSource: $targetMethodOnSource, - targetPropertyOnSource: null, - magicProperty: null, - ); - } - - public function withTargetPropertyOnSource(string $className, string|null $targetPropertyOnSource): self - { - if (isset($this->originalResolver)) { - throw new GraphQLRuntimeException('You cannot modify the target method via withTargetMethodOnSource because it was already used. You can still wrap the callable using getResolver/withResolver'); - } - - return $this->with( - callable: null, - targetClass: $className, - targetMethodOnSource: null, - targetPropertyOnSource: $targetPropertyOnSource, - magicProperty: null, - ); - } - - public function withMagicProperty(string $className, string $magicProperty): self - { - if (isset($this->originalResolver)) { - throw new GraphQLRuntimeException('You cannot modify the target method via withMagicProperty because it was already used. You can still wrap the callable using getResolver/withResolver'); - } - - return $this->with( - callable: null, - targetClass: $className, - targetMethodOnSource: null, - targetPropertyOnSource: null, - magicProperty: $magicProperty, - ); - } - public function isInjectSource(): bool { return $this->injectSource; @@ -203,55 +130,11 @@ public function withMiddlewareAnnotations(MiddlewareAnnotations $middlewareAnnot return $this->with(middlewareAnnotations: $middlewareAnnotations); } - public function getRefMethod(): ReflectionMethod|null - { - return $this->refMethod; - } - - public function withRefMethod(ReflectionMethod $refMethod): self - { - return $this->with(refMethod: $refMethod); - } - - public function getRefProperty(): ReflectionProperty|null - { - return $this->refProperty; - } - - public function withRefProperty(ReflectionProperty $refProperty): self - { - return $this->with(refProperty: $refProperty); - } - /** * Returns the original callable that will be used to resolve the field. */ - public function getOriginalResolver(): ResolverInterface + public function getOriginalResolver(): SourcePropertyResolver|MagicPropertyResolver|SourceMethodResolver|ServiceResolver { - if (isset($this->originalResolver)) { - return $this->originalResolver; - } - - 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) { - assert($this->targetClass !== null); - - $this->originalResolver = new SourceMethodResolver($this->targetClass, $this->targetMethodOnSource); - } elseif ($this->targetPropertyOnSource !== null) { - assert($this->targetClass !== null); - - $this->originalResolver = new SourcePropertyResolver($this->targetClass, $this->targetPropertyOnSource); - } elseif ($this->magicProperty !== null) { - assert($this->targetClass !== null); - - $this->originalResolver = new MagicPropertyResolver($this->targetClass, $this->magicProperty); - } else { - throw new GraphQLRuntimeException('The QueryFieldDescriptor should be passed either a resolve method (via withCallable) or a target method on source object (via withTargetMethodOnSource) or a magic property (via withMagicProperty).'); - } - return $this->originalResolver; } @@ -261,10 +144,6 @@ public function getOriginalResolver(): ResolverInterface */ public function getResolver(): callable { - if (! isset($this->resolver)) { - $this->resolver = $this->getOriginalResolver(); - } - return $this->resolver; } diff --git a/src/Types/InputType.php b/src/Types/InputType.php index d026255b17..4135a3ffb9 100644 --- a/src/Types/InputType.php +++ b/src/Types/InputType.php @@ -4,6 +4,7 @@ namespace TheCodingMachine\GraphQLite\Types; +use ArgumentCountError; use GraphQL\Type\Definition\ResolveInfo; use ReflectionClass; use TheCodingMachine\GraphQLite\FailedResolvingInputType; @@ -66,19 +67,9 @@ public function __construct( /** @param array $args */ public function resolve(object|null $source, array $args, mixed $context, ResolveInfo $resolveInfo): object { - $constructorArgs = []; - foreach ($this->constructorInputFields as $constructorInputField) { - $name = $constructorInputField->name; - $resolve = $constructorInputField->getResolve(); - - if (! array_key_exists($name, $args)) { - continue; - } - - $constructorArgs[$name] = $resolve(null, $args, $context, $resolveInfo); - } - - $instance = $this->createInstance($constructorArgs); + // Sometimes developers may wish to pull the source from somewhere (like a model from a database) + // instead of actually creating a new instance. So if given, we'll use that. + $source = $this->createInstance($this->makeConstructorArgs($source, $args, $context, $resolveInfo)); foreach ($this->inputFields as $inputField) { $name = $inputField->name; @@ -87,14 +78,14 @@ public function resolve(object|null $source, array $args, mixed $context, Resolv } $resolve = $inputField->getResolve(); - $resolve($instance, $args, $context, $resolveInfo); + $resolve($source, $args, $context, $resolveInfo); } if ($this->inputTypeValidator && $this->inputTypeValidator->isEnabled()) { - $this->inputTypeValidator->validate($instance); + $this->inputTypeValidator->validate($source); } - return $instance; + return $source; } public function decorate(callable $decorator): void @@ -102,6 +93,30 @@ public function decorate(callable $decorator): void throw FailedResolvingInputType::createForDecorator($this->className); } + /** + * @param array $args + * + * @return array + */ + private function makeConstructorArgs(object|null $source, array $args, mixed $context, ResolveInfo $resolveInfo): array + { + $constructorArgs = []; + foreach ($this->constructorInputFields as $constructorInputField) { + $name = $constructorInputField->name; + $resolve = $constructorInputField->getResolve(); + + if (! array_key_exists($name, $args)) { + continue; + } + + // Although $source will most likely be either `null` or unused by the resolver, we'll still + // pass it in there in case the developer does want to use a source somehow. + $constructorArgs[$name] = $resolve($source, $args, $context, $resolveInfo); + } + + return $constructorArgs; + } + /** * Creates an instance of the input class. * @@ -110,23 +125,13 @@ public function decorate(callable $decorator): void private function createInstance(array $values): object { $refClass = new ReflectionClass($this->className); - $constructor = $refClass->getConstructor(); - $constructorParameters = $constructor ? $constructor->getParameters() : []; - - $parameters = []; - foreach ($constructorParameters as $parameter) { - $name = $parameter->getName(); - if (! array_key_exists($name, $values)) { - if (! $parameter->isDefaultValueAvailable()) { - throw FailedResolvingInputType::createForMissingConstructorParameter($refClass->getName(), $name); - } - - $values[$name] = $parameter->getDefaultValue(); - } - $parameters[] = $values[$name]; + try { + // This is the same as named parameters syntax, meaning default values are automatically used + // and any missing properties without default values will throw a fatal error. + return $refClass->newInstance(...$values); + } catch (ArgumentCountError $e) { + throw FailedResolvingInputType::createForMissingConstructorParameter($e); } - - return $refClass->newInstanceArgs($parameters); } } diff --git a/tests/FieldsBuilderTest.php b/tests/FieldsBuilderTest.php index 9f25172526..ae039b1c18 100644 --- a/tests/FieldsBuilderTest.php +++ b/tests/FieldsBuilderTest.php @@ -777,7 +777,7 @@ public function testSecurityBadQuery(): void $resolve = $query->resolveFn; $this->expectException(BadExpressionInSecurityException::class); - $this->expectExceptionMessage('An error occurred while evaluating expression in @Security annotation of method "TheCodingMachine\GraphQLite\Fixtures\TestControllerWithBadSecurity::testBadSecurity": Unexpected token "name" of value "is" around position 6 for expression `this is not valid expression language`.'); + $this->expectExceptionMessage('An error occurred while evaluating expression in @Security annotation of method "TheCodingMachine\GraphQLite\Fixtures\TestControllerWithBadSecurity::testBadSecurity()": Unexpected token "name" of value "is" around position 6 for expression `this is not valid expression language`.'); $result = $resolve(new stdClass(), [], null, $this->createMock(ResolveInfo::class)); } diff --git a/tests/Fixtures/Inputs/TestConstructorAndPropertiesInvalid.php b/tests/Fixtures/Inputs/TestConstructorPromotedProperties.php similarity index 53% rename from tests/Fixtures/Inputs/TestConstructorAndPropertiesInvalid.php rename to tests/Fixtures/Inputs/TestConstructorPromotedProperties.php index 2c55bbe0cb..7eaaae3356 100644 --- a/tests/Fixtures/Inputs/TestConstructorAndPropertiesInvalid.php +++ b/tests/Fixtures/Inputs/TestConstructorPromotedProperties.php @@ -2,39 +2,24 @@ namespace TheCodingMachine\GraphQLite\Fixtures\Inputs; -use Exception; use TheCodingMachine\GraphQLite\Annotations\Field; use TheCodingMachine\GraphQLite\Annotations\Input; use TheCodingMachine\GraphQLite\Annotations\Right; -/** - * @Input() - */ -class TestConstructorAndPropertiesInvalid +#[Input] +class TestConstructorPromotedProperties { - - /** - * @Field() - */ - private \DateTimeImmutable $date; - - /** - * @Field() - * @Right("INVALID_MIDDLEWARE") - * @var string - */ - private $foo; - - /** - * @Field() - * @var int - */ - private $bar; - - public function __construct(\DateTimeImmutable $date, string $foo) + #[Field] + private int $bar; + + public function __construct( + #[Field] + private readonly \DateTimeImmutable $date, + #[Field] + #[Right('FOOOOO')] + public string $foo + ) { - $this->date = $date; - $this->foo = $foo; } public function getDate(): \DateTimeImmutable @@ -47,11 +32,6 @@ public function setFoo(string $foo): void throw new \RuntimeException("This should not be called"); } - public function getFoo(): string - { - return $this->foo; - } - public function setBar(int $bar): void { $this->bar = $bar; @@ -61,4 +41,4 @@ public function getBar(): int { return $this->bar; } -} +} \ No newline at end of file diff --git a/tests/Fixtures/Integration/Controllers/ArticleController.php b/tests/Fixtures/Integration/Controllers/ArticleController.php index ca2c32eff4..723adc9878 100644 --- a/tests/Fixtures/Integration/Controllers/ArticleController.php +++ b/tests/Fixtures/Integration/Controllers/ArticleController.php @@ -7,6 +7,7 @@ use TheCodingMachine\GraphQLite\Annotations\Query; use TheCodingMachine\GraphQLite\Fixtures\Integration\Models\Article; use TheCodingMachine\GraphQLite\Fixtures\Integration\Models\Contact; +use TheCodingMachine\GraphQLite\Fixtures\Integration\Models\UpdateArticleInput; use TheCodingMachine\GraphQLite\Fixtures\Integration\Models\User; class ArticleController @@ -34,4 +35,13 @@ public function createArticle(Article $article): Article { return $article; } + + #[Mutation] + public function updateArticle(UpdateArticleInput $input): Article + { + $article = new Article('test'); + $article->magazine = $input->magazine; + + return $article; + } } diff --git a/tests/Fixtures/Integration/Models/UpdateArticleInput.php b/tests/Fixtures/Integration/Models/UpdateArticleInput.php new file mode 100644 index 0000000000..ecd4f538f8 --- /dev/null +++ b/tests/Fixtures/Integration/Models/UpdateArticleInput.php @@ -0,0 +1,19 @@ +toArray(DebugFlag::RETHROW_INTERNAL_EXCEPTIONS); } + public function testEndToEndInputConstructor(): void + { + $schema = $this->mainContainer->get(Schema::class); + assert($schema instanceof Schema); + + $queryString = ' + mutation { + updateArticle(input: { + magazine: "Test" + }) { + magazine + } + } + '; + + $result = GraphQL::executeQuery( + $schema, + $queryString, + ); + + $data = $this->getSuccessResult($result); + $this->assertSame('Test', $data['updateArticle']['magazine']); + $queryString = ' + mutation { + updateArticle(input: { + magazine: "NYTimes" + }) { + magazine + } + } + '; + + $result = GraphQL::executeQuery( + $schema, + $queryString, + ); + + $this->assertSame('Access denied.', $result->toArray(DebugFlag::RETHROW_UNSAFE_EXCEPTIONS)['errors'][0]['message']); + } + public function testEndToEndSetterWithSecurity(): void { $container = $this->createContainer([ diff --git a/tests/Middlewares/AuthorizationFieldMiddlewareTest.php b/tests/Middlewares/AuthorizationFieldMiddlewareTest.php index c596c0399e..f756cae352 100644 --- a/tests/Middlewares/AuthorizationFieldMiddlewareTest.php +++ b/tests/Middlewares/AuthorizationFieldMiddlewareTest.php @@ -102,14 +102,15 @@ public function testThrowsForbiddenExceptionWhenNotAuthorized(): void */ private function stubDescriptor(array $annotations): QueryFieldDescriptor { - $descriptor = new QueryFieldDescriptor( + $resolver = fn () => self::fail('Should not be called.'); + + return new QueryFieldDescriptor( name: 'foo', type: Type::string(), + resolver: $resolver, + originalResolver: new ServiceResolver($resolver), middlewareAnnotations: new MiddlewareAnnotations($annotations), ); - $descriptor = $descriptor->withResolver(fn () => self::fail('Should not be called.')); - - return $descriptor; } private function stubFieldHandler(): FieldHandlerInterface diff --git a/tests/Middlewares/AuthorizationInputFieldMiddlewareTest.php b/tests/Middlewares/AuthorizationInputFieldMiddlewareTest.php index db299e99e6..783feab83b 100644 --- a/tests/Middlewares/AuthorizationInputFieldMiddlewareTest.php +++ b/tests/Middlewares/AuthorizationInputFieldMiddlewareTest.php @@ -84,16 +84,15 @@ public function testThrowsForbiddenExceptionWhenNotAuthorized(): void */ private function stubDescriptor(array $annotations): InputFieldDescriptor { - $descriptor = new InputFieldDescriptor( + $resolver = fn () => self::fail('Should not be called.'); + + return new InputFieldDescriptor( name: 'foo', type: Type::string(), - targetClass: stdClass::class, - targetMethodOnSource: 'foo', + resolver: $resolver, + originalResolver: new ServiceResolver($resolver), middlewareAnnotations: new MiddlewareAnnotations($annotations), ); - $descriptor = $descriptor->withResolver(fn () => self::fail('Should not be called.')); - - return $descriptor; } private function stubFieldHandler(): InputFieldHandlerInterface @@ -109,10 +108,11 @@ public function handle(InputFieldDescriptor $inputFieldDescriptor): InputField|n ], originalResolver: $inputFieldDescriptor->getOriginalResolver(), resolver: $inputFieldDescriptor->getResolver(), + forConstructorHydration: false, comment: null, isUpdate: false, hasDefaultValue: false, - defaultValue: null + defaultValue: null, ); } }; diff --git a/tests/Middlewares/CostFieldMiddlewareTest.php b/tests/Middlewares/CostFieldMiddlewareTest.php index c5ddccabc6..c0e5a93631 100644 --- a/tests/Middlewares/CostFieldMiddlewareTest.php +++ b/tests/Middlewares/CostFieldMiddlewareTest.php @@ -129,14 +129,15 @@ public static function addsCostInDescriptionProvider(): iterable */ private function stubDescriptor(array $annotations): QueryFieldDescriptor { - $descriptor = new QueryFieldDescriptor( + $resolver = fn () => self::fail('Should not be called.'); + + return new QueryFieldDescriptor( name: 'foo', type: Type::string(), + resolver: $resolver, + originalResolver: new ServiceResolver($resolver), middlewareAnnotations: new MiddlewareAnnotations($annotations), ); - $descriptor = $descriptor->withResolver(fn () => self::fail('Should not be called.')); - - return $descriptor; } private function stubFieldHandler(FieldDefinition|null $field): FieldHandlerInterface diff --git a/tests/Middlewares/FieldMiddlewarePipeTest.php b/tests/Middlewares/FieldMiddlewarePipeTest.php index 8fd3815920..0f70d8c468 100644 --- a/tests/Middlewares/FieldMiddlewarePipeTest.php +++ b/tests/Middlewares/FieldMiddlewarePipeTest.php @@ -19,9 +19,12 @@ public function handle(QueryFieldDescriptor $fieldDescriptor): ?FieldDefinition } }; + $resolver = fn () => self::fail('Should not be called.'); $descriptor = new QueryFieldDescriptor( name: 'foo', type: Type::string(), + resolver: $resolver, + originalResolver: new ServiceResolver($resolver), ); $middlewarePipe = new FieldMiddlewarePipe(); @@ -38,6 +41,8 @@ public function process(QueryFieldDescriptor $queryFieldDescriptor, FieldHandler $descriptor = new QueryFieldDescriptor( name: 'bar', type: Type::string(), + resolver: $resolver, + originalResolver: new ServiceResolver($resolver), ); $definition = $middlewarePipe->process($descriptor, $finalHandler); diff --git a/tests/Middlewares/InputFieldMiddlewarePipeTest.php b/tests/Middlewares/InputFieldMiddlewarePipeTest.php index 5454d5ec8d..7dbe879175 100644 --- a/tests/Middlewares/InputFieldMiddlewarePipeTest.php +++ b/tests/Middlewares/InputFieldMiddlewarePipeTest.php @@ -19,13 +19,15 @@ public function handle(InputFieldDescriptor $inputFieldDescriptor): ?InputField } }; + $resolver = static function (){ + return null; + }; $middlewarePipe = new InputFieldMiddlewarePipe(); $inputFieldDescriptor = new InputFieldDescriptor( name: 'foo', type: Type::string(), - callable: static function (){ - return null; - } + resolver: $resolver, + originalResolver: new ServiceResolver($resolver), ); $definition = $middlewarePipe->process($inputFieldDescriptor, $finalHandler); $this->assertSame('foo', $definition->name); diff --git a/tests/Middlewares/SourceResolverTest.php b/tests/Middlewares/SourceResolverTest.php index 09e1768b3d..5c47a7fbec 100644 --- a/tests/Middlewares/SourceResolverTest.php +++ b/tests/Middlewares/SourceResolverTest.php @@ -3,7 +3,9 @@ namespace TheCodingMachine\GraphQLite\Middlewares; use PHPUnit\Framework\TestCase; +use ReflectionMethod; use stdClass; +use TheCodingMachine\GraphQLite\Fixtures\TestType; use TheCodingMachine\GraphQLite\GraphQLRuntimeException; class SourceResolverTest extends TestCase @@ -11,15 +13,15 @@ class SourceResolverTest extends TestCase public function testExceptionInInvoke() { - $sourceResolver = new SourceMethodResolver(stdClass::class, 'test'); + $sourceResolver = new SourceMethodResolver(new ReflectionMethod(TestType::class, 'customField')); $this->expectException(GraphQLRuntimeException::class); $sourceResolver(null); } public function testToString() { - $sourceResolver = new SourceMethodResolver(stdClass::class, 'test'); + $sourceResolver = new SourceMethodResolver(new ReflectionMethod(TestType::class, 'customField')); - $this->assertSame('stdClass::test()', $sourceResolver->toString()); + $this->assertSame('TheCodingMachine\GraphQLite\Fixtures\TestType::customField()', $sourceResolver->toString()); } } diff --git a/tests/QueryFieldDescriptorTest.php b/tests/QueryFieldDescriptorTest.php index fa42c373da..7ad4a3ba9a 100644 --- a/tests/QueryFieldDescriptorTest.php +++ b/tests/QueryFieldDescriptorTest.php @@ -5,79 +5,22 @@ use GraphQL\Type\Definition\Type; use PHPUnit\Framework\TestCase; use stdClass; +use TheCodingMachine\GraphQLite\Middlewares\ServiceResolver; class QueryFieldDescriptorTest extends TestCase { - public function testExceptionInSetCallable(): void - { - $descriptor = new QueryFieldDescriptor( - name: 'test', - type: Type::string(), - callable: [$this, 'testExceptionInSetCallable'], - ); - $descriptor->getResolver(); - - $this->expectException(GraphQLRuntimeException::class); - $descriptor->withCallable([$this, 'testExceptionInSetCallable']); - } - - public function testExceptionInSetTargetMethodOnSource(): void - { - $descriptor = new QueryFieldDescriptor( - name: 'test', - type: Type::string(), - targetClass: stdClass::class, - targetMethodOnSource: 'test' - ); - $descriptor->getResolver(); - - $this->expectException(GraphQLRuntimeException::class); - $descriptor->withTargetMethodOnSource(stdClass::class, 'test'); - } - - public function testExceptionInSetTargetPropertyOnSource(): void - { - $descriptor = new QueryFieldDescriptor( - name: 'test', - type: Type::string(), - targetClass: stdClass::class, - targetPropertyOnSource: 'test', - ); - $descriptor->getResolver(); - - $this->expectException(GraphQLRuntimeException::class); - $descriptor->withTargetPropertyOnSource(stdClass::class, 'test'); - } - - public function testExceptionInSetMagicProperty(): void - { - $descriptor = new QueryFieldDescriptor( - name: 'test', - type: Type::string(), - targetClass: stdClass::class, - magicProperty: 'test' - ); - $descriptor->getResolver(); - - $this->expectException(GraphQLRuntimeException::class); - $descriptor->withMagicProperty(stdClass::class, 'test'); - } - - public function testExceptionInGetOriginalResolver(): void - { - $descriptor = new QueryFieldDescriptor('test', Type::string()); - $this->expectException(GraphQLRuntimeException::class); - $descriptor->getOriginalResolver(); - } - /** * @dataProvider withAddedCommentLineProvider */ public function testWithAddedCommentLine(string $expected, string|null $previous, string $added): void { + $resolver = fn () => null; + $descriptor = (new QueryFieldDescriptor( 'test', Type::string(), + resolver: $resolver, + originalResolver: new ServiceResolver($resolver), comment: $previous, ))->withAddedCommentLines($added); diff --git a/tests/QueryFieldTest.php b/tests/QueryFieldTest.php index 6a14a5be33..38b0f19f3b 100644 --- a/tests/QueryFieldTest.php +++ b/tests/QueryFieldTest.php @@ -8,6 +8,7 @@ use GraphQL\Type\Definition\ResolveInfo; use GraphQL\Type\Definition\Type; use PHPUnit\Framework\TestCase; +use ReflectionMethod; use TheCodingMachine\GraphQLite\Fixtures\TestObject; use TheCodingMachine\GraphQLite\Middlewares\ServiceResolver; use TheCodingMachine\GraphQLite\Middlewares\SourceMethodResolver; @@ -19,7 +20,7 @@ class QueryFieldTest extends TestCase { public function testExceptionsHandling(): void { - $sourceResolver = new SourceMethodResolver(TestObject::class, 'getTest'); + $sourceResolver = new SourceMethodResolver(new ReflectionMethod(TestObject::class, 'getTest')); $queryField = new QueryField('foo', Type::string(), [ new class implements ParameterInterface { public function resolve(?object $source, array $args, mixed $context, ResolveInfo $info): mixed diff --git a/tests/Types/InputTypeTest.php b/tests/Types/InputTypeTest.php index be46fdc24b..58026484e0 100644 --- a/tests/Types/InputTypeTest.php +++ b/tests/Types/InputTypeTest.php @@ -17,7 +17,7 @@ use TheCodingMachine\GraphQLite\Fixtures\Inputs\InputInterface; use TheCodingMachine\GraphQLite\Fixtures\Inputs\InputWithSetter; use TheCodingMachine\GraphQLite\Fixtures\Inputs\TestConstructorAndProperties; -use TheCodingMachine\GraphQLite\Fixtures\Inputs\TestConstructorAndPropertiesInvalid; +use TheCodingMachine\GraphQLite\Fixtures\Inputs\TestConstructorPromotedProperties; use TheCodingMachine\GraphQLite\Fixtures\Inputs\TestOnlyConstruct; use TheCodingMachine\GraphQLite\Fixtures\Inputs\TypedFooBar; @@ -175,22 +175,34 @@ public function testResolvesCorrectlyWithConstructorAndProperties(): void $this->assertEquals(200, $result->getBar()); } - /** - * @group PR-466 - */ - public function testConstructorHydrationFailingWithMiddlewareAnnotations(): void + public function testResolvesCorrectlyWithConstructorPromotedProperties(): void { - $this->expectException(IncompatibleAnnotationsException::class); - $input = new InputType( - TestConstructorAndPropertiesInvalid::class, - 'TestConstructorAndPropertiesInvalidInput', + TestConstructorPromotedProperties::class, + 'TestConstructorPromotedPropertiesInput', null, false, $this->getFieldsBuilder(), ); $input->freeze(); $fields = $input->getFields(); + + $date = "2022-05-02T04:42:30Z"; + + $args = [ + 'date' => $date, + 'foo' => 'Foo', + 'bar' => 200, + ]; + + $resolveInfo = $this->createMock(ResolveInfo::class); + + /** @var TestConstructorPromotedProperties $result */ + $result = $input->resolve(null, $args, [], $resolveInfo); + + $this->assertEquals(new DateTime("2022-05-02T04:42:30Z"), $result->getDate()); + $this->assertEquals('Foo', $result->foo); + $this->assertEquals(200, $result->getBar()); } public function testFailsResolvingFieldWithoutRequiredConstructParam(): void @@ -202,7 +214,7 @@ public function testFailsResolvingFieldWithoutRequiredConstructParam(): void $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."); + $this->expectExceptionMessage("TheCodingMachine\GraphQLite\Fixtures\Inputs\FooBar::__construct(): Argument #1 (\$foo) not passed. It should be mapped as required field."); $input->resolve(null, $args, [], $resolveInfo); }