Completed
Push — master ( cbfed7...c39fb6 )
by Rafael
05:06
created

ResolverExecutor::resolveMethodArguments()   D

Complexity

Conditions 9
Paths 11

Size

Total Lines 26
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 9

Importance

Changes 0
Metric Value
cc 9
eloc 14
nc 11
nop 2
dl 0
loc 26
ccs 15
cts 15
cp 1
crap 9
rs 4.909
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\FieldDefinition;
21
use Ynlo\GraphQLBundle\Definition\ObjectDefinitionInterface;
22
use Ynlo\GraphQLBundle\Definition\QueryDefinition;
23
use Ynlo\GraphQLBundle\Definition\Registry\DefinitionManager;
24
use Ynlo\GraphQLBundle\Model\ID;
25
26
/**
27
 * This resolver act as a middleware between the query 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 QueryDefinition
36
     */
37
    protected $query;
38
39
    /**
40
     * @var DefinitionManager
41
     */
42
    protected $manager;
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 DefinitionManager  $manager
67
     * @param QueryDefinition    $query
68
     */
69 2
    public function __construct(ContainerInterface $container, DefinitionManager $manager, QueryDefinition $query)
70
    {
71 2
        $this->query = $query;
72 2
        $this->manager = $manager;
73 2
        $this->container = $container;
74 2
    }
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 14
    public function __invoke($root, array $args, $context, ResolveInfo $resolveInfo)
87
    {
88 14
        $this->root = $root;
89 14
        $this->args = $args;
90 14
        $this->context = $context;
91 14
        $this->resolveInfo = $resolveInfo;
92
93 14
        $resolverName = $this->query->getResolver();
94
95 14
        $resolver = null;
96 14
        $refMethod = null;
97
98 14
        if (class_exists($resolverName)) {
99 14
            $refClass = new \ReflectionClass($resolverName);
100
101
            /** @var callable $resolver */
102 14
            $resolver = $refClass->newInstance();
103 14
            if ($resolver instanceof ContainerAwareInterface) {
104 14
                $resolver->setContainer($this->container);
105
            }
106 14
            if ($refClass->hasMethod('__invoke')) {
107 14
                $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 14
        if ($resolver && $refMethod) {
115 14
            $resolveContext = new ResolverContext();
116 14
            $resolveContext->setDefinition($this->query);
117 14
            $resolveContext->setArgs($args);
118 14
            $resolveContext->setRoot($root);
119 14
            $resolveContext->setDefinitionManager($this->manager);
120 14
            $resolveContext->setResolveInfo($resolveInfo);
121
122 14
            if ($resolver instanceof AbstractResolver) {
123 14
                $resolver->setContext($resolveContext);
124
            }
125
126
            //A very strange issue are causing the fail of some tests without this clear
127
            //everything indicates that is a issue with cached entities through test executions
128
            //any clear on tests or during load fixtures does not have any effect
129
            //I'm not sure if this patch has any other side effect
130
            //Reproduce: comment the following line and run all integration tests
131
            //FIXME: find the cause of the issue and fix it
132 14
            $this->container->get('doctrine')->getManager()->clear();
133 14
            $params = $this->prepareMethodParameters($refMethod, $args);
134
135 14
            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

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