Passed
Push — master ( cf54c3...3ae95d )
by Rafael
07:43
created

ResolverExecutor   C

Complexity

Total Complexity 56

Size/Duplication

Total Lines 328
Duplicated Lines 0 %

Test Coverage

Coverage 84.29%

Importance

Changes 0
Metric Value
wmc 56
dl 0
loc 328
ccs 118
cts 140
cp 0.8429
rs 5.5555
c 0
b 0
f 0

8 Methods

Rating   Name   Duplication   Size   Complexity  
D resolveMethodArguments() 0 26 9
C prepareMethodParameters() 0 36 8
B resolveObjectExtensions() 0 28 4
A __construct() 0 5 1
F __invoke() 0 70 17
B setObjectValue() 0 14 5
B arrayToObject() 0 31 6
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 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\PropertyAccess\PropertyAccessor;
19
use Ynlo\GraphQLBundle\Definition\HasExtensionsInterface;
20
use Ynlo\GraphQLBundle\Definition\ExecutableDefinitionInterface;
21
use Ynlo\GraphQLBundle\Definition\FieldDefinition;
22
use Ynlo\GraphQLBundle\Definition\NodeAwareDefinitionInterface;
23
use Ynlo\GraphQLBundle\Definition\ObjectDefinitionInterface;
24
use Ynlo\GraphQLBundle\Definition\Registry\Endpoint;
25
use Ynlo\GraphQLBundle\Extension\ExtensionInterface;
26
use Ynlo\GraphQLBundle\Extension\ExtensionsAwareInterface;
27
use Ynlo\GraphQLBundle\Model\ID;
28
use Ynlo\GraphQLBundle\Type\Types;
29
30
/**
31
 * This resolver act as a middleware between the executableDefinition and final resolvers.
32
 * Using injection of parameters can resolve the parameters needed by the final resolver before invoke
33
 */
34
class ResolverExecutor implements ContainerAwareInterface
35
{
36
    use ContainerAwareTrait;
37
38
    /**
39
     * @var ExecutableDefinitionInterface
40
     */
41
    protected $executableDefinition;
42
43
    /**
44
     * @var Endpoint
45
     */
46
    protected $endpoint;
47
48
    /**
49
     * @var mixed
50
     */
51
    protected $root;
52
53
    /**
54
     * @var ResolveInfo
55
     */
56
    protected $resolveInfo;
57
58
    /**
59
     * @var mixed
60
     */
61
    protected $context;
62
63
    /**
64
     * @var array
65
     */
66
    protected $args = [];
67
68
    /**
69
     * @param ContainerInterface            $container
70
     * @param Endpoint                      $endpoint
71
     * @param ExecutableDefinitionInterface $executableDefinition
72
     */
73 19
    public function __construct(ContainerInterface $container, Endpoint $endpoint, ExecutableDefinitionInterface $executableDefinition)
74
    {
75 19
        $this->executableDefinition = $executableDefinition;
76 19
        $this->endpoint = $endpoint;
77 19
        $this->container = $container;
78 19
    }
79
80
    /**
81
     * @param mixed       $root
82
     * @param array       $args
83
     * @param mixed       $context
84
     * @param ResolveInfo $resolveInfo
85
     *
86
     * @return mixed
87
     *
88
     * @throws \Exception
89
     */
90 21
    public function __invoke($root, array $args, $context, ResolveInfo $resolveInfo)
91
    {
92 21
        $this->root = $root;
93 21
        $this->args = $args;
94 21
        $this->context = $context;
95 21
        $this->resolveInfo = $resolveInfo;
96
97 21
        $resolverName = $this->executableDefinition->getResolver();
98
99 21
        $resolver = null;
100 21
        $refMethod = null;
101
102 21
        if (class_exists($resolverName)) {
103 21
            $refClass = new \ReflectionClass($resolverName);
104
105
            /** @var callable $resolver */
106 21
            $resolver = $refClass->newInstance();
107 21
            if ($resolver instanceof ContainerAwareInterface) {
108 21
                $resolver->setContainer($this->container);
109
            }
110 21
            if ($refClass->hasMethod('__invoke')) {
111 21
                $refMethod = $refClass->getMethod('__invoke');
112
            }
113
        } elseif (method_exists($root, $resolverName)) {
114
            $resolver = $root;
115
            $refMethod = new \ReflectionMethod(ClassUtils::getClass($root), $resolverName);
116
        }
117
118 21
        if ($resolver && $refMethod) {
119 21
            $resolveContext = new ResolverContext();
120 21
            $resolveContext->setDefinition($this->executableDefinition);
121 21
            $resolveContext->setArgs($args);
122 21
            $resolveContext->setRoot($root);
123 21
            $resolveContext->setEndpoint($this->endpoint);
124 21
            $resolveContext->setResolveInfo($resolveInfo);
125
126 21
            $type = null;
127 21
            if ($this->executableDefinition instanceof NodeAwareDefinitionInterface && $this->executableDefinition->getNode()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->executableDefinition->getNode() of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
128 21
                $type = $this->executableDefinition->getNode();
129
            }
130
131 21
            if (!$type && $this->executableDefinition->hasMeta('node')) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $type of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
132 3
                $type = $this->executableDefinition->getMeta('node');
133
            }
134 21
            if (!$type) {
135 17
                $type = $this->executableDefinition->getType();
136
            }
137
138 21
            $nodeDefinition = null;
139 21
            if ($this->endpoint->hasType($type)) {
140 21
                if ($nodeDefinition = $this->endpoint->getType($type)) {
141 21
                    $resolveContext->setNodeDefinition($nodeDefinition);
142
                }
143
            }
144
145 21
            if ($resolver instanceof AbstractResolver) {
146 21
                $resolver->setContext($resolveContext);
147
            }
148
149 21
            if ($resolver instanceof ExtensionsAwareInterface && $nodeDefinition instanceof HasExtensionsInterface) {
150 21
                $resolver->setExtensions($this->resolveObjectExtensions($nodeDefinition));
151
            }
152
153 21
            $params = $this->prepareMethodParameters($refMethod, $args);
154
155 21
            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

155
            return $refMethod->invokeArgs(/** @scrutinizer ignore-type */ $resolver, $params);
Loading history...
156
        }
157
158
        $error = sprintf('The resolver "%s" for executableDefinition "%s" is not a valid resolver. Resolvers should have a method "__invoke(...)"', $resolverName, $this->executableDefinition->getName());
159
        throw new \RuntimeException($error);
160
    }
