Completed
Push — master ( 4d2ac5...1a75da )
by Portey
03:12
created

Processor::processQueryFields()   C

Complexity

Conditions 8
Paths 6

Size

Total Lines 24
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 13.3594

Importance

Changes 3
Bugs 0 Features 1
Metric Value
c 3
b 0
f 1
dl 0
loc 24
ccs 9
cts 16
cp 0.5625
rs 5.7377
cc 8
eloc 14
nc 6
nop 4
crap 13.3594
1
<?php
2
/*
3
* This file is a part of graphql-youshido project.
4
*
5
* @author Portey Vasil <[email protected]>
6
* @author Alexandr Viniychuk <[email protected]>
7
* created: 11/28/15 1:05 AM
8
*/
9
10
namespace Youshido\GraphQL;
11
12
use Symfony\Component\PropertyAccess\PropertyAccess;
13
use Symfony\Component\PropertyAccess\PropertyAccessor;
14
use Youshido\GraphQL\Definition\SchemaType;
15
use Youshido\GraphQL\Definition\TypeDefinitionType;
16
use Youshido\GraphQL\Parser\Ast\FragmentReference;
17
use Youshido\GraphQL\Parser\Ast\Mutation;
18
use Youshido\GraphQL\Parser\Ast\Query;
19
use Youshido\GraphQL\Parser\Parser;
20
use Youshido\GraphQL\Type\Field\Field;
21
use Youshido\GraphQL\Type\Object\AbstractEnumType;
22
use Youshido\GraphQL\Type\Object\InputObjectType;
23
use Youshido\GraphQL\Type\Object\ObjectType;
24
use Youshido\GraphQL\Type\Scalar\AbstractScalarType;
25
use Youshido\GraphQL\Type\TypeInterface;
26
use Youshido\GraphQL\Type\TypeMap;
27
use Youshido\GraphQL\Validator\Exception\ResolveException;
28
use Youshido\GraphQL\Validator\ResolveValidator\ResolveValidatorInterface;
29
30
class Processor
31
{
32
33
    const TYPE_NAME_QUERY = '__typename';
34
35
    /** @var  array */
36
    protected $data;
37
38
    /** @var ResolveValidatorInterface */
39
    protected $resolveValidator;
40
41
    /** @var Schema */
42
    protected $schema;
43
44
    /** @var PropertyAccessor */
45
    protected $propertyAccessor;
46
47
    /** @var Request */
48
    protected $request;
49
50 13
    public function __construct(ResolveValidatorInterface $validator)
51
    {
52 13
        $this->resolveValidator = $validator;
53
54 13
        $this->propertyAccessor = PropertyAccess::createPropertyAccessor();
55 13
    }
56
57 13
    public function processQuery($queryString, $variables = [])
58
    {
59 13
        $this->resolveValidator->clearErrors();
60 13
        $this->data = [];
61
62
        try {
63 13
            $this->parseAndCreateRequest($queryString, $variables);
64
65 13
            if ($this->request->hasQueries()) {
66 13
                foreach ($this->request->getQueries() as $query) {
67 13
                    if ($queryResult = $this->executeQuery($query, $this->getSchema()->getQueryType())) {
68 12
                        $this->data = array_merge($this->data, $queryResult);
69 12
                    };
70 13
                }
71 13
            }
72
73 13
            if ($this->request->hasMutations()) {
74
                foreach ($this->request->getMutations() as $mutation) {
75
                    if ($mutationResult = $this->executeMutation($mutation, $this->getSchema()->getMutationType())) {
76
                        $this->data = array_merge($this->data, $mutationResult);
77
                    }
78 1
                }
79
            }
80 13
        } catch (\Exception $e) {
81
            $this->resolveValidator->clearErrors();
82
83
            $this->resolveValidator->addError($e);
84
        }
85 13
    }
86
87
    /**
88
     * @param Mutation        $mutation
89
     * @param InputObjectType $objectType
90
     *
91
     * @return array|bool|mixed
92
     */
93
    protected function executeMutation($mutation, $objectType)
94
    {
95
        if (!$this->checkFieldExist($objectType, $mutation)) {
96
97
            return null;
98
        }
99
100
        /** @var Field $field */
101
        $field = $objectType->getConfig()->getField($mutation->getName());
102
103
        if (!$this->resolveValidator->validateArguments($field, $mutation, $this->request)) {
104
            return null;
105
        }
106
107
        $alias         = $mutation->hasAlias() ? $mutation->getAlias() : $mutation->getName();
108
        $resolvedValue = $this->resolveValue($field, null, $mutation);
109
110
        if (!$this->resolveValidator->validateResolvedValue($resolvedValue, $field->getType()->getKind())) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
111
            $this->resolveValidator->addError(new ResolveException(sprintf('Not valid resolved value for mutation "%s"', $field->getType()->getName())));
112
113
            return [$alias => null];
114
        }
115
116
        $value = null;
117
        if ($mutation->hasFields()) {
118
            $outputType = $field->getType()->getConfig()->getOutputType();
119
120
            $value = $this->processQueryFields($mutation, $outputType, $resolvedValue, []);
121
        }
122
123
        return [$alias => $value];
124
    }
125
126 13
    protected function parseAndCreateRequest($query, $variables = [])
127
    {
128 13
        $parser = new Parser();
129
130 13
        $parser->setSource($query);
131 13
        $data = $parser->parse();
132
133 13
        $this->request = new Request($data);
134 13
        $this->request->setVariables($variables);
135 13
    }
136
137
    /**
138
     * @param Query|Field $query
139
     * @param ObjectType  $currentLevelSchema
140
     * @param null        $contextValue
141
     * @return array|bool|mixed
142
     */
143 13
    protected function executeQuery($query, $currentLevelSchema, $contextValue = null)
144
    {
145 13
        if (!$this->checkFieldExist($currentLevelSchema, $query)) {
146
147
            return null;
148
        }
149
150
        /** @todo need to check if the correct field class is used here */
151
152
        /** @var Field $field */
153 13
        $field = $currentLevelSchema->getConfig()->getField($query->getName());
154 13
        if (get_class($query) == 'Youshido\GraphQL\Parser\Ast\Field') {
155 11
            $alias            = $query->getName();
156 11
            $preResolvedValue = $this->getPreResolvedValue($contextValue, $query);
157
158 11
            if ($field->getConfig()->getType()->getKind() == TypeMap::KIND_LIST) {
159 1
                if(!is_array($preResolvedValue)){
160
                    $value = null;
0 ignored issues
show
Unused Code introduced by
$value is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
161
                    $this->resolveValidator->addError(new ResolveException('Not valid resolve value for list type'));
162
                }
163
164
165 1
                $listValue = [];
166 1
                foreach ($preResolvedValue as $resolvedValueItem) {
0 ignored issues
show
Bug introduced by
The expression $preResolvedValue of type object|integer|double|string|null|boolean|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
167
                    /** @var TypeInterface $type */
168 1
                    $type = $field->getType()->getConfig()->getItem();
169
170 1
                    if ($type->getKind() == TypeMap::KIND_ENUM) {
171
                        /** @var $type AbstractEnumType */
172 1
                        if(!$type->isValidValue($resolvedValueItem)) {
173
                            $this->resolveValidator->addError(new ResolveException('Not valid value for enum type'));
174
175
                            $listValue = null;
176
                            break;
177
                        }
178
179 1
                        $listValue[] = $type->resolve($resolvedValueItem);
180 1
                    } else {
181
                        /** @var AbstractScalarType $type */
182
                        $listValue[] = $type->serialize($preResolvedValue);
183
                    }
184 1
                }
185
186 1
                $value = $listValue;
187 1
            } else {
188 11
                if ($field->getType()->getKind() == TypeMap::KIND_ENUM) {
189
                    if(!$field->getType()->isValidValue($preResolvedValue)) {
190
                        $this->resolveValidator->addError(new ResolveException('Not valid value for enum type'));
191
                        $value = null;
192
                    } else {
193
                        $value = $field->getType()->resolve($preResolvedValue);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Youshido\GraphQL\Type\TypeInterface as the method resolve() does only exist in the following implementations of said interface: Youshido\GraphQL\Definition\ArgumentListType, Youshido\GraphQL\Definition\ArgumentType, Youshido\GraphQL\Definition\EnumValueListType, Youshido\GraphQL\Definition\EnumValueType, Youshido\GraphQL\Definition\FieldListType, Youshido\GraphQL\Definition\FieldType, Youshido\GraphQL\Definition\InputValueListType, Youshido\GraphQL\Definition\InputValueType, Youshido\GraphQL\Definition\InterfaceListType, Youshido\GraphQL\Definition\InterfaceType, Youshido\GraphQL\Definition\MutationType, Youshido\GraphQL\Definition\PossibleOfListType, Youshido\GraphQL\Definition\PossibleOfType, Youshido\GraphQL\Definition\QueryListType, Youshido\GraphQL\Definition\QueryType, Youshido\GraphQL\Definition\SchemaType, Youshido\GraphQL\Definition\TypeDefinitionType, Youshido\GraphQL\Type\ListType\AbstractListType, Youshido\GraphQL\Type\ListType\ListType, Youshido\GraphQL\Type\Object\AbstractEnumType, Youshido\GraphQL\Type\Ob...AbstractInputObjectType, Youshido\GraphQL\Type\Object\AbstractObjectType, Youshido\GraphQL\Type\Object\EnumType, Youshido\GraphQL\Type\Object\InputObjectType, Youshido\GraphQL\Type\Object\ObjectType, Youshido\Tests\DataProvider\UserType, Youshido\Tests\Schema\DroidType, Youshido\Tests\Schema\EpisodeEnum, Youshido\Tests\Schema\HumanType, Youshido\Tests\Schema\QueryType.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
194
                    }
195
                } else {
196 11
                    $value = $field->getType()->serialize($preResolvedValue);
197
                }
198
            }
199 11
        } else {
200 13
            if (!$this->resolveValidator->validateArguments($field, $query, $this->request)) {
201 1
                return null;
202
            }
203
204 12
            $resolvedValue = $this->resolveValue($field, $contextValue, $query);
205 12
            $alias         = $query->hasAlias() ? $query->getAlias() : $query->getName();
0 ignored issues
show
Bug introduced by
The method hasAlias does only exist in Youshido\GraphQL\Parser\Ast\Query, but not in Youshido\GraphQL\Type\Field\Field.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
Bug introduced by
The method getAlias does only exist in Youshido\GraphQL\Parser\Ast\Query, but not in Youshido\GraphQL\Type\Field\Field.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
206
207 12
            if (!$this->resolveValidator->validateResolvedValue($resolvedValue, $field->getType()->getKind())) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
208
                $this->resolveValidator->addError(new ResolveException(sprintf('Not valid resolved value for query "%s"', $field->getType()->getName())));
209
210
                return [$alias => null];
211
            }
212
213 12
            $value = [];
214 12
            if ($resolvedValue) {
215 11
                if ($field->getType()->getKind() == TypeMap::KIND_LIST) {
216 4
                    foreach ($resolvedValue as $resolvedValueItem) {
217 4
                        $value[] = [];
218 4
                        $index   = count($value) - 1;
219
220 4
                        if($field->getConfig()->getType()->getConfig()->getItem()->getKind() == TypeMap::KIND_INTERFACE) {
221 2
                            $resolvedValueItem = $field->getConfig()->getType()->getConfig()->getItemConfig()->resolveType($resolvedValueItem);
222 2
                            $type = $field->getConfig()->getType()->getConfig()->getItem();
223 2
                        } else {
224 2
                            $type = $field->getType();
225
                        }
226
227 4
                        $value[$index] = $this->processQueryFields($query, $type, $resolvedValueItem, $value[$index]);
228 4
                    }
229 4
                } else {
230 11
                    $value = $this->processQueryFields($query, $field->getType(), $resolvedValue, $value);
231
                }
232 11
            } else {
233 3
                $value = $resolvedValue;
234
            }
235
        }
236
237 12
        return [$alias => $value];
238
    }
239
240
    /**
241
     * @param $objectType InputObjectType|ObjectType
242
     * @param $query      Mutation|Query
243
     * @return null
244
     */
245 13
    private function checkFieldExist($objectType, $query)
246
    {
247 13
        if (!$objectType->getConfig()->hasField($query->getName())) {
248
            $this->resolveValidator->addError(new ResolveException(sprintf('Field "%s" not found in schema', $query->getName())));
249
250
            return false;
251
        }
252
253 13
        return true;
254
    }
255
256
    /**
257
     * @param $value
258
     * @param $query Field
259
     *
260
     * @throws \Exception
261
     *
262
     * @return mixed
263
     */
264 11
    protected function getPreResolvedValue($value, $query)
265
    {
266 11
        if (is_array($value)) {
267 7
            if (array_key_exists($query->getName(), $value)) {
268 7
                return $value[$query->getName()];
269
            } else {
270
                throw new \Exception('Not found in resolve result', $query->getName());
271
            }
272 4
        } elseif (is_object($value)) {
273 4
            return $this->propertyAccessor->getValue($value, $query->getName());
274
        }
275
276
        return $value;
277
    }
278
279
    /**
280
     * @param $field        Field
281
     * @param $contextValue mixed
282
     * @param $query        Query
283
     *
284
     * @return mixed
285
     */
286 12
    protected function resolveValue($field, $contextValue, $query)
287
    {
288 12
        $resolvedValue = $field->getConfig()->resolve($contextValue, $this->parseArgumentsValues($field, $query));
289
290 12
        if($field->getType()->getKind() == TypeMap::KIND_INTERFACE){
291 4
            $resolvedValue = $field->getType()->resolveType($resolvedValue);
292 4
        }
293
294 12
        return $resolvedValue;
295
    }
296
297
    /**
298
     * @param $field     Field
299
     * @param $query     Query
300
     *
301
     * @return array
302
     */
303 12
    public function parseArgumentsValues($field, $query)
304
    {
305 12
        if ($query instanceof \Youshido\GraphQL\Parser\Ast\Field) {
306
            return [];
307
        }
308
309 12
        $args      = [];
310 12
        foreach ($query->getArguments() as $argument) {
311 5
            $args[$argument->getName()] = $field->getConfig()->getArgument($argument->getName())->getType()->parseValue($argument->getValue()->getValue());
312 12
        }
313
314 12
        return $args;
315
    }
316
317
    /**
318
     * @param $query         Query
319
     * @param $queryType     ObjectType|TypeInterface|Field
320
     * @param $resolvedValue mixed
321
     * @param $value         array
322
     *
323
     * @throws \Exception
324
     *
325
     * @return array
326
     */
327 11
    protected function processQueryFields($query, $queryType, $resolvedValue, $value)
328
    {
329 11
        foreach ($query->getFields() as $field) {
330 11
            if ($field instanceof FragmentReference) {
331
                if (!$fragment = $this->request->getFragment($field->getName())) {
332
                    throw new \Exception(sprintf('Fragment reference "%s" not found', $field->getName()));
333
                }
334
335
                if ($fragment->getModel() !== $queryType->getName()) {
336
                    throw new \Exception(sprintf('Fragment reference "%s" not found on model "%s"', $field->getName(), $queryType->getName()));
337
                }
338
339
                foreach ($fragment->getFields() as $fragmentField) {
340
                    $value = $this->collectValue($value, $this->executeQuery($fragmentField, $queryType, $resolvedValue));
341
                }
342 11
            } else if($field->getName() == self::TYPE_NAME_QUERY) {
343 1
                $value = $this->collectValue($value, [$field->getAlias() ?: $field->getName() => $queryType->getName()]);
344 1
            } else {
345 11
                $value = $this->collectValue($value, $this->executeQuery($field, $queryType, $resolvedValue));
346
            }
347 11
        }
348
349 11
        return $value;
350
    }
351
352 11
    protected function collectValue($value, $queryValue)
353
    {
354 11
        if ($queryValue && is_array($queryValue)) {
355 11
            $value = array_merge($value, $queryValue);
356 11
        } else {
357
            $value = $queryValue;
358
        }
359
360 11
        return $value;
361
    }
362
363 13
    public function getSchema()
364
    {
365 13
        return $this->schema;
366
    }
367
368 13
    public function setSchema(Schema $schema)
369
    {
370 13
        $this->schema = $schema;
371
372 13
        $__schema = new SchemaType();
373 13
        $__schema->setSchema($schema);
374
375 13
        $__type = new TypeDefinitionType();
376 13
        $__type->setSchema($schema);
377
378 13
        $this->schema->addQuery('__schema', $__schema);
379 13
        $this->schema->addQuery('__type', $__type);
380 13
    }
381
382
    /**
383
     * @return ResolveValidatorInterface
384
     */
385
    public function getResolveValidator()
386
    {
387
        return $this->resolveValidator;
388
    }
389
390 13
    public function getResponseData()
391
    {
392 13
        $result = [];
393
394 13
        if (!empty($this->data)) {
395 12
            $result['data'] = $this->data;
396 12
        }
397
398 13
        if ($this->resolveValidator->hasErrors()) {
399 1
            $result['errors'] = $this->resolveValidator->getErrorsArray();
400 1
        }
401
402 13
        return $result;
403
    }
404
}
405