Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow constructor fields to use middleware #636

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions src/FailedResolvingInputType.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
171 changes: 75 additions & 96 deletions src/FieldsBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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
Expand Down Expand Up @@ -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
{
Expand Down Expand Up @@ -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();
Expand All @@ -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(),
);
}
Expand Down Expand Up @@ -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
{
Expand Down Expand Up @@ -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;
Expand Down
57 changes: 33 additions & 24 deletions src/InputField.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,25 @@ final class InputField extends InputObjectField
/** @var callable */
private $resolve;

private bool $forConstructorHydration = false;

/**
* @param (Type&InputType) $type
* @param array<string, ParameterInterface> $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,
Expand All @@ -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'];
Expand Down Expand Up @@ -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(),
Expand Down
Loading
Loading