1 | <?php |
||||
2 | /******************************************************************************* |
||||
3 | * This file is part of the GraphQL Bundle package. |
||||
4 | * |
||||
5 | * (c) YnloUltratech <[email protected]> |
||||
6 | * |
||||
7 | * For the full copyright and license information, please view the LICENSE |
||||
8 | * file that was distributed with this source code. |
||||
9 | ******************************************************************************/ |
||||
10 | |||||
11 | namespace Ynlo\GraphQLBundle\Resolver; |
||||
12 | |||||
13 | use Doctrine\Common\Util\ClassUtils; |
||||
14 | use Doctrine\ORM\EntityNotFoundException; |
||||
15 | use GraphQL\Type\Definition\ResolveInfo; |
||||
16 | use Symfony\Component\DependencyInjection\ContainerAwareInterface; |
||||
17 | use Symfony\Component\DependencyInjection\ContainerAwareTrait; |
||||
18 | use Symfony\Component\DependencyInjection\ContainerInterface; |
||||
19 | use Symfony\Component\EventDispatcher\EventDispatcherInterface; |
||||
20 | use Symfony\Component\PropertyAccess\PropertyAccessor; |
||||
21 | use Ynlo\GraphQLBundle\Component\AutoWire\AutoWire; |
||||
22 | use Ynlo\GraphQLBundle\Definition\ClassAwareDefinitionInterface; |
||||
23 | use Ynlo\GraphQLBundle\Definition\ExecutableDefinitionInterface; |
||||
24 | use Ynlo\GraphQLBundle\Definition\FieldDefinition; |
||||
25 | use Ynlo\GraphQLBundle\Definition\FieldsAwareDefinitionInterface; |
||||
26 | use Ynlo\GraphQLBundle\Definition\HasExtensionsInterface; |
||||
27 | use Ynlo\GraphQLBundle\Definition\Registry\Endpoint; |
||||
28 | use Ynlo\GraphQLBundle\Definition\UnionDefinition; |
||||
29 | use Ynlo\GraphQLBundle\Events\EventDispatcherAwareInterface; |
||||
30 | use Ynlo\GraphQLBundle\Exception\Controlled\BadRequestError; |
||||
31 | use Ynlo\GraphQLBundle\Exception\Controlled\NotFoundError; |
||||
32 | use Ynlo\GraphQLBundle\Extension\ExtensionInterface; |
||||
33 | use Ynlo\GraphQLBundle\Extension\ExtensionManager; |
||||
34 | use Ynlo\GraphQLBundle\Extension\ExtensionsAwareInterface; |
||||
35 | use Ynlo\GraphQLBundle\Model\NodeInterface; |
||||
36 | use Ynlo\GraphQLBundle\Subscription\AsynchronousJobInterface; |
||||
37 | use Ynlo\GraphQLBundle\Subscription\FilteredSubscriptionInterface; |
||||
38 | use Ynlo\GraphQLBundle\Subscription\Subscriber; |
||||
39 | use Ynlo\GraphQLBundle\Subscription\SubscriptionEvent; |
||||
40 | use Ynlo\GraphQLBundle\Subscription\SubscriptionRequest; |
||||
41 | use Ynlo\GraphQLBundle\Type\Types; |
||||
42 | use Ynlo\GraphQLBundle\Util\IDEncoder; |
||||
43 | |||||
44 | /** |
||||
45 | * This resolver act as a middleware between the executableDefinition and final resolvers. |
||||
46 | * Using injection of parameters can resolve the parameters needed by the final resolver before invoke |
||||
47 | */ |
||||
48 | class ResolverExecutor implements ContainerAwareInterface |
||||
49 | { |
||||
50 | use ContainerAwareTrait; |
||||
51 | |||||
52 | /** |
||||
53 | * @var Endpoint |
||||
54 | */ |
||||
55 | protected $endpoint; |
||||
56 | |||||
57 | /** |
||||
58 | * @var ExecutableDefinitionInterface |
||||
59 | */ |
||||
60 | protected $executableDefinition; |
||||
61 | |||||
62 | /** |
||||
63 | * @var mixed |
||||
64 | */ |
||||
65 | protected $root; |
||||
66 | |||||
67 | /** |
||||
68 | * @var ResolveInfo |
||||
69 | */ |
||||
70 | protected $resolveInfo; |
||||
71 | |||||
72 | /** |
||||
73 | * @var mixed |
||||
74 | */ |
||||
75 | protected $context; |
||||
76 | |||||
77 | /** |
||||
78 | * ResolverExecutor constructor. |
||||
79 | * |
||||
80 | * @param ContainerInterface $container |
||||
81 | * @param ExecutableDefinitionInterface $executableDefinition |
||||
82 | */ |
||||
83 | 1 | public function __construct(ContainerInterface $container, ExecutableDefinitionInterface $executableDefinition) |
|||
84 | { |
||||
85 | 1 | $this->container = $container; |
|||
86 | 1 | $this->executableDefinition = $executableDefinition; |
|||
87 | 1 | } |
|||
88 | |||||
89 | /** |
||||
90 | * @param mixed $root |
||||
91 | * @param array $args |
||||
92 | * @param ResolverContext $context |
||||
93 | * @param ResolveInfo $resolveInfo |
||||
94 | * |
||||
95 | * @return mixed |
||||
96 | * |
||||
97 | * @throws \Exception |
||||
98 | */ |
||||
99 | 1 | public function __invoke($root, array $args, ResolverContext $context, ResolveInfo $resolveInfo) |
|||
100 | { |
||||
101 | 1 | $this->root = $root; |
|||
102 | 1 | $this->resolveInfo = $resolveInfo; |
|||
103 | 1 | $this->endpoint = $context->getEndpoint(); |
|||
104 | |||||
105 | 1 | $resolverName = $this->executableDefinition->getResolver(); |
|||
106 | |||||
107 | 1 | $resolver = null; |
|||
108 | 1 | $refMethod = null; |
|||
109 | |||||
110 | 1 | if (class_exists($resolverName)) { |
|||
111 | 1 | $refClass = new \ReflectionClass($resolverName); |
|||
112 | |||||
113 | //Verify if exist a service with resolver name and use it |
||||
114 | //otherwise build the resolver using simple injection |
||||
115 | //@see Ynlo\GraphQLBundle\Component\AutoWire\AutoWire |
||||
116 | 1 | if ($this->container->has($resolverName)) { |
|||
117 | 1 | $resolver = $this->container->get($resolverName); |
|||
118 | } else { |
||||
119 | /** @var callable $resolver */ |
||||
120 | $resolver = $this->container->get(AutoWire::class)->createInstance($refClass->getName()); |
||||
121 | } |
||||
122 | |||||
123 | 1 | if ($resolver instanceof ContainerAwareInterface) { |
|||
124 | 1 | $resolver->setContainer($this->container); |
|||
125 | } |
||||
126 | |||||
127 | 1 | if ($refClass->hasMethod('__invoke')) { |
|||
128 | 1 | $refMethod = $refClass->getMethod('__invoke'); |
|||
129 | } |
||||
130 | } elseif (method_exists($root, $resolverName)) { |
||||
131 | $resolver = $root; |
||||
132 | $refMethod = new \ReflectionMethod(ClassUtils::getClass($root), $resolverName); |
||||
133 | } |
||||
134 | |||||
135 | /** @var SubscriptionRequest|null $subscriptionRequest */ |
||||
136 | 1 | $subscriptionRequest = $context->getMeta('subscriptionRequest'); |
|||
137 | |||||
138 | 1 | $isSubscriptionSubscribe = $resolveInfo->operation->operation === 'subscription' && !$subscriptionRequest && !$resolver instanceof EmptyObjectResolver; |
|||
139 | 1 | $isSubscriptionRequest = $resolveInfo->operation->operation === 'subscription' && $subscriptionRequest && !$resolver instanceof EmptyObjectResolver; |
|||
140 | |||||
141 | 1 | if ($resolver && $refMethod) { |
|||
142 | 1 | $this->context = ContextBuilder::create($context->getEndpoint()) |
|||
143 | 1 | ->setRoot($root) |
|||
144 | 1 | ->setResolveInfo($resolveInfo) |
|||
145 | 1 | ->setArgs($args) |
|||
146 | 1 | ->setMetas($context->getMetas()) |
|||
147 | 1 | ->setDefinition($this->executableDefinition) |
|||
148 | 1 | ->build(); |
|||
149 | |||||
150 | 1 | if ($resolver instanceof ResolverInterface) { |
|||
151 | 1 | $resolver->setContext($this->context); |
|||
152 | } |
||||
153 | |||||
154 | 1 | $node = $this->context->getNode(); |
|||
155 | 1 | if ($resolver instanceof ExtensionsAwareInterface && $node instanceof HasExtensionsInterface) { |
|||
156 | 1 | $resolver->setExtensions($this->resolveObjectExtensions($node)); |
|||
157 | } |
||||
158 | |||||
159 | 1 | if ($resolver instanceof EventDispatcherAwareInterface) { |
|||
160 | 1 | $resolver->setEventDispatcher($this->container->get(EventDispatcherInterface::class)); |
|||
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||||
161 | } |
||||
162 | |||||
163 | 1 | if ($subscriptionRequest) { |
|||
164 | $args = array_merge($args, $subscriptionRequest->getData()); |
||||
165 | } |
||||
166 | |||||
167 | 1 | $params = $this->prepareMethodParameters($refMethod, $args, !$subscriptionRequest); |
|||
168 | |||||
169 | // resolve filters |
||||
170 | 1 | $filters = []; |
|||
171 | 1 | if ($isSubscriptionSubscribe && $resolver instanceof FilteredSubscriptionInterface) { |
|||
172 | $filterMethod = new \ReflectionMethod(get_class($resolver), 'getFilters'); |
||||
173 | $filters = $filterMethod->invoke($resolver); |
||||
174 | } |
||||
175 | |||||
176 | // call the async queue |
||||
177 | 1 | if ($isSubscriptionSubscribe && $resolver instanceof AsynchronousJobInterface) { |
|||
178 | $asyncMethod = new \ReflectionMethod(get_class($resolver), 'onSubscribe'); |
||||
179 | $asyncMethod->invokeArgs($resolver, $params); |
||||
180 | } |
||||
181 | |||||
182 | 1 | if ($isSubscriptionSubscribe) { |
|||
183 | $resolver = $this->container->get(Subscriber::class); |
||||
184 | $result = $resolver->__invoke($this->context, array_merge($this->normalizeArguments($args), $filters)); |
||||
185 | } else { |
||||
186 | 1 | $result = $refMethod->invokeArgs($resolver, $params); |
|||
0 ignored issues
–
show
It seems like
$resolver can also be of type callable ; however, parameter $object of ReflectionMethod::invokeArgs() does only seem to accept object , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
187 | } |
||||
188 | |||||
189 | 1 | if ($isSubscriptionRequest) { |
|||
190 | if (!$result) { |
||||
191 | exit; |
||||
0 ignored issues
–
show
|
|||||
192 | } |
||||
193 | |||||
194 | $subscriptionUnion = $this->endpoint->getType($this->executableDefinition->getType()); |
||||
195 | $result = new SubscriptionEvent($result); |
||||
196 | if ($subscriptionUnion instanceof UnionDefinition) { |
||||
197 | $result->setConcreteType(array_values($subscriptionUnion->getTypes())[0]->getType()); |
||||
198 | } |
||||
199 | } |
||||
200 | |||||
201 | 1 | return $result; |
|||
202 | } |
||||
203 | |||||
204 | $error = sprintf('The resolver "%s" for "%s" is not a valid resolver. Resolvers should have a method "__invoke(...)"', $resolverName, $this->executableDefinition->getName()); |
||||
205 | throw new \RuntimeException($error); |
||||
206 | } |
||||
207 | |||||
208 | /** |
||||
209 | * @param HasExtensionsInterface $objectDefinition |
||||
210 | * |
||||
211 | * @return ExtensionInterface[] |
||||
212 | */ |
||||
213 | 1 | private function resolveObjectExtensions(HasExtensionsInterface $objectDefinition): array |
|||
214 | { |
||||
215 | 1 | $extensions = []; |
|||
216 | |||||
217 | //get all extensions registered as services |
||||
218 | 1 | $registeredExtensions = $this->container->get(ExtensionManager::class)->getExtensions(); |
|||
219 | 1 | foreach ($registeredExtensions as $registeredExtension) { |
|||
220 | foreach ($objectDefinition->getExtensions() as $extensionDefinition) { |
||||
221 | $extensionClass = $extensionDefinition->getClass(); |
||||
222 | if (\get_class($registeredExtension) === $extensionClass) { |
||||
223 | $extensions[$extensionClass] = $registeredExtension; |
||||
224 | } |
||||
225 | } |
||||
226 | } |
||||
227 | |||||
228 | //get all extensions not registered as services |
||||
229 | 1 | foreach ($objectDefinition->getExtensions() as $extensionDefinition) { |
|||
230 | $class = $extensionDefinition->getClass(); |
||||
231 | if (!isset($extensions[$class])) { |
||||
232 | $instance = new $class(); |
||||
233 | if ($instance instanceof ContainerAwareInterface) { |
||||
234 | $instance->setContainer($this->container); |
||||
235 | } |
||||
236 | |||||
237 | $extensions[$class] = $instance; |
||||
238 | } |
||||
239 | } |
||||
240 | |||||
241 | 1 | return array_values($extensions); |
|||
242 | } |
||||
243 | |||||
244 | /** |
||||
245 | * @param \ReflectionMethod $refMethod |
||||
246 | * @param array $args |
||||
247 | * @param bool $removeUnknownArguments |
||||
248 | * |
||||
249 | * @throws \Exception |
||||
250 | * |
||||
251 | * @return array |
||||
252 | */ |
||||
253 | 1 | private function prepareMethodParameters(\ReflectionMethod $refMethod, array $args, $removeUnknownArguments = true): array |
|||
254 | { |
||||
255 | 1 | $normalizedArguments = $this->normalizeArguments($args); |
|||
256 | 1 | if (!$removeUnknownArguments) { |
|||
257 | $normalizedArguments = array_merge($args, $normalizedArguments); |
||||
258 | } |
||||
259 | 1 | $normalizedArguments['args'] = $normalizedArguments; |
|||
260 | 1 | $indexedArguments = $this->resolveMethodArguments($refMethod, $normalizedArguments); |
|||
261 | 1 | ksort($indexedArguments); |
|||
262 | |||||
263 | 1 | return $indexedArguments; |
|||
264 | } |
||||
265 | |||||
266 | /** |
||||
267 | * @param array $args |
||||
268 | * |
||||
269 | * @return array |
||||
270 | */ |
||||
271 | 1 | protected function normalizeArguments(array $args) |
|||
272 | { |
||||
273 | //normalize arguments |
||||
274 | 1 | $normalizedArguments = []; |
|||
275 | 1 | foreach ($args as $key => $value) { |
|||
276 | 1 | if ($this->executableDefinition->hasArgument($key)) { |
|||
277 | 1 | $argument = $this->executableDefinition->getArgument($key); |
|||
278 | 1 | if ('input' === $key) { |
|||
279 | $normalizedValue = $value; |
||||
280 | } else { |
||||
281 | 1 | $normalizedValue = $this->normalizeValue($value, $argument->getType()); |
|||
282 | |||||
283 | //normalize argument into respective inputs objects |
||||
284 | 1 | if (\is_array($normalizedValue) && $this->endpoint->hasType($argument->getType())) { |
|||
285 | 1 | if ($argument->isList()) { |
|||
286 | 1 | $tmp = []; |
|||
287 | 1 | foreach ($normalizedValue as $childValue) { |
|||
288 | 1 | $tmp[] = $this->arrayToObject($childValue, $this->endpoint->getType($argument->getType())); |
|||
289 | } |
||||
290 | 1 | $normalizedValue = $tmp; |
|||
291 | } else { |
||||
292 | 1 | $normalizedValue = $this->arrayToObject($normalizedValue, $this->endpoint->getType($argument->getType())); |
|||
293 | } |
||||
294 | } |
||||
295 | } |
||||
296 | 1 | $normalizedArguments[$argument->getName()] = $normalizedValue; |
|||
297 | 1 | $normalizedArguments[$argument->getInternalName()] = $normalizedValue; |
|||
298 | } |
||||
299 | } |
||||
300 | |||||
301 | 1 | return $normalizedArguments; |
|||
302 | } |
||||
303 | |||||
304 | /** |
||||
305 | * @param \ReflectionMethod $method |
||||
306 | * @param array $incomeArgs |
||||
307 | * |
||||
308 | * @return array |
||||
309 | */ |
||||
310 | 1 | private function resolveMethodArguments(\ReflectionMethod $method, array $incomeArgs) |
|||
311 | { |
||||
312 | 1 | $orderedArguments = []; |
|||
313 | 1 | foreach ($method->getParameters() as $parameter) { |
|||
314 | 1 | if ($parameter->isOptional()) { |
|||
315 | 1 | $orderedArguments[$parameter->getPosition()] = $parameter->getDefaultValue(); |
|||
316 | } |
||||
317 | 1 | foreach ($incomeArgs as $key => $value) { |
|||
318 | 1 | if ($parameter->getName() === $key) { |
|||
319 | 1 | $orderedArguments[$parameter->getPosition()] = $value; |
|||
320 | 1 | continue 2; |
|||
321 | } |
||||
322 | } |
||||
323 | |||||
324 | //inject root common argument |
||||
325 | 1 | if ($this->root && !isset($incomeArgs['root']) && 'root' === $parameter->getName()) { |
|||
326 | 1 | $orderedArguments[$parameter->getPosition()] = $this->root; |
|||
327 | } |
||||
328 | |||||
329 | //inject context common argument |
||||
330 | 1 | if ($this->context |
|||
331 | 1 | && 'context' === $parameter->getName() |
|||
332 | 1 | && $parameter->getClass() |
|||
333 | 1 | && is_a($parameter->getClass()->getName(), ResolverContext::class, true) |
|||
334 | ) { |
||||
335 | 1 | $orderedArguments[$parameter->getPosition()] = $this->context; |
|||
336 | } |
||||
337 | } |
||||
338 | |||||
339 | // verify required arguments and types |
||||
340 | 1 | foreach ($method->getParameters() as $index => $parameter) { |
|||
341 | 1 | $givenValue = $orderedArguments[$index] ?? null; |
|||
342 | // does not allow null on required arguments |
||||
343 | 1 | if (null === $givenValue && !$parameter->allowsNull()) { |
|||
344 | throw new BadRequestError(); |
||||
345 | } |
||||
346 | } |
||||
347 | |||||
348 | 1 | return $orderedArguments; |
|||
349 | } |
||||
350 | |||||
351 | /** |
||||
352 | * @param mixed $value |
||||
353 | * @param string $type |
||||
354 | * |
||||
355 | * @return mixed |
||||
356 | */ |
||||
357 | 1 | private function normalizeValue($value, string $type) |
|||
358 | { |
||||
359 | 1 | if (Types::ID === $type && $value) { |
|||
360 | 1 | if (\is_array($value)) { |
|||
361 | 1 | $idsArray = []; |
|||
362 | 1 | foreach ($value as $id) { |
|||
363 | 1 | if ($id) { |
|||
364 | 1 | $idsArray[] = $this->decodeID($id); |
|||
365 | } |
||||
366 | } |
||||
367 | 1 | $value = $idsArray; |
|||
368 | } else { |
||||
369 | 1 | $value = $this->decodeID($value); |
|||
370 | } |
||||
371 | } |
||||
372 | |||||
373 | 1 | return $value; |
|||
374 | } |
||||
375 | |||||
376 | /** |
||||
377 | * @param string $globalId |
||||
378 | * |
||||
379 | * @return NodeInterface |
||||
380 | */ |
||||
381 | 1 | private function decodeID(string $globalId): NodeInterface |
|||
382 | { |
||||
383 | try { |
||||
384 | 1 | $node = IDEncoder::decode($globalId); |
|||
385 | 1 | if (!$node) { |
|||
386 | throw new EntityNotFoundException(); |
||||
387 | } |
||||
388 | |||||
389 | 1 | return $node; |
|||
390 | } catch (EntityNotFoundException $exception) { |
||||
391 | throw new NotFoundError(sprintf('The given Node "%s" does not exists.', $globalId)); |
||||
392 | } |
||||
393 | } |
||||
394 | |||||
395 | /** |
||||
396 | * Convert a array into object using given definition |
||||
397 | * |
||||
398 | * @param array $data data to populate the object |
||||
399 | * @param FieldsAwareDefinitionInterface $definition object definition |
||||
400 | * |
||||
401 | * @return mixed |
||||
402 | */ |
||||
403 | 1 | private function arrayToObject(array $data, FieldsAwareDefinitionInterface $definition) |
|||
404 | { |
||||
405 | 1 | $class = null; |
|||
406 | 1 | if ($definition instanceof ClassAwareDefinitionInterface) { |
|||
407 | 1 | $class = $definition->getClass(); |
|||
408 | } |
||||
409 | |||||
410 | //normalize data |
||||
411 | 1 | foreach ($data as $fieldName => &$value) { |
|||
412 | 1 | if (!$definition->hasField($fieldName)) { |
|||
413 | continue; |
||||
414 | } |
||||
415 | 1 | $fieldDefinition = $definition->getField($fieldName); |
|||
416 | 1 | $value = $this->normalizeValue($value, $fieldDefinition->getType()); |
|||
417 | } |
||||
418 | 1 | unset($value); |
|||
419 | |||||
420 | //instantiate object |
||||
421 | 1 | if (class_exists($class)) { |
|||
422 | 1 | $object = new $class(); |
|||
423 | } else { |
||||
424 | 1 | $object = $data; |
|||
425 | } |
||||
426 | |||||
427 | //populate object |
||||
428 | 1 | foreach ($data as $key => $value) { |
|||
429 | 1 | if (!$definition->hasField($key)) { |
|||
430 | continue; |
||||
431 | } |
||||
432 | 1 | $fieldDefinition = $definition->getField($key); |
|||
433 | |||||
434 | 1 | if (\is_array($value) && $this->endpoint->hasType($fieldDefinition->getType())) { |
|||
435 | 1 | $childType = $this->endpoint->getType($fieldDefinition->getType()); |
|||
436 | 1 | if ($childType instanceof FieldsAwareDefinitionInterface) { |
|||
437 | 1 | $value = $this->arrayToObject($value, $childType); |
|||
438 | } |
||||
439 | } |
||||
440 | |||||
441 | 1 | $this->setObjectValue($object, $fieldDefinition, $value); |
|||
442 | } |
||||
443 | |||||
444 | 1 | return $object; |
|||
445 | } |
||||
446 | |||||
447 | /** |
||||
448 | * @param mixed $object |
||||
449 | * @param FieldDefinition $fieldDefinition |
||||
450 | * @param mixed $value |
||||
451 | * |
||||
452 | * @throws \ReflectionException |
||||
453 | */ |
||||
454 | 1 | private function setObjectValue(&$object, FieldDefinition $fieldDefinition, $value): void |
|||
455 | { |
||||
456 | //using setter |
||||
457 | 1 | $accessor = new PropertyAccessor(); |
|||
458 | 1 | $propertyName = $fieldDefinition->getOriginName(); |
|||
459 | 1 | if (\is_array($object)) { |
|||
460 | 1 | $object[$propertyName ?? $fieldDefinition->getName()] = $value; |
|||
461 | } else { |
||||
462 | 1 | if ($accessor->isWritable($object, $propertyName)) { |
|||
463 | 1 | $accessor->setValue($object, $propertyName, $value); |
|||
464 | } else { |
||||
465 | //using reflection |
||||
466 | $refClass = new \ReflectionClass(\get_class($object)); |
||||
467 | if ($refClass->hasProperty($object) && $property = $refClass->getProperty($object)) { |
||||
468 | $property->setAccessible(true); |
||||
469 | $property->setValue($object, $value); |
||||
470 | } |
||||
471 | } |
||||
472 | } |
||||
473 | 1 | } |
|||
474 | } |
||||
475 |