Passed
Push — master ( c325a6...db2527 )
by Yonel Ceruto
11:21
created

ResolverExecutor::setObjectValue()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 14
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 6.6

Importance

Changes 0
Metric Value
cc 5
eloc 10
nc 4
nop 3
dl 0
loc 14
ccs 6
cts 10
cp 0.6
crap 6.6
rs 8.8571
c 0
b 0
f 0
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 21
    public function __invoke($root, array $args, $context, ResolveInfo $resolveInfo)
94
    {
95 21
        $this->root = $root;
96 21
        $this->args = $args;
97 21
        $this->context = $context;
98 21
        $this->resolveInfo = $resolveInfo;
99
100 21
        $resolverName = $this->executableDefinition->getResolver();
101
102 21
        $resolver = null;
103 21
        $refMethod = null;
104
105 21
        if (class_exists($resolverName)) {
106 21
            $refClass = new \ReflectionClass($resolverName);
107
108
            /** @var callable $resolver */
109 21
            $resolver = $this->container->get(AutoWire::class)->createInstance($refClass->getName());
110 21
            if ($resolver instanceof ContainerAwareInterface) {
111 21
                $resolver->setContainer($this->container);
112
            }
113 21
            if ($refClass->hasMethod('__invoke')) {
114 21
                $refMethod = $refClass->getMethod('__invoke');
115
            }
116
        } elseif (method_exists($root, $resolverName)) {
117
            $resolver = $root;
118
            $refMethod = new \ReflectionMethod(ClassUtils::getClass($root), $resolverName);
119
        }
120
121 21
        if ($resolver && $refMethod) {
122 21
            $resolveContext = new ResolverContext();
123 21
            $resolveContext->setDefinition($this->executableDefinition);
124 21
            $resolveContext->setArgs($args);
125 21
            $resolveContext->setRoot($root);
126 21
            $resolveContext->setEndpoint($this->endpoint);
127 21
            $resolveContext->setResolveInfo($resolveInfo);
128
129 21
            $type = null;
130 21
            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...
131 21
                $type = $this->executableDefinition->getNode();
132
            }
133
134 21
            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...
135 3
                $type = $this->executableDefinition->getMeta('node');
136
            }
137 21
            if (!$type) {
138 17
                $type = $this->executableDefinition->getType();
139
            }
140
141 21
            $nodeDefinition = null;
142 21
            if ($this->endpoint->hasType($type)) {
143 21
                if ($nodeDefinition = $this->endpoint->getType($type)) {
144 21
                    $resolveContext->setNodeDefinition($nodeDefinition);
145
                }
146
            }
147
148 21
            if ($resolver instanceof AbstractResolver) {
149 21
                $resolver->setContext($resolveContext);
150
            }
151
152 21
            if ($resolver instanceof ExtensionsAwareInterface && $nodeDefinition instanceof HasExtensionsInterface) {
153 21
                $resolver->setExtensions($this->resolveObjectExtensions($nodeDefinition));
154
            }
155
156 21
            $params = $this->prepareMethodParameters($refMethod, $args);
157
158 21
            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

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