Completed
Push — master ( 8e3b13...e5ed6d )
by Rafael
07:59
created

applyArgumentsNamingConventions()   C

Complexity

Conditions 7
Paths 5

Size

Total Lines 22
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 15.4039

Importance

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

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