Completed
Push — master ( 15c856...4ce16e )
by Rafael
05:47
created

ResolverExecutor   F

Complexity

Total Complexity 72

Size/Duplication

Total Lines 367
Duplicated Lines 0 %

Test Coverage

Coverage 80.88%

Importance

Changes 0
Metric Value
wmc 72
dl 0
loc 367
ccs 127
cts 157
cp 0.8088
rs 2.5423
c 0
b 0
f 0

9 Methods

Rating   Name   Duplication   Size   Complexity  
B setObjectValue() 0 14 5
D resolveMethodArguments() 0 26 9
C prepareMethodParameters() 0 36 8
C resolveObjectExtensions() 0 29 7
B arrayToObject() 0 31 6
C applyArgumentsNamingConventions() 0 22 12
A __construct() 0 5 1
F __invoke() 0 79 18
B normalizeValue() 0 17 6

How to fix   Complexity   

Complex Class

Complex classes like ResolverExecutor often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ResolverExecutor, and based on these observations, apply Extract Interface, too.

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

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