ResolverExecutor::__invoke()   F
last analyzed

Complexity

Conditions 25
Paths > 20000

Size

Total Lines 107
Code Lines 62

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 42
CRAP Score 43.8909

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 62
c 3
b 0
f 0
dl 0
loc 107
ccs 42
cts 61
cp 0.6885
rs 0
cc 25
nc 46170
nop 4
crap 43.8909

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
It seems like $this->container->get(Sy...atcherInterface::class) can also be of type null; however, parameter $eventDispatcher of Ynlo\GraphQLBundle\Event...e::setEventDispatcher() does only seem to accept Symfony\Component\EventD...ventDispatcherInterface, 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 ignore-type  annotation

160
                $resolver->setEventDispatcher(/** @scrutinizer ignore-type */ $this->container->get(EventDispatcherInterface::class));
Loading history...
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
Bug introduced by
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 ignore-type  annotation

186
                $result = $refMethod->invokeArgs(/** @scrutinizer ignore-type */ $resolver, $params);
Loading history...
187
            }
188
189 1
            if ($isSubscriptionRequest) {
190
                if (!$result) {
191
                    exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
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