thecodingmachine /
graphql-controllers
| 1 | <?php |
||
| 2 | |||
| 3 | |||
| 4 | namespace TheCodingMachine\GraphQL\Controllers; |
||
| 5 | |||
| 6 | use function get_class; |
||
| 7 | use GraphQL\Type\Definition\FieldDefinition; |
||
| 8 | use GraphQL\Type\Definition\IDType; |
||
| 9 | use GraphQL\Type\Definition\InputObjectType; |
||
| 10 | use GraphQL\Type\Definition\InputType; |
||
| 11 | use GraphQL\Type\Definition\ListOfType; |
||
| 12 | use GraphQL\Type\Definition\NonNull; |
||
| 13 | use GraphQL\Type\Definition\OutputType; |
||
| 14 | use GraphQL\Type\Definition\ScalarType; |
||
| 15 | use GraphQL\Type\Definition\Type; |
||
| 16 | use InvalidArgumentException; |
||
| 17 | use function is_array; |
||
| 18 | use TheCodingMachine\GraphQL\Controllers\Hydrators\HydratorInterface; |
||
| 19 | use TheCodingMachine\GraphQL\Controllers\Types\DateTimeType; |
||
| 20 | use TheCodingMachine\GraphQL\Controllers\Types\ID; |
||
| 21 | |||
| 22 | /** |
||
| 23 | * A GraphQL field that maps to a PHP method automatically. |
||
| 24 | */ |
||
| 25 | class QueryField extends FieldDefinition |
||
| 26 | { |
||
| 27 | /** |
||
| 28 | * QueryField constructor. |
||
| 29 | * @param string $name |
||
| 30 | * @param OutputType&Type $type |
||
| 31 | * @param array[] $arguments Indexed by argument name, value: ['type'=>InputType, 'defaultValue'=>val]. |
||
| 32 | * @param callable|null $resolve The method to execute |
||
| 33 | * @param string|null $targetMethodOnSource The name of the method to execute on the source object. Mutually exclusive with $resolve parameter. |
||
| 34 | * @param HydratorInterface $hydrator |
||
| 35 | * @param null|string $comment |
||
| 36 | * @param bool $injectSource Whether to inject the source object (for Fields), or null for Query and Mutations |
||
| 37 | * @param array $additionalConfig |
||
| 38 | */ |
||
| 39 | public function __construct(string $name, OutputType $type, array $arguments, ?callable $resolve, ?string $targetMethodOnSource, HydratorInterface $hydrator, ?string $comment, bool $injectSource, array $additionalConfig = []) |
||
| 40 | { |
||
| 41 | $config = [ |
||
| 42 | 'name' => $name, |
||
| 43 | 'type' => $type, |
||
| 44 | 'args' => array_map(function(array $item) { return $item['type']; }, $arguments) |
||
| 45 | ]; |
||
| 46 | if ($comment) { |
||
| 47 | $config['description'] = $comment; |
||
| 48 | } |
||
| 49 | |||
| 50 | $config['resolve'] = function ($source, array $args) use ($resolve, $targetMethodOnSource, $arguments, $injectSource, $hydrator) { |
||
| 51 | $toPassArgs = []; |
||
| 52 | if ($injectSource) { |
||
| 53 | $toPassArgs[] = $source; |
||
| 54 | } |
||
| 55 | foreach ($arguments as $name => $arr) { |
||
| 56 | $type = $arr['type']; |
||
| 57 | if (isset($args[$name])) { |
||
| 58 | $val = $this->castVal($args[$name], $type, $hydrator); |
||
| 59 | } elseif (array_key_exists('defaultValue', $arr)) { |
||
| 60 | $val = $arr['defaultValue']; |
||
| 61 | } else { |
||
| 62 | throw new GraphQLException("Expected argument '$name' was not provided."); |
||
| 63 | } |
||
| 64 | |||
| 65 | $toPassArgs[] = $val; |
||
| 66 | } |
||
| 67 | |||
| 68 | if ($resolve !== null) { |
||
| 69 | return $resolve(...$toPassArgs); |
||
| 70 | } |
||
| 71 | if ($targetMethodOnSource !== null) { |
||
| 72 | $method = [$source, $targetMethodOnSource]; |
||
| 73 | return $method(...$toPassArgs); |
||
| 74 | } |
||
| 75 | throw new \InvalidArgumentException('The QueryField constructor should be passed either a resolve method or a target method on source object.'); |
||
| 76 | }; |
||
| 77 | |||
| 78 | $config += $additionalConfig; |
||
| 79 | parent::__construct($config); |
||
| 80 | } |
||
| 81 | |||
| 82 | private function stripNonNullType(Type $type): Type |
||
| 83 | { |
||
| 84 | if ($type instanceof NonNull) { |
||
| 85 | return $this->stripNonNullType($type->getWrappedType()); |
||
| 86 | } |
||
| 87 | return $type; |
||
| 88 | } |
||
| 89 | |||
| 90 | /** |
||
| 91 | * Casts a value received from GraphQL into an argument passed to a method. |
||
| 92 | * |
||
| 93 | * @param mixed $val |
||
| 94 | * @param InputType $type |
||
| 95 | * @return mixed |
||
| 96 | */ |
||
| 97 | private function castVal($val, InputType $type, HydratorInterface $hydrator) |
||
| 98 | { |
||
| 99 | $type = $this->stripNonNullType($type); |
||
| 100 | if ($type instanceof ListOfType) { |
||
| 101 | if (!is_array($val)) { |
||
| 102 | throw new InvalidArgumentException('Expected GraphQL List but value passed is not an array.'); |
||
| 103 | } |
||
| 104 | return array_map(function($item) use ($type, $hydrator) { |
||
| 105 | return $this->castVal($item, $type->getWrappedType(), $hydrator); |
||
|
0 ignored issues
–
show
Bug
introduced
by
Loading history...
|
|||
| 106 | }, $val); |
||
| 107 | } elseif ($type instanceof DateTimeType) { |
||
| 108 | return new \DateTimeImmutable($val); |
||
| 109 | } elseif ($type instanceof IDType) { |
||
| 110 | return new ID($val); |
||
| 111 | } elseif ($type instanceof InputObjectType) { |
||
| 112 | return $hydrator->hydrate($val, $type); |
||
| 113 | } elseif (!$type instanceof ScalarType) { |
||
| 114 | throw new \RuntimeException('Unexpected type: '.get_class($type)); |
||
| 115 | } |
||
| 116 | return $val; |
||
| 117 | } |
||
| 118 | } |
||
| 119 |