161
162
    /**
163
     * @param HasExtensionsInterface $objectDefinition
164
     *
165
     * @return ExtensionInterface[]
166
     */
167 21
    protected function resolveObjectExtensions(HasExtensionsInterface $objectDefinition): array
168
    {
169 21
        $extensions = [];
170 21
        foreach ($objectDefinition->getExtensions() as $extensionDefinition) {
171 8
            $extensionClass = $extensionDefinition->getClass();
172 8
            if ($this->container->has($extensionClass)) {
173
                $extensionInstance = $this->container->get($extensionClass);
174
            } else {
175 8
                $extensionInstance = new $extensionClass();
176 8
                if ($extensionInstance instanceof ContainerAwareInterface) {
177 8
                    $extensionInstance->setContainer($this->container);
178
                }
179
            }
180 8
            $extensions[] = [$extensionDefinition->getPriority(), $extensionInstance];
181
        }
182
183
        //sort by priority
184 21
        usort(
185 21
            $extensions,
186 21
            function ($extension1, $extension2) {
187
                list($priority1) = $extension1;
188
                list($priority2) = $extension2;
189
190
                return version_compare($priority2 + 250, $priority1 + 250);
191 21
            }
192
        );
193
194 21
        return array_column($extensions, 1);
195
    }
196
197
    /**
198
     * @param \ReflectionMethod $refMethod
199
     * @param array             $args
200
     *
201
     * @throws \Exception
202
     *
203
     * @return array
204
     */
205 21
    protected function prepareMethodParameters(\ReflectionMethod $refMethod, array $args): array
206
    {
207
        //normalize arguments
208 21
        $normalizedArguments = [];
209 21
        foreach ($args as $key => $value) {
210 21
            if ($this->executableDefinition->hasArgument($key)) {
211 21
                $argument = $this->executableDefinition->getArgument($key);
212 21
                if ('input' === $key) {
213 8
                    $normalizedValue = $value;
214
                } else {
215 13
                    $normalizedValue = $this->normalizeValue($value, $argument->getType());
216
217
                    //normalize argument into respective inputs objects
218 13
                    if (is_array($normalizedValue) && $this->endpoint->hasType($argument->getType())) {
219 7
                        if ($argument->isList()) {
220 7
                            $tmp = [];
221 7
                            foreach ($normalizedValue as $childValue) {
222 7
                                $tmp[] = $this->arrayToObject($childValue, $this->endpoint->getType($argument->getType()));
223
                            }
224 7
                            $normalizedValue = $tmp;
225
                        } else {
226
                            $normalizedValue = $this->arrayToObject($normalizedValue, $this->endpoint->getType($argument->getType()));
227
                        }
228
                    }
229
                }
230 21
                $normalizedArguments[$argument->getName()] = $normalizedValue;
231 21
                $normalizedArguments[$argument->getInternalName()] = $normalizedValue;
232
            }
233
        }
234
235 21
        $normalizedArguments['args'] = $normalizedArguments;
236 21
        $normalizedArguments['root'] = $this->root;
237 21
        $indexedArguments = $this->resolveMethodArguments($refMethod, $normalizedArguments);
238 21
        ksort($indexedArguments);
239
240 21
        return $indexedArguments;
241
    }
242
243
    /**
244
     * @param \ReflectionMethod $method
245
     * @param array             $incomeArgs
246
     *
247
     * @return array
248
     */
249 21
    protected function resolveMethodArguments(\ReflectionMethod $method, array $incomeArgs)
250
    {
251 21
        $orderedArguments = [];
252 21
        foreach ($method->getParameters() as $parameter) {
253 21
            if ($parameter->isOptional()) {
254 10
                $orderedArguments[$parameter->getPosition()] = $parameter->getDefaultValue();
255
            }
256 21
            foreach ($incomeArgs as $key => $value) {
257 21
                if ($parameter->getName() === $key) {
258 21
                    $orderedArguments[$parameter->getPosition()] = $value;
259 21
                    continue 2;
260
                }
261
            }
262
263
            //inject root common argument
264
            if ($this->root
265
                && 'root' === $parameter->getName()
266
                && $parameter->getClass()
267
                && $parameter->getClass()->isInstance($this->root)
268
269
            ) {
270
                $orderedArguments[$parameter->getPosition()] = $this->root;
271
            }
272
        }
273
274 21
        return $orderedArguments;
275
    }
276
277
    /**
278
     * @param mixed  $value
279
     * @param string $type
280
     *
281
     * @return mixed
282
     */
283 13
    protected function normalizeValue($value, string $type)
284
    {
285 13
        if (Types::ID === $type && $value) {
286 4
            if (\is_array($value)) {
287 2
                $idsArray = [];
288 2
                foreach ($value as $id) {
289 2
                    if ($id) {
290
                        $idsArray[] = ID::createFromString($id);
291 2
                    }
292
                }
293 2
                $value = $idsArray;
294
            } else {
295
                $value = ID::createFromString($value);
296
            }
297 13
        }
298
299
        return $value;
300
    }
301
302
    /**
303
     * Convert a array into object using given definition
304
     *
305
     * @param array                     $data       data to populate the object
306
     * @param ObjectDefinitionInterface $definition object definition
307
     *
308 7
     * @return mixed
309
     */
310 7
    protected function arrayToObject(array $data, ObjectDefinitionInterface $definition)
311
    {
312
        $class = $definition->getClass();
313 7
314 7
        //normalize data
315
        foreach ($data as $fieldName => &$value) {
316
            if (!$definition->hasField($fieldName)) {
317 7
                continue;
318 7
            }
319
            $fieldDefinition = $definition->getField($fieldName);
320 7
            $value = $this->normalizeValue($value, $fieldDefinition->getType());
321
        }
322
        unset($value);
323 7
324 7
        //instantiate object
325
        if (class_exists($class)) {
326
            $object = new $class();
327
        } else {
328
            return $data;
329
        }
330 7
331 7
        //populate object
332
        foreach ($data as $key => $value) {
333
            if (!$definition->hasField($key)) {
334 7
                continue;
335 7
            }
336
            $fieldDefinition = $definition->getField($key);
337
            $this->setObjectValue($object, $fieldDefinition, $value);
338 7
        }
339
340
        return $object;
341
    }
342
343
    /**
344
     * @param mixed           $object
345
     * @param FieldDefinition $fieldDefinition
346 7
     * @param mixed           $value
347
     */
348
    protected function setObjectValue($object, FieldDefinition $fieldDefinition, $value)
349 7
    {
350 7
        //using setter
351 7
        $accessor = new PropertyAccessor();
352 7
        $propertyName = $fieldDefinition->getOriginName();
353 7
        if ($propertyName) {
354
            if ($accessor->isWritable($object, $propertyName)) {
355
                $accessor->setValue($object, $propertyName, $value);
356
            } else {
357
                //using reflection
358
                $refClass = new \ReflectionClass(\get_class($object));
359
                if ($refClass->hasProperty($fieldDefinition->getOriginName()) && $property = $refClass->getProperty($fieldDefinition->getOriginName())) {
360
                    $property->setAccessible(true);
361
                    $property->setValue($object, $value);
362
                }
363 7
            }
364
        }
365
    }
366
}
367