Completed
Push — master ( 2975e7...46c3ee )
by Rafael
07:49
created

ResolverExecutor::resolveMethodArguments()   C

Complexity

Conditions 14
Paths 19

Size

Total Lines 37

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 42.1677

Importance

Changes 0
Metric Value
dl 0
loc 37
ccs 10
cts 21
cp 0.4762
rs 6.2666
c 0
b 0
f 0
cc 14
nc 19
nop 2
crap 42.1677

How to fix   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 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\NodeAwareDefinitionInterface;
27
use Ynlo\GraphQLBundle\Definition\Registry\Endpoint;
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\Type\Types;
33
use Ynlo\GraphQLBundle\Util\IDEncoder;
34
35
/**
36
 * This resolver act as a middleware between the executableDefinition and final resolvers.
37
 * Using injection of parameters can resolve the parameters needed by the final resolver before invoke
38
 */
39
class ResolverExecutor implements ContainerAwareInterface
40
{
41
    use ContainerAwareTrait;
42
43
    /**
44
     * @var Endpoint
45
     */
46
    protected $endpoint;
47
48
    /**
49
     * @var ExecutableDefinitionInterface
50
     */
51
    protected $executableDefinition;
52
53
    /**
54
     * @var mixed
55
     */
56
    protected $root;
57
58
    /**
59
     * @var ResolveInfo
60
     */
61
    protected $resolveInfo;
62
63
    /**
64
     * @var mixed
65
     */
66
    protected $context;
67
68
    /**
69
     * @var array
70
     */
71
    protected $args = [];
72
73 1
    public function __construct(ContainerInterface $container, Endpoint $endpoint, ExecutableDefinitionInterface $executableDefinition)
74
    {
75 1
        $this->container = $container;
76 1
        $this->endpoint = $endpoint;
77 1
        $this->executableDefinition = $executableDefinition;
78 1
    }
79
80
    /**
81
     * @param mixed                 $root
82
     * @param array                 $args
83
     * @param QueryExecutionContext $context
84
     * @param ResolveInfo           $resolveInfo
85
     *
86
     * @return mixed
87
     *
88
     * @throws \Exception
89
     */
90 1
    public function __invoke($root, array $args, QueryExecutionContext $context, ResolveInfo $resolveInfo)
91
    {
92 1
        $this->root = $root;
93 1
        $this->args = $args;
94 1
        $this->context = new QueryExecutionContext($context->getEndpoint(), $this->executableDefinition);
95 1
        $this->resolveInfo = $resolveInfo;
96
97 1
        $resolverName = $this->executableDefinition->getResolver();
98
99 1
        $resolver = null;
100 1
        $refMethod = null;
101
102 1
        if (class_exists($resolverName)) {
103 1
            $refClass = new \ReflectionClass($resolverName);
104
105
            //Verify if exist a service with resolver name and use it
106
            //otherwise build the resolver using simple injection
107
            //@see Ynlo\GraphQLBundle\Component\AutoWire\AutoWire
108 1
            if ($this->container->has($resolverName)) {
109 1
                $resolver = $this->container->get($resolverName);
110
            } else {
111
                /** @var callable $resolver */
112
                $resolver = $this->container->get(AutoWire::class)->createInstance($refClass->getName());
113
            }
114
115 1
            if ($resolver instanceof ContainerAwareInterface) {
116 1
                $resolver->setContainer($this->container);
117
            }
118
119 1
            if ($refClass->hasMethod('__invoke')) {
120 1
                $refMethod = $refClass->getMethod('__invoke');
121
            }
122
        } elseif (method_exists($root, $resolverName)) {
123
            $resolver = $root;
124
            $refMethod = new \ReflectionMethod(ClassUtils::getClass($root), $resolverName);
125
        }
126
127 1
        if ($resolver && $refMethod) {
128 1
            $resolveContext = new ResolverContext();
129 1
            $resolveContext->setDefinition($this->executableDefinition);
130 1
            $resolveContext->setArgs($args);
131 1
            $resolveContext->setRoot($root);
132 1
            $resolveContext->setEndpoint($this->endpoint);
133 1
            $resolveContext->setResolveInfo($resolveInfo);
134
135 1
            $type = null;
136 1
            if ($this->executableDefinition instanceof NodeAwareDefinitionInterface && $this->executableDefinition->getNode()) {
137 1
                $type = $this->executableDefinition->getNode();
138
            }
139
140 1
            if (!$type && $this->executableDefinition->hasMeta('node')) {
141
                $type = $this->executableDefinition->getMeta('node');
142
            }
143 1
            if (!$type) {
144
                $type = $this->executableDefinition->getType();
145
            }
146
147 1
            $nodeDefinition = null;
148 1
            if ($this->endpoint->hasType($type)) {
149 1
                if ($nodeDefinition = $this->endpoint->getType($type)) {
150 1
                    $resolveContext->setNodeDefinition($nodeDefinition);
151
                }
152
            }
153
154 1
            if ($resolver instanceof ResolverInterface) {
155 1
                $resolver->setContext($resolveContext);
156
            }
157
158 1
            if ($resolver instanceof ExtensionsAwareInterface && $nodeDefinition instanceof HasExtensionsInterface) {
159 1
                $resolver->setExtensions($this->resolveObjectExtensions($nodeDefinition));
160
            }
161
162 1
            if ($resolver instanceof EventDispatcherAwareInterface) {
163 1
                $resolver->setEventDispatcher($this->container->get(EventDispatcherInterface::class));
164
            }
165
166 1
            $params = $this->prepareMethodParameters($refMethod, $args);
167
168 1
            return $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

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