Passed
Push — master ( c39fb6...9e9a0d )
by Rafael
04:42
created

ResolverExecutor::prepareMethodParameters()   C

Complexity

Conditions 8
Paths 6

Size

Total Lines 35
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 8.0069

Importance

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

137
            return $refMethod->invokeArgs(/** @scrutinizer ignore-type */ $resolver, $params);
Loading history...
138
        }
139
140
        $error = sprintf('The resolver "%s" for executableDefinition "%s" is not a valid resolver. Resolvers should have a method "__invoke(...)"', $resolverName, $this->executableDefinition->getName());
141
        throw new \RuntimeException($error);
142
    }
143
144
    /**
145
     * @param \ReflectionMethod $refMethod
146
     * @param array             $args
147
     *
148
     * @throws \Exception
149
     *
150
     * @return array
151
     */
152 21
    protected function prepareMethodParameters(\ReflectionMethod $refMethod, array $args): array
153
    {
154
        //normalize arguments
155 21
        $normalizedArguments = [];
156 21
        foreach ($args as $key => $value) {
157 21
            if ($this->executableDefinition->hasArgument($key)) {
158 21
                $argument = $this->executableDefinition->getArgument($key);
159 21
                if ('input' === $key) {
160 7
                    $normalizedValue = $value;
161
                } else {
162 14
                    $normalizedValue = $this->normalizeValue($value, $argument->getType());
163
164
                    //normalize argument into respective inputs objects
165 14
                    if (is_array($normalizedValue) && $this->endpoint->hasType($argument->getType())) {
166 7
                        if ($argument->isList()) {
167 7
                            $tmp = [];
168 7
                            foreach ($normalizedValue as $childValue) {
169 7
                                $tmp[] = $this->arrayToObject($childValue, $this->endpoint->getType($argument->getType()));
170
                            }
171 7
                            $normalizedValue = $tmp;
172
                        } else {
173
                            $normalizedValue = $this->arrayToObject($normalizedValue, $this->endpoint->getType($argument->getType()));
174
                        }
175
                    }
176
                }
177 21
                $normalizedArguments[$argument->getName()] = $normalizedValue;
178 21
                $normalizedArguments[$argument->getInternalName()] = $normalizedValue;
179
            }
180
        }
181
182 21
        $normalizedArguments['args'] = $normalizedArguments;
183 21
        $indexedArguments = $this->resolveMethodArguments($refMethod, $normalizedArguments);
184 21
        ksort($indexedArguments);
185
186 21
        return $indexedArguments;
187
    }
188
189
    /**
190
     * @param \ReflectionMethod $method
191
     * @param array             $incomeArgs
192
     *
193
     * @return array
194
     */
195 21
    protected function resolveMethodArguments(\ReflectionMethod $method, array $incomeArgs)
196
    {
197 21
        $orderedArguments = [];
198 21
        foreach ($method->getParameters() as $parameter) {
199 21
            if ($parameter->isOptional()) {
200 10
                $orderedArguments[$parameter->getPosition()] = $parameter->getDefaultValue();
201
            }
202 21
            foreach ($incomeArgs as $key => $value) {
203 21
                if ($parameter->getName() === $key) {
204 21
                    $orderedArguments[$parameter->getPosition()] = $value;
205 21
                    continue 2;
206
                }
207
            }
208
209
            //inject root common argument
210
            if ($this->root
211
                && 'root' === $parameter->getName()
212
                && $parameter->getClass()
213
                && $parameter->getClass()->isInstance($this->root)
214
215
            ) {
216
                $orderedArguments[$parameter->getPosition()] = $this->root;
217
            }
218
        }
219
220 21
        return $orderedArguments;
221
    }
222
223
    /**
224
     * @param mixed  $value
225
     * @param string $type
226
     *
227
     * @return mixed
228
     */
229 14
    protected function normalizeValue($value, string $type)
230
    {
231 14
        if (Type::ID === $type) {
232 3
            if (\is_array($value)) {
233 2
                $idsArray = [];
234 2
                foreach ($value as $id) {
235 2
                    $idsArray[] = ID::createFromString($id);
236
                }
237 2
                $value = $idsArray;
238
            } else {
239 1
                $value = ID::createFromString($value);
240
            }
241
        }
242
243 14
        return $value;
244
    }
245
246
    /**
247
     * Convert a array into object using given definition
248
     *
249
     * @param array                     $data       data to populate the object
250
     * @param ObjectDefinitionInterface $definition object definition
251
     *
252
     * @return mixed
253
     */
254 7
    protected function arrayToObject(array $data, ObjectDefinitionInterface $definition)
255
    {
256 7
        $class = $definition->getClass();
257
258
        //normalize data
259 7
        foreach ($data as $fieldName => &$value) {
260 7
            if (!$definition->hasField($fieldName)) {
261
                continue;
262
            }
263 7
            $fieldDefinition = $definition->getField($fieldName);
264 7
            $value = $this->normalizeValue($value, $fieldDefinition->getType());
265
        }
266 7
        unset($value);
267
268
        //instantiate object
269 7
        if (class_exists($class)) {
270 7
            $object = new $class();
271
        } else {
272
            return $data;
273
        }
274
275
        //populate object
276 7
        foreach ($data as $key => $value) {
277 7
            if (!$definition->hasField($key)) {
278
                continue;
279
            }
280 7
            $fieldDefinition = $definition->getField($key);
281 7
            $this->setObjectValue($object, $fieldDefinition, $value);
282
        }
283
284 7
        return $object;
285
    }
286
287
    /**
288
     * @param mixed           $object
289
     * @param FieldDefinition $fieldDefinition
290
     * @param mixed           $value
291
     */
292 7
    protected function setObjectValue($object, FieldDefinition $fieldDefinition, $value)
293
    {
294
        //using setter
295 7
        $accessor = new PropertyAccessor();
296 7
        $propertyName = $fieldDefinition->getOriginName();
297 7
        if ($propertyName) {
298 7
            if ($accessor->isWritable($object, $propertyName)) {
299 7
                $accessor->setValue($object, $propertyName, $value);
300
            } else {
301
                //using reflection
302
                $refClass = new \ReflectionClass(\get_class($object));
303
                if ($refClass->hasProperty($fieldDefinition->getOriginName()) && $property = $refClass->getProperty($fieldDefinition->getOriginName())) {
304
                    $property->setAccessible(true);
305
                    $property->setValue($object, $value);
306
                }
307
            }
308
        }
309 7
    }
310
}
311