Completed
Push — master ( 828139...847e66 )
by Rafael
04:50
created

ResolverExecutor::__invoke()   F

Complexity

Conditions 18
Paths 776

Size

Total Lines 78
Code Lines 46

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 40
CRAP Score 18.7184

Importance

Changes 0
Metric Value
dl 0
loc 78
ccs 40
cts 46
cp 0.8696
rs 2.5641
c 0
b 0
f 0
cc 18
eloc 46
nc 776
nop 4
crap 18.7184

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

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