Completed
Pull Request — master (#13)
by Rafael
06:46
created

ResolverExecutor::normalizeArguments()   B

Complexity

Conditions 8
Paths 6

Size

Total Lines 31
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 8.0109

Importance

Changes 0
Metric Value
eloc 19
dl 0
loc 31
ccs 17
cts 18
cp 0.9444
rs 8.4444
c 0
b 0
f 0
cc 8
nc 6
nop 1
crap 8.0109
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 GraphQL\Type\Definition\ResolveInfo;
15
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
16
use Symfony\Component\DependencyInjection\ContainerAwareTrait;
17
use Symfony\Component\DependencyInjection\ContainerInterface;
18
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
19
use Symfony\Component\PropertyAccess\PropertyAccessor;
20
use Ynlo\GraphQLBundle\Component\AutoWire\AutoWire;
21
use Ynlo\GraphQLBundle\Definition\ClassAwareDefinitionInterface;
22
use Ynlo\GraphQLBundle\Definition\ExecutableDefinitionInterface;
23
use Ynlo\GraphQLBundle\Definition\FieldDefinition;
24
use Ynlo\GraphQLBundle\Definition\FieldsAwareDefinitionInterface;
25
use Ynlo\GraphQLBundle\Definition\HasExtensionsInterface;
26
use Ynlo\GraphQLBundle\Definition\Registry\Endpoint;
27
use Ynlo\GraphQLBundle\Definition\UnionDefinition;
28
use Ynlo\GraphQLBundle\Events\EventDispatcherAwareInterface;
29
use Ynlo\GraphQLBundle\Extension\ExtensionInterface;
30
use Ynlo\GraphQLBundle\Extension\ExtensionManager;
31
use Ynlo\GraphQLBundle\Extension\ExtensionsAwareInterface;
32
use Ynlo\GraphQLBundle\Subscription\AsynchronousJobInterface;
33
use Ynlo\GraphQLBundle\Subscription\FilteredSubscriptionInterface;
34
use Ynlo\GraphQLBundle\Subscription\Subscriber;
35
use Ynlo\GraphQLBundle\Subscription\SubscriptionEvent;
36
use Ynlo\GraphQLBundle\Subscription\SubscriptionRequest;
37
use Ynlo\GraphQLBundle\Type\Types;
38
use Ynlo\GraphQLBundle\Util\IDEncoder;
39
40
/**
41
 * This resolver act as a middleware between the executableDefinition and final resolvers.
42
 * Using injection of parameters can resolve the parameters needed by the final resolver before invoke
43
 */
44
class ResolverExecutor implements ContainerAwareInterface
45
{
46
    use ContainerAwareTrait;
47
48
    /**
49
     * @var Endpoint
50
     */
51
    protected $endpoint;
52
53
    /**
54
     * @var ExecutableDefinitionInterface
55
     */
56
    protected $executableDefinition;
57
58
    /**
59
     * @var mixed
60
     */
61
    protected $root;
62
63
    /**
64
     * @var ResolveInfo
65
     */
66
    protected $resolveInfo;
67
68
    /**
69
     * @var mixed
70
     */
71
    protected $context;
72
73
    /**
74
     * ResolverExecutor constructor.
75
     *
76
     * @param ContainerInterface            $container
77
     * @param ExecutableDefinitionInterface $executableDefinition
78
     */
79 1
    public function __construct(ContainerInterface $container, ExecutableDefinitionInterface $executableDefinition)
80
    {
81 1
        $this->container = $container;
82 1
        $this->executableDefinition = $executableDefinition;
83 1
    }
84
85
    /**
86
     * @param mixed           $root
87
     * @param array           $args
88
     * @param ResolverContext $context
89
     * @param ResolveInfo     $resolveInfo
90
     *
91
     * @return mixed
92
     *
93
     * @throws \Exception
94
     */
95 1
    public function __invoke($root, array $args, ResolverContext $context, ResolveInfo $resolveInfo)
96
    {
97 1
        $this->root = $root;
98 1
        $this->resolveInfo = $resolveInfo;
99 1
        $this->endpoint = $context->getEndpoint();
100
101 1
        $resolverName = $this->executableDefinition->getResolver();
102
103 1
        $resolver = null;
104 1
        $refMethod = null;
105
106 1
        if (class_exists($resolverName)) {
107 1
            $refClass = new \ReflectionClass($resolverName);
108
109
            //Verify if exist a service with resolver name and use it
110
            //otherwise build the resolver using simple injection
111
            //@see Ynlo\GraphQLBundle\Component\AutoWire\AutoWire
112 1
            if ($this->container->has($resolverName)) {
113 1
                $resolver = $this->container->get($resolverName);
114
            } else {
115
                /** @var callable $resolver */
116
                $resolver = $this->container->get(AutoWire::class)->createInstance($refClass->getName());
117
            }
118
119 1
            if ($resolver instanceof ContainerAwareInterface) {
120 1
                $resolver->setContainer($this->container);
121
            }
122
123 1
            if ($refClass->hasMethod('__invoke')) {
124 1
                $refMethod = $refClass->getMethod('__invoke');
125
            }
126
        } elseif (method_exists($root, $resolverName)) {
127
            $resolver = $root;
128
            $refMethod = new \ReflectionMethod(ClassUtils::getClass($root), $resolverName);
129
        }
130
131
        /** @var SubscriptionRequest|null $subscriptionRequest */
132 1
        $subscriptionRequest = $context->getMeta('subscriptionRequest');
133
134 1
        $isSubscriptionSubscribe = $resolveInfo->operation->operation === 'subscription' && !$subscriptionRequest && !$resolver instanceof EmptyObjectResolver;
135 1
        $isSubscriptionRequest = $resolveInfo->operation->operation === 'subscription' && $subscriptionRequest && !$resolver instanceof EmptyObjectResolver;
136
137 1
        if ($resolver && $refMethod) {
138 1
            $this->context = ContextBuilder::create($context->getEndpoint())
139 1
                                           ->setRoot($root)
140 1
                                           ->setResolveInfo($resolveInfo)
141 1
                                           ->setArgs($args)
142 1
                                           ->setMetas($context->getMetas())
143 1
                                           ->setDefinition($this->executableDefinition)
144 1
                                           ->build();
145
146 1
            if ($resolver instanceof ResolverInterface) {
147 1
                $resolver->setContext($this->context);
148
            }
149
150 1
            $node = $this->context->getNode();
151 1
            if ($resolver instanceof ExtensionsAwareInterface && $node instanceof HasExtensionsInterface) {
152 1
                $resolver->setExtensions($this->resolveObjectExtensions($node));
153
            }
154
155 1
            if ($resolver instanceof EventDispatcherAwareInterface) {
156 1
                $resolver->setEventDispatcher($this->container->get(EventDispatcherInterface::class));
157
            }
158
159 1
            if ($subscriptionRequest) {
160
                $args = array_merge($args, $subscriptionRequest->getData());
161
            }
162
163 1
            $params = $this->prepareMethodParameters($refMethod, $args, !$subscriptionRequest);
164
165
            // resolve filters
166 1
            $filters = [];
167 1
            if ($isSubscriptionSubscribe && $resolver instanceof FilteredSubscriptionInterface) {
168
                $filterMethod = new \ReflectionMethod(get_class($resolver), 'getFilters');
169
                $filters = $filterMethod->invoke($resolver);
170
            }
171
172
            // call the async queue
173 1
            if ($isSubscriptionSubscribe && $resolver instanceof AsynchronousJobInterface) {
174
                $asyncMethod = new \ReflectionMethod(get_class($resolver), 'onSubscribe');
175
                $asyncMethod->invokeArgs($resolver, $params);
176
            }
177
178 1
            if ($isSubscriptionSubscribe) {
179
                $resolver = $this->container->get(Subscriber::class);
180
                $result = $resolver->__invoke($this->context, array_merge($this->normalizeArguments($args), $filters));
181
            } else {
182 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

182
                $result = $refMethod->invokeArgs(/** @scrutinizer ignore-type */ $resolver, $params);
Loading history...
183
            }
184
185 1
            if ($isSubscriptionRequest) {
186
                if (!$result) {
187
                    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...
188
                }
189
190
                $subscriptionUnion = $this->endpoint->getType($this->executableDefinition->getType());
191
                $result = new SubscriptionEvent($result);
192
                if ($subscriptionUnion instanceof UnionDefinition) {
193
                    $result->setConcreteType(array_values($subscriptionUnion->getTypes())[0]->getType());
194
                }
195
            }
196
197 1
            return $result;
198
        }
199
200
        $error = sprintf('The resolver "%s" for "%s" is not a valid resolver. Resolvers should have a method "__invoke(...)"', $resolverName, $this->executableDefinition->getName());
201
        throw new \RuntimeException($error);
202
    }
203
204
    /**
205
     * @param HasExtensionsInterface $objectDefinition
206
     *
207
     * @return ExtensionInterface[]
208
     */
209 1
    private function resolveObjectExtensions(HasExtensionsInterface $objectDefinition): array
210
    {
211 1
        $extensions = [];
212
213
        //get all extensions registered as services
214 1
        $registeredExtensions = $this->container->get(ExtensionManager::class)->getExtensions();
215 1
        foreach ($registeredExtensions as $registeredExtension) {
216
            foreach ($objectDefinition->getExtensions() as $extensionDefinition) {
217
                $extensionClass = $extensionDefinition->getClass();
218
                if (\get_class($registeredExtension) === $extensionClass) {
219
                    $extensions[$extensionClass] = $registeredExtension;
220
                }
221
            }
222
        }
223
224
        //get all extensions not registered as services
225 1
        foreach ($objectDefinition->getExtensions() as $extensionDefinition) {
226
            $class = $extensionDefinition->getClass();
227
            if (!isset($extensions[$class])) {
228
                $instance = new $class();
229
                if ($instance instanceof ContainerAwareInterface) {
230
                    $instance->setContainer($this->container);
231
                }
232
233
                $extensions[$class] = $instance;
234
            }
235
        }
236
237 1
        return array_values($extensions);
238
    }
239
240
    /**
241
     * @param \ReflectionMethod $refMethod
242
     * @param array             $args
243
     * @param bool              $removeUnknownArguments
244
     *
245
     * @throws \Exception
246
     *
247
     * @return array
248
     */
249 1
    private function prepareMethodParameters(\ReflectionMethod $refMethod, array $args, $removeUnknownArguments = true): array
250
    {
251 1
        $normalizedArguments = $this->normalizeArguments($args);
252 1
        if (!$removeUnknownArguments) {
253
            $normalizedArguments = array_merge($args, $normalizedArguments);
254
        }
255 1
        $normalizedArguments['args'] = $normalizedArguments;
256 1
        $indexedArguments = $this->resolveMethodArguments($refMethod, $normalizedArguments);
257 1
        ksort($indexedArguments);
258
259 1
        return $indexedArguments;
260
    }
261
262
    /**
263
     * @param array $args
264
     *
265
     * @return array
266
     */
267 1
    protected function normalizeArguments(array $args)
268
    {
269
        //normalize arguments
270 1
        $normalizedArguments = [];
271 1
        foreach ($args as $key => $value) {
272 1
            if ($this->executableDefinition->hasArgument($key)) {
273 1
                $argument = $this->executableDefinition->getArgument($key);
274 1
                if ('input' === $key) {
275
                    $normalizedValue = $value;
276
                } else {
277 1
                    $normalizedValue = $this->normalizeValue($value, $argument->getType());
278
279
                    //normalize argument into respective inputs objects
280 1
                    if (\is_array($normalizedValue) && $this->endpoint->hasType($argument->getType())) {
281 1
                        if ($argument->isList()) {
282 1
                            $tmp = [];
283 1
                            foreach ($normalizedValue as $childValue) {
284 1
                                $tmp[] = $this->arrayToObject($childValue, $this->endpoint->getType($argument->getType()));
285
                            }
286 1
                            $normalizedValue = $tmp;
287
                        } else {
288 1
                            $normalizedValue = $this->arrayToObject($normalizedValue, $this->endpoint->getType($argument->getType()));
289
                        }
290
                    }
291
                }
292 1
                $normalizedArguments[$argument->getName()] = $normalizedValue;
293 1
                $normalizedArguments[$argument->getInternalName()] = $normalizedValue;
294
            }
295
        }
296
297 1
        return $normalizedArguments;
298
    }
299
300
    /**
301
     * @param \ReflectionMethod $method
302
     * @param array             $incomeArgs
303
     *
304
     * @return array
305
     */
306 1
    private function resolveMethodArguments(\ReflectionMethod $method, array $incomeArgs)
307
    {
308 1
        $orderedArguments = [];
309 1
        foreach ($method->getParameters() as $parameter) {
310 1
            if ($parameter->isOptional()) {
311 1
                $orderedArguments[$parameter->getPosition()] = $parameter->getDefaultValue();
312
            }
313 1
            foreach ($incomeArgs as $key => $value) {
314 1
                if ($parameter->getName() === $key) {
315 1
                    $orderedArguments[$parameter->getPosition()] = $value;
316 1
                    continue 2;
317
                }
318
            }
319
320
            //inject root common argument
321 1
            if ($this->root && !isset($incomeArgs['root']) && 'root' === $parameter->getName()) {
322 1
                $orderedArguments[$parameter->getPosition()] = $this->root;
323
            }
324
325
            //inject context common argument
326 1
            if ($this->context
327 1
                && 'context' === $parameter->getName()
328 1
                && $parameter->getClass()
329 1
                && is_a($parameter->getClass()->getName(), ResolverContext::class, true)
330
            ) {
331 1
                $orderedArguments[$parameter->getPosition()] = $this->context;
332
            }
333
        }
334
335 1
        return $orderedArguments;
336
    }
337
338
    /**
339
     * @param mixed  $value
340
     * @param string $type
341
     *
342
     * @return mixed
343
     */
344 1
    private function normalizeValue($value, string $type)
345
    {
346 1
        if (Types::ID === $type && $value) {
347 1
            if (\is_array($value)) {
348 1
                $idsArray = [];
349 1
                foreach ($value as $id) {
350 1
                    if ($id) {
351 1
                        $idsArray[] = IDEncoder::decode($id);
352
                    }
353
                }
354 1
                $value = $idsArray;
355
            } else {
356 1
                $value = IDEncoder::decode($value);
357
            }
358
        }
359
360 1
        return $value;
361
    }
362
363
    /**
364
     * Convert a array into object using given definition
365
     *
366
     * @param array                          $data       data to populate the object
367
     * @param FieldsAwareDefinitionInterface $definition object definition
368
     *
369
     * @return mixed
370
     */
371 1
    private function arrayToObject(array $data, FieldsAwareDefinitionInterface $definition)
372
    {
373 1
        $class = null;
374 1
        if ($definition instanceof ClassAwareDefinitionInterface) {
375 1
            $class = $definition->getClass();
376
        }
377
378
        //normalize data
379 1
        foreach ($data as $fieldName => &$value) {
380 1
            if (!$definition->hasField($fieldName)) {
381
                continue;
382
            }
383 1
            $fieldDefinition = $definition->getField($fieldName);
384 1
            $value = $this->normalizeValue($value, $fieldDefinition->getType());
385
        }
386 1
        unset($value);
387
388
        //instantiate object
389 1
        if (class_exists($class)) {
390 1
            $object = new $class();
391
        } else {
392 1
            $object = $data;
393
        }
394
395
        //populate object
396 1
        foreach ($data as $key => $value) {
397 1
            if (!$definition->hasField($key)) {
398
                continue;
399
            }
400 1
            $fieldDefinition = $definition->getField($key);
401
402 1
            if (\is_array($value) && $this->endpoint->hasType($fieldDefinition->getType())) {
403 1
                $childType = $this->endpoint->getType($fieldDefinition->getType());
404 1
                if ($childType instanceof FieldsAwareDefinitionInterface) {
405 1
                    $value = $this->arrayToObject($value, $childType);
406
                }
407
            }
408
409 1
            $this->setObjectValue($object, $fieldDefinition, $value);
410
        }
411
412 1
        return $object;
413
    }
414
415
    /**
416
     * @param mixed           $object
417
     * @param FieldDefinition $fieldDefinition
418
     * @param mixed           $value
419
     *
420
     * @throws \ReflectionException
421
     */
422 1
    private function setObjectValue(&$object, FieldDefinition $fieldDefinition, $value): void
423
    {
424
        //using setter
425 1
        $accessor = new PropertyAccessor();
426 1
        $propertyName = $fieldDefinition->getOriginName();
427 1
        if (\is_array($object)) {
428 1
            $object[$propertyName ?? $fieldDefinition->getName()] = $value;
429
        } else {
430 1
            if ($accessor->isWritable($object, $propertyName)) {
431 1
                $accessor->setValue($object, $propertyName, $value);
432
            } else {
433
                //using reflection
434
                $refClass = new \ReflectionClass(\get_class($object));
435
                if ($refClass->hasProperty($object) && $property = $refClass->getProperty($object)) {
436
                    $property->setAccessible(true);
437
                    $property->setValue($object, $value);
438
                }
439
            }
440
        }
441 1
    }
442
}
443