Completed
Push — master ( 300eda...15c856 )
by Rafael
09:26
created

ResolverExecutor::resolveObjectExtensions()   C

Complexity

Conditions 7
Paths 16

Size

Total Lines 29
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 7.6383

Importance

Changes 0
Metric Value
dl 0
loc 29
ccs 13
cts 17
cp 0.7647
rs 6.7272
c 0
b 0
f 0
cc 7
eloc 15
nc 16
nop 1
crap 7.6383
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
    public function __construct(ContainerInterface $container, Endpoint $endpoint, ExecutableDefinitionInterface $executableDefinition)
73
    {
74
        $this->container = $container;
75
        $this->endpoint = $endpoint;
76 19
        $this->executableDefinition = $executableDefinition;
77
    }
78 19
79 19
    /**
80 19
     * @param mixed       $root
81 19
     * @param array       $args
82
     * @param mixed       $context
83
     * @param ResolveInfo $resolveInfo
84
     *
85
     * @return mixed
86
     *
87
     * @throws \Exception
88
     */
89
    public function __invoke($root, array $args, $context, ResolveInfo $resolveInfo)
90
    {
91
        $this->root = $root;
92
        $this->args = $args;
93 22
        $this->context = $context;
94
        $this->resolveInfo = $resolveInfo;
95 22
96 22
        $resolverName = $this->executableDefinition->getResolver();
97 22
98 22
        $resolver = null;
99
        $refMethod = null;
100 22
101
        if (class_exists($resolverName)) {
102 22
            $refClass = new \ReflectionClass($resolverName);
103 22
104
            //Verify if exist a service with resolver name and use it
105 22
            //otherwise build the resolver using simple injection
106 22
            //@see Ynlo\GraphQLBundle\Component\AutoWire\AutoWire
107
            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
            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 22
            $resolver = $root;
123 22
            $refMethod = new \ReflectionMethod(ClassUtils::getClass($root), $resolverName);
124
        }
125
126
        if ($resolver && $refMethod) {
127
            $resolveContext = new ResolverContext();
128
            $resolveContext->setDefinition($this->executableDefinition);
129
            $resolveContext->setArgs($args);
130 22
            $resolveContext->setRoot($root);
131 22
            $resolveContext->setEndpoint($this->endpoint);
132 22
            $resolveContext->setResolveInfo($resolveInfo);
133 22
134 22
            $type = null;
135 22
            if ($this->executableDefinition instanceof NodeAwareDefinitionInterface && $this->executableDefinition->getNode()) {
136 22
                $type = $this->executableDefinition->getNode();
137
            }
138 22
139 22
            if (!$type && $this->executableDefinition->hasMeta('node')) {
140 22
                $type = $this->executableDefinition->getMeta('node');
141
            }
142
            if (!$type) {
143 22
                $type = $this->executableDefinition->getType();
144 3
            }
145
146 22
            $nodeDefinition = null;
147 17
            if ($this->endpoint->hasType($type)) {
148
                if ($nodeDefinition = $this->endpoint->getType($type)) {
149
                    $resolveContext->setNodeDefinition($nodeDefinition);
150 22
                }
151 22
            }
152 22
153 22
            if ($resolver instanceof AbstractResolver) {
154
                $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 22
163
            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 22
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 22
        throw new \RuntimeException($error);
168
    }
169
170
    /**
171
     * @param HasExtensionsInterface $objectDefinition
172
     *
173
     * @return ExtensionInterface[]
174
     */
175
    protected function resolveObjectExtensions(HasExtensionsInterface $objectDefinition): array
176
    {
177
        $extensions = [];
178
179 22
        //get all extensions registered as services
180
        $registeredExtensions = $this->container->get(ExtensionManager::class)->getExtensions();
181 22
        foreach ($registeredExtensions as $registeredExtension) {
182 22
            foreach ($objectDefinition->getExtensions() as $extensionDefinition) {
183 8
                $extensionClass = $extensionDefinition->getClass();
184 8
                if (get_class($registeredExtension) === $extensionClass) {
185
                    $extensions[$extensionClass] = $registeredExtension;
186
                }
187 8
            }
188 8
        }
189 8
190
        //get all extensions not registered as services
191
        foreach ($objectDefinition->getExtensions() as $extensionDefinition) {
192 8
            $class = $extensionDefinition->getClass();
193
            if (!isset($extensions[$class])) {
194
                $instance = new $class();
195
                if ($instance instanceof ContainerAwareInterface) {
196 22
                    $instance->setContainer($this->container);
197 22
                }
198 22
199
                $extensions[$class] = $instance;
200
            }
201
        }
202
203 22
        return array_values($extensions);
204
    }
205
206 22
    /**
207
     * @param \ReflectionMethod $refMethod
208
     * @param array             $args
209
     *
210
     * @throws \Exception
211
     *
212
     * @return array
213
     */
214
    protected function prepareMethodParameters(\ReflectionMethod $refMethod, array $args): array
215
    {
216
        //normalize arguments
217 22
        $normalizedArguments = [];
218
        foreach ($args as $key => $value) {
219
            if ($this->executableDefinition->hasArgument($key)) {
220 22
                $argument = $this->executableDefinition->getArgument($key);
221 22
                if ('input' === $key) {
222 22
                    $normalizedValue = $value;
223 22
                } else {
224 22
                    $normalizedValue = $this->normalizeValue($value, $argument->getType());
225 8
226
                    //normalize argument into respective inputs objects
227 14
                    if (is_array($normalizedValue) && $this->endpoint->hasType($argument->getType())) {
228
                        if ($argument->isList()) {
229
                            $tmp = [];
230 14
                            foreach ($normalizedValue as $childValue) {
231 7
                                $tmp[] = $this->arrayToObject($childValue, $this->endpoint->getType($argument->getType()));
232 7
                            }
233 7
                            $normalizedValue = $tmp;
234 7
                        } else {
235
                            $normalizedValue = $this->arrayToObject($normalizedValue, $this->endpoint->getType($argument->getType()));
236 7
                        }
237
                    }
238
                }
239
                $normalizedArguments[$argument->getName()] = $normalizedValue;
240
                $normalizedArguments[$argument->getInternalName()] = $normalizedValue;
241
            }
242 22
        }
243 22
        $this->applyArgumentsNamingConventions($normalizedArguments);
244
        $normalizedArguments['args'] = $normalizedArguments;
245
        $normalizedArguments['root'] = $this->root;
246 22
        $indexedArguments = $this->resolveMethodArguments($refMethod, $normalizedArguments);
247 22
        ksort($indexedArguments);
248 22
249 22
        return $indexedArguments;
250 22
    }
251
252 22
    /**
253
     * Apply some conventions to given arguments
254
     *
255
     * @param array $args
256
     */
257
    protected function applyArgumentsNamingConventions(&$args)
258
    {
259
        //TODO: move this behavior to some configurable external service or middleware
260 22
        //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
        foreach ($args as $name => &$value) {
263
            if ($value instanceof ID && 'id' !== $name) {
264
                $definition = $this->endpoint->getType($value->getNodeType());
265 22
                if ($definition instanceof ObjectDefinition && $definition->getClass()) {
266 22
                    /** @var EntityManager $em */
267
                    $em = $this->container->get('doctrine')->getManager();
268
                    $value = $em->getRepository($definition->getClass())->find($value->getDatabaseId());
269
                }
270
            }
271
            if (is_array($value)) {
272
                foreach ($value as &$val) {
273
                    if ($val instanceof ID && 'ids' !== $name) {
274 22
                        $definition = $this->endpoint->getType($val->getNodeType());
275 18
                        if ($definition instanceof ObjectDefinition && $definition->getClass()) {
276 18
                            /** @var EntityManager $em */
277
                            $em = $this->container->get('doctrine')->getManager();
278
                            $val = $em->getRepository($definition->getClass())->find($val->getDatabaseId());
279
                        }
280
                    }
281 22
                }
282
            }
283
        }
284
    }
285
286
    /**
287 22
     * @param \ReflectionMethod $method
288
     * @param array             $incomeArgs
289
     *
290
     * @return array
291
     */
292
    protected function resolveMethodArguments(\ReflectionMethod $method, array $incomeArgs)
293
    {
294
        $orderedArguments = [];
295 22
        foreach ($method->getParameters() as $parameter) {
296
            if ($parameter->isOptional()) {
297 22
                $orderedArguments[$parameter->getPosition()] = $parameter->getDefaultValue();
298 22
            }
299 22
            foreach ($incomeArgs as $key => $value) {
300 10
                if ($parameter->getName() === $key) {
301
                    $orderedArguments[$parameter->getPosition()] = $value;
302 22
                    continue 2;
303 22
                }
304 22
            }
305 22
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
        return $orderedArguments;
318
    }
319
320 22
    /**
321
     * @param mixed  $value
322
     * @param string $type
323
     *
324
     * @return mixed
325
     */
326
    protected function normalizeValue($value, string $type)
327
    {
328
        if (Types::ID === $type && $value) {
329 14
            if (\is_array($value)) {
330
                $idsArray = [];
331 14
                foreach ($value as $id) {
332 5
                    if ($id) {
333 2
                        $idsArray[] = ID::createFromString($id);
334 2
                    }
335 2
                }
336 2
                $value = $idsArray;
337
            } else {
338
                $value = ID::createFromString($value);
339 2
            }
340
        }
341 3
342
        return $value;
343
    }
344
345 14
    /**
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
    protected function arrayToObject(array $data, ObjectDefinitionInterface $definition)
354
    {
355
        $class = $definition->getClass();
356 7
357
        //normalize data
358 7
        foreach ($data as $fieldName => &$value) {
359
            if (!$definition->hasField($fieldName)) {
360
                continue;
361 7
            }
362 7
            $fieldDefinition = $definition->getField($fieldName);
363
            $value = $this->normalizeValue($value, $fieldDefinition->getType());
364
        }
365 7
        unset($value);
366 7
367
        //instantiate object
368 7
        if (class_exists($class)) {
369
            $object = new $class();
370
        } else {
371 7
            return $data;
372 7
        }
373
374
        //populate object
375
        foreach ($data as $key => $value) {
376
            if (!$definition->hasField($key)) {
377
                continue;
378 7
            }
379 7
            $fieldDefinition = $definition->getField($key);
380
            $this->setObjectValue($object, $fieldDefinition, $value);
381
        }
382 7
383 7
        return $object;
384
    }
385
386 7
    /**
387
     * @param mixed           $object
388
     * @param FieldDefinition $fieldDefinition
389
     * @param mixed           $value
390
     */
391
    protected function setObjectValue($object, FieldDefinition $fieldDefinition, $value)
392
    {
393
        //using setter
394 7
        $accessor = new PropertyAccessor();
395
        $propertyName = $fieldDefinition->getOriginName();
396
        if ($propertyName) {
397 7
            if ($accessor->isWritable($object, $propertyName)) {
398 7
                $accessor->setValue($object, $propertyName, $value);
399 7
            } else {
400 7
                //using reflection
401 7
                $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
    }
409
}
410