Completed
Push — master ( 1826e3...724c6f )
by Rafael
07:29
created

ResolverExecutor::normalizeValue()   B

Complexity

Conditions 6
Paths 3

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 6

Importance

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

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