Completed
Push — master ( eee93d...ab3d31 )
by Rafael
04:38
created

ResolverExecutor::decodeID()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 3.7085

Importance

Changes 0
Metric Value
eloc 7
dl 0
loc 11
ccs 4
cts 7
cp 0.5714
rs 10
c 0
b 0
f 0
cc 3
nc 3
nop 1
crap 3.7085
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));
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
            // does not allow different class to expected class
348 1
            $expectedType = $parameter->getType();
349 1
            if ($expectedType && !$expectedType->isBuiltin() && !is_a($givenValue, $expectedType->getName(), true)) {
350
                throw new BadRequestError();
351
            }
352
353
            // does not allow different scalar type
354 1
            if ($expectedType && $expectedType->isBuiltin() && gettype($givenValue) !== $expectedType->getName()) {
355 1
                throw new BadRequestError();
356
            }
357
        }
358
359 1
        return $orderedArguments;
360
    }
361
362
    /**
363
     * @param mixed  $value
364
     * @param string $type
365
     *
366
     * @return mixed
367
     */
368 1
    private function normalizeValue($value, string $type)
369
    {
370 1
        if (Types::ID === $type && $value) {
371 1
            if (\is_array($value)) {
372 1
                $idsArray = [];
373 1
                foreach ($value as $id) {
374 1
                    if ($id) {
375 1
                        $idsArray[] = $this->decodeID($id);
376
                    }
377
                }
378 1
                $value = $idsArray;
379
            } else {
380 1
                $value = $this->decodeID($value);
381
            }
382
        }
383
384 1
        return $value;
385
    }
386
387
    /**
388
     * @param string $globalId
389
     *
390
     * @return NodeInterface
391
     */
392 1
    private function decodeID(string $globalId): NodeInterface
393
    {
394
        try {
395 1
            $node = IDEncoder::decode($globalId);
396 1
            if (!$node) {
397
                throw new EntityNotFoundException();
398
            }
399
400 1
            return $node;
401
        } catch (EntityNotFoundException $exception) {
402
            throw new NotFoundError(sprintf('The given Node "%s" does not exists.', $globalId));
403
        }
404
    }
405
406
    /**
407
     * Convert a array into object using given definition
408
     *
409
     * @param array                          $data       data to populate the object
410
     * @param FieldsAwareDefinitionInterface $definition object definition
411
     *
412
     * @return mixed
413
     */
414 1
    private function arrayToObject(array $data, FieldsAwareDefinitionInterface $definition)
415
    {
416 1
        $class = null;
417 1
        if ($definition instanceof ClassAwareDefinitionInterface) {
418 1
            $class = $definition->getClass();
419
        }
420
421
        //normalize data
422 1
        foreach ($data as $fieldName => &$value) {
423 1
            if (!$definition->hasField($fieldName)) {
424
                continue;
425
            }
426 1
            $fieldDefinition = $definition->getField($fieldName);
427 1
            $value = $this->normalizeValue($value, $fieldDefinition->getType());
428
        }
429 1
        unset($value);
430
431
        //instantiate object
432 1
        if (class_exists($class)) {
433 1
            $object = new $class();
434
        } else {
435 1
            $object = $data;
436
        }
437
438
        //populate object
439 1
        foreach ($data as $key => $value) {
440 1
            if (!$definition->hasField($key)) {
441
                continue;
442
            }
443 1
            $fieldDefinition = $definition->getField($key);
444
445 1
            if (\is_array($value) && $this->endpoint->hasType($fieldDefinition->getType())) {
446 1
                $childType = $this->endpoint->getType($fieldDefinition->getType());
447 1
                if ($childType instanceof FieldsAwareDefinitionInterface) {
448 1
                    $value = $this->arrayToObject($value, $childType);
449
                }
450
            }
451
452 1
            $this->setObjectValue($object, $fieldDefinition, $value);
453
        }
454
455 1
        return $object;
456
    }
457
458
    /**
459
     * @param mixed           $object
460
     * @param FieldDefinition $fieldDefinition
461
     * @param mixed           $value
462
     *
463
     * @throws \ReflectionException
464
     */
465 1
    private function setObjectValue(&$object, FieldDefinition $fieldDefinition, $value): void
466
    {
467
        //using setter
468 1
        $accessor = new PropertyAccessor();
469 1
        $propertyName = $fieldDefinition->getOriginName();
470 1
        if (\is_array($object)) {
471 1
            $object[$propertyName ?? $fieldDefinition->getName()] = $value;
472
        } else {
473 1
            if ($accessor->isWritable($object, $propertyName)) {
474 1
                $accessor->setValue($object, $propertyName, $value);
475
            } else {
476
                //using reflection
477
                $refClass = new \ReflectionClass(\get_class($object));
478
                if ($refClass->hasProperty($object) && $property = $refClass->getProperty($object)) {
479
                    $property->setAccessible(true);
480
                    $property->setValue($object, $value);
481
                }
482
            }
483
        }
484 1
    }
485
}
486