Passed
Push — master ( 319fcb...7a107a )
by Rafael
04:41
created

ResolverExecutor::__invoke()   F

Complexity

Conditions 19
Paths 1930

Size

Total Lines 83
Code Lines 48

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 42
CRAP Score 19.7049

Importance

Changes 0
Metric Value
dl 0
loc 83
c 0
b 0
f 0
ccs 42
cts 48
cp 0.875
rs 2.0734
cc 19
eloc 48
nc 1930
nop 4
crap 19.7049

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

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