diff --git a/docs/annotations/arguments-transformer.md b/docs/annotations/arguments-transformer.md index f97c72183..f0db3ef74 100644 --- a/docs/annotations/arguments-transformer.md +++ b/docs/annotations/arguments-transformer.md @@ -99,3 +99,47 @@ class RootMutation { ``` So, the resolver (the `createUser` method) will receive an instance of the class `UserRegisterInput` instead of an array of data. + +### Special arguments + +The arguments transformer is also able to transform some `special arguments` (see auto-guessing) : + +- If the type `@info` is specified, it will be replaced by the current `ResolveInfo` + +You can use it this way : + +```php +/** + * @GQL\Type + */ +class RootQuery { + /** + * @GQL\Field( + * type="[User]", + * args={ + * @GQL\Arg(name="ids", type="[Int]"), + * @GQL\Arg(name="info", type="@info") + * }, + * resolve="@=call(service('UserRepository').getUser, arguments({ids: '[Int]', info: '@info'}, arg))" + * ) + */ + public $getUsers; +} +``` + +or with auto-guessing + +```php +/** + * @GQL\Provider + */ +class UsersResolver { + /** + * @GQL\Query(type="[User]") + */ + public function getUser(int $id, ResolveInfo $info):array + { + ... + } +} +``` \ No newline at end of file diff --git a/docs/annotations/index.md b/docs/annotations/index.md index 539c92267..7ad07121d 100644 --- a/docs/annotations/index.md +++ b/docs/annotations/index.md @@ -191,6 +191,34 @@ The GraphQL arguments will be auto-guessed as: - `@Arg(name="input", type="MyInput!")` (The input type corresponding to the `MyInputClass` will be used). - `@Arg(name="limit", type="Int", default = 10)` + +#### Special arguments + +In conjonction with the `Arguments transformer`, some arguments with type-hinted classes can also be auto-guessed. +The class `GraphQL\Type\Definition\ResolveInfo` will be auto-guessed as the `ResolveInfo` for the resolver. +Special arguments are not exposed as GraphQL argument as there are only used internally by the arguments transformer. + +You can inject it as follow: +```php +/** + * @GQL\Type + */ +class MyType { + /** + * @GQL\Field(type="[String]!") + */ + public function getSomething(int $amount, ResolveInfo $info) { + ... + } +} +``` + +The GraphQL arguments will only be: + +- `@Arg(name="amount", type="Int!")` + + + ### Limitation of auto-guessing: When trying to auto-guess a type or args based on PHP Reflection (from type hinted method parameters or type hinted return value), there is a limitation. diff --git a/src/Config/Parser/AnnotationParser.php b/src/Config/Parser/AnnotationParser.php index 21bfb0ec1..06827db63 100644 --- a/src/Config/Parser/AnnotationParser.php +++ b/src/Config/Parser/AnnotationParser.php @@ -11,6 +11,7 @@ use Doctrine\ORM\Mapping\OneToMany; use Doctrine\ORM\Mapping\OneToOne; use Exception; +use GraphQL\Type\Definition\ResolveInfo; use Overblog\GraphQLBundle\Annotation as GQL; use Overblog\GraphQLBundle\Config\Parser\Annotation\GraphClass; use Overblog\GraphQLBundle\Relay\Connection\ConnectionInterface; @@ -64,6 +65,11 @@ class AnnotationParser implements PreParserInterface private const VALID_INPUT_TYPES = [self::GQL_SCALAR, self::GQL_ENUM, self::GQL_INPUT]; private const VALID_OUTPUT_TYPES = [self::GQL_SCALAR, self::GQL_TYPE, self::GQL_INTERFACE, self::GQL_UNION, self::GQL_ENUM]; + /** Allow injection of special arguments in arguments transformer */ + private const SPECIAL_ARGUMENTS = [ + ResolveInfo::class => '@info', + ]; + /** * {@inheritdoc} * @@ -519,7 +525,8 @@ private static function getTypeFieldConfigurationFromReflector(GraphClass $graph } if (!empty($args)) { - $fieldConfiguration['args'] = $args; + /** Remove special arguments **/ + $fieldConfiguration['args'] = array_filter($args, fn ($arg) => !in_array($arg['type'], self::SPECIAL_ARGUMENTS)); } $fieldName = $fieldAnnotation->name ?: $fieldName; @@ -906,19 +913,23 @@ private static function guessArgs(ReflectionMethod $method): array throw new InvalidArgumentException(sprintf('Argument n°%s "$%s" on method "%s" cannot be auto-guessed as there is not type hint.', $index + 1, $parameter->getName(), $method->getName())); } - try { - // @phpstan-ignore-next-line - $gqlType = self::resolveGraphQLTypeFromReflectionType($parameter->getType(), self::VALID_INPUT_TYPES, $parameter->isDefaultValueAvailable()); - } catch (Exception $e) { - throw new InvalidArgumentException(sprintf('Argument n°%s "$%s" on method "%s" cannot be auto-guessed : %s".', $index + 1, $parameter->getName(), $method->getName(), $e->getMessage())); - } + if ($parameter->getClass() && isset(self::SPECIAL_ARGUMENTS[$parameter->getClass()->getName()])) { + $argumentConfig = ['type' => self::SPECIAL_ARGUMENTS[$parameter->getClass()->getName()]]; + } else { + try { + // @phpstan-ignore-next-line + $gqlType = self::resolveGraphQLTypeFromReflectionType($parameter->getType(), self::VALID_INPUT_TYPES, $parameter->isDefaultValueAvailable()); + } catch (Exception $e) { + throw new InvalidArgumentException(sprintf('Argument n°%s "$%s" on method "%s" cannot be auto-guessed : %s".', $index + 1, $parameter->getName(), $method->getName(), $e->getMessage())); + } - $argumentConfig = []; - if ($parameter->isDefaultValueAvailable()) { - $argumentConfig['defaultValue'] = $parameter->getDefaultValue(); - } + $argumentConfig = []; + if ($parameter->isDefaultValueAvailable()) { + $argumentConfig['defaultValue'] = $parameter->getDefaultValue(); + } - $argumentConfig['type'] = $gqlType; + $argumentConfig['type'] = $gqlType; + } $arguments[$parameter->getName()] = $argumentConfig; } diff --git a/src/Transformer/ArgumentsTransformer.php b/src/Transformer/ArgumentsTransformer.php index 9c310a0db..8c54fe397 100644 --- a/src/Transformer/ArgumentsTransformer.php +++ b/src/Transformer/ArgumentsTransformer.php @@ -174,7 +174,14 @@ public function getArguments(array $mapping, $data, ResolveInfo $info) foreach ($mapping as $name => $type) { try { - $value = $this->getInstanceAndValidate($type, $data[$name], $info, $name); + switch ($type) { + case '@info': + $value = $info; + break; + default: + $value = $this->getInstanceAndValidate($type, $data[$name], $info, $name); + break; + } $args[] = $value; } catch (InvalidArgumentError $exception) { $exceptions[] = $exception; diff --git a/tests/Config/Parser/AnnotationParserTest.php b/tests/Config/Parser/AnnotationParserTest.php index 48455eee1..87d0a7569 100644 --- a/tests/Config/Parser/AnnotationParserTest.php +++ b/tests/Config/Parser/AnnotationParserTest.php @@ -132,6 +132,11 @@ public function testTypes(): void 'args' => ['jediOnly' => ['type' => 'Boolean', 'description' => 'Only Jedi victims']], 'resolve' => '@=call(value.getVictims, arguments({jediOnly: "Boolean"}, args))', ], + 'oldMasters' => [ + 'type' => '[Character]', + 'args' => ['jediOnly' => ['type' => 'Boolean', 'description' => 'Only Jedi victims']], + 'resolve' => '@=call(value.getOldMasters, arguments({info: "@info", jediOnly: "Boolean"}, args))', + ], ], ]); @@ -333,7 +338,7 @@ public function testArgsAndReturnGuessing(): void 'away' => ['type' => 'Boolean', 'defaultValue' => false], 'maxDistance' => ['type' => 'Float', 'defaultValue' => null], ], - 'resolve' => '@=call(value.getCasualties, arguments({areaId: "Int!", raceId: "String!", dayStart: "Int", dayEnd: "Int", nameStartingWith: "String", planet: "PlanetInput", away: "Boolean", maxDistance: "Float"}, args))', + 'resolve' => '@=call(value.getCasualties, arguments({areaId: "Int!", raceId: "String!", dayStart: "Int", dayEnd: "Int", nameStartingWith: "String", planet: "PlanetInput", info: "@info", away: "Boolean", maxDistance: "Float"}, args))', 'complexity' => '@=childrenComplexity * 5', ], ], diff --git a/tests/Config/Parser/fixtures/annotations/Type/Battle.php b/tests/Config/Parser/fixtures/annotations/Type/Battle.php index 35b2909d6..2675cd995 100644 --- a/tests/Config/Parser/fixtures/annotations/Type/Battle.php +++ b/tests/Config/Parser/fixtures/annotations/Type/Battle.php @@ -4,6 +4,7 @@ namespace Overblog\GraphQLBundle\Tests\Config\Parser\fixtures\annotations\Type; +use GraphQL\Type\Definition\ResolveInfo; use Overblog\GraphQLBundle\Annotation as GQL; use Overblog\GraphQLBundle\Tests\Config\Parser\fixtures\annotations\Input\Planet; @@ -27,6 +28,7 @@ public function getCasualties( int $dayEnd = null, string $nameStartingWith = '', Planet $planet = null, + ResolveInfo $info = null, bool $away = false, float $maxDistance = null ): ?int { diff --git a/tests/Config/Parser/fixtures/annotations/Type/Sith.php b/tests/Config/Parser/fixtures/annotations/Type/Sith.php index de50d7551..0ece413a2 100644 --- a/tests/Config/Parser/fixtures/annotations/Type/Sith.php +++ b/tests/Config/Parser/fixtures/annotations/Type/Sith.php @@ -4,6 +4,7 @@ namespace Overblog\GraphQLBundle\Tests\Config\Parser\fixtures\annotations\Type; +use GraphQL\Type\Definition\ResolveInfo; use Overblog\GraphQLBundle\Annotation as GQL; use Overblog\GraphQLBundle\Tests\Config\Parser\fixtures\annotations\Union\Killable; @@ -45,4 +46,19 @@ public function getVictims(bool $jediOnly = false): array { return []; } + + /** + * @GQL\Field( + * type="[Character]", + * name="oldMasters", + * args={ + * @GQl\Arg(name="info", type="@info"), + * @GQL\Arg(name="jediOnly", type="Boolean", description="Only Jedi victims", default=false) + * } + * ) + */ + public function getOldMasters(ResolveInfo $info, bool $jediOnly = false): array + { + return []; + } } diff --git a/tests/Transformer/ArgumentsTransformerTest.php b/tests/Transformer/ArgumentsTransformerTest.php index 71ef0b398..c4d1dd1f4 100644 --- a/tests/Transformer/ArgumentsTransformerTest.php +++ b/tests/Transformer/ArgumentsTransformerTest.php @@ -187,6 +187,16 @@ public function testPopulating(): void $this->assertEquals('enum1', $res->field3->value); } + public function testSpecialArguments(): void + { + $transformer = $this->getTransformer([]); + $info = $this->getResolveInfo(self::getTypes()); + $res = $transformer->getArguments(['p1' => 'Int!', 'p2' => '@info'], ['p1' => 12], $info); + + $this->assertEquals($res[0], 12); + $this->assertEquals($res[1], $info); + } + public function testRaisedErrors(): void { $violation = new ConstraintViolation('validation_error', 'validation_error', [], 'invalid', 'field2', 'invalid');