Completed
Push — master ( 45d3b1...40b0e0 )
by Rafael
04:30
created

applyArgumentsNamingConventions()   C

Complexity

Conditions 12
Paths 16

Size

Total Lines 22
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 30

Importance

Changes 0
Metric Value
dl 0
loc 22
ccs 7
cts 14
cp 0.5
rs 5.8703
c 0
b 0
f 0
cc 12
eloc 13
nc 16
nop 1
crap 30

How to fix   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
            }
117
118 22
            if ($resolver instanceof ContainerAwareInterface) {
119 22
                $resolver->setContainer($this->container);
120
            }
121
122 22
            if ($refClass->hasMethod('__invoke')) {
123 22
                $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 22
        if ($resolver && $refMethod) {
131 22
            $resolveContext = new ResolverContext();
132 22
            $resolveContext->setDefinition($this->executableDefinition);
133 22
            $resolveContext->setArgs($args);
134 22
            $resolveContext->setRoot($root);
135 22
            $resolveContext->setEndpoint($this->endpoint);
136 22
            $resolveContext->setResolveInfo($resolveInfo);
137
138 22
            $type = null;
139 22
            if ($this->executableDefinition instanceof NodeAwareDefinitionInterface && $this->executableDefinition->getNode()) {
140 22
                $type = $this->executableDefinition->getNode();
141
            }
142
143 22
            if (!$type && $this->executableDefinition->hasMeta('node')) {
144 3
                $type = $this->executableDefinition->getMeta('node');
145
            }
146 22
            if (!$type) {
147 17
                $type = $this->executableDefinition->getType();
148
            }
149
150 22
            $nodeDefinition = null;
151 22
            if ($this->endpoint->hasType($type)) {
152 22
                if ($nodeDefinition = $this->endpoint->getType($type)) {
153 22
                    $resolveContext->setNodeDefinition($nodeDefinition);
154
                }
155
            }
156
157 22
            if ($resolver instanceof AbstractResolver) {
158 22
                $resolver->setContext($resolveContext);
159
            }
160
161 22
            if ($resolver instanceof ExtensionsAwareInterface && $nodeDefinition instanceof HasExtensionsInterface) {
162 22
                $resolver->setExtensions($this->resolveObjectExtensions($nodeDefinition));
163
            }
164
165 22
            $params = $this->prepareMethodParameters($refMethod, $args);
166
167 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

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