Passed
Push — master ( 46c3ee...445208 )
by Rafael
04:28
created

ResolverExecutor::__invoke()   C

Complexity

Conditions 12
Paths 90

Size

Total Lines 66

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 32
CRAP Score 12.5669

Importance

Changes 0
Metric Value
dl 0
loc 66
ccs 32
cts 38
cp 0.8421
rs 6.3151
c 0
b 0
f 0
cc 12
nc 90
nop 4
crap 12.5669

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 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\Events\EventDispatcherAwareInterface;
27
use Ynlo\GraphQLBundle\Extension\ExtensionInterface;
28
use Ynlo\GraphQLBundle\Extension\ExtensionManager;
29
use Ynlo\GraphQLBundle\Extension\ExtensionsAwareInterface;
30
use Ynlo\GraphQLBundle\Type\Types;
31
use Ynlo\GraphQLBundle\Util\IDEncoder;
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 Endpoint
0 ignored issues
show
Bug introduced by
The type Ynlo\GraphQLBundle\Resolver\Endpoint was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
43
     */
44
    protected $endpoint;
45
46
    /**
47
     * @var ExecutableDefinitionInterface
48
     */
49
    protected $executableDefinition;
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
     * ResolverExecutor constructor.
73
     *
74
     * @param ContainerInterface            $container
75
     * @param ExecutableDefinitionInterface $executableDefinition
76
     */
77 1
    public function __construct(ContainerInterface $container, ExecutableDefinitionInterface $executableDefinition)
78
    {
79 1
        $this->container = $container;
80 1
        $this->executableDefinition = $executableDefinition;
81 1
    }
82
83
    /**
84
     * @param mixed           $root
85
     * @param array           $args
86
     * @param ResolverContext $context
87
     * @param ResolveInfo     $resolveInfo
88
     *
89
     * @return mixed
90
     *
91
     * @throws \Exception
92
     */
93 1
    public function __invoke($root, array $args, ResolverContext $context, ResolveInfo $resolveInfo)
94
    {
95 1
        $this->root = $root;
96 1
        $this->args = $args;
97 1
        $this->resolveInfo = $resolveInfo;
98 1
        $this->endpoint = $context->getEndpoint();
0 ignored issues
show
Documentation Bug introduced by
It seems like $context->getEndpoint() of type Ynlo\GraphQLBundle\Definition\Registry\Endpoint is incompatible with the declared type Ynlo\GraphQLBundle\Resolver\Endpoint of property $endpoint.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
99
100 1
        $resolverName = $this->executableDefinition->getResolver();
101
102 1
        $resolver = null;
103 1
        $refMethod = null;
104
105 1
        if (class_exists($resolverName)) {
106 1
            $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 1
            if ($this->container->has($resolverName)) {
112 1
                $resolver = $this->container->get($resolverName);
113
            } else {
114
                /** @var callable $resolver */
115
                $resolver = $this->container->get(AutoWire::class)->createInstance($refClass->getName());
116
            }
117
118 1
            if ($resolver instanceof ContainerAwareInterface) {
119 1
                $resolver->setContainer($this->container);
120
            }
121
122 1
            if ($refClass->hasMethod('__invoke')) {
123 1
                $refMethod = $refClass->getMethod('__invoke');
124
            }
125
        } elseif (method_exists($root, $resolverName)) {
126
            $resolver = $root;
127
            $refMethod = new \ReflectionMethod(ClassUtils::getClass($root), $resolverName);
128
        }
129
130 1
        if ($resolver && $refMethod) {
131 1
            $this->context = ContextBuilder::create($context->getEndpoint())
132 1
                                           ->setRoot($root)
133 1
                                           ->setResolveInfo($resolveInfo)
134 1
                                           ->setArgs($args)
135 1
                                           ->setDefinition($this->executableDefinition)
136 1
                                           ->build();
137
138
139 1
            if ($resolver instanceof ResolverInterface) {
140 1
                $resolver->setContext($this->context);
141
            }
142
143 1
            $node = $this->context->getNode();
144 1
            if ($resolver instanceof ExtensionsAwareInterface && $node instanceof HasExtensionsInterface) {
145 1
                $resolver->setExtensions($this->resolveObjectExtensions($node));
146
            }
147
148 1
            if ($resolver instanceof EventDispatcherAwareInterface) {
149 1
                $resolver->setEventDispatcher($this->container->get(EventDispatcherInterface::class));
150
            }
151
152 1
            $params = $this->prepareMethodParameters($refMethod, $args);
153
154 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

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