Completed
Push — master ( de8c12...6bfa1f )
by Portey
05:07
created

Processor::processQueryFields()   C

Complexity

Conditions 11
Paths 8

Size

Total Lines 32
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 14.8984

Importance

Changes 4
Bugs 0 Features 1
Metric Value
c 4
b 0
f 1
dl 0
loc 32
ccs 15
cts 22
cp 0.6818
rs 5.2653
cc 11
eloc 19
nc 8
nop 4
crap 14.8984

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\Ast\TypedFragmentReference;
20
use Youshido\GraphQL\Parser\Parser;
21
use Youshido\GraphQL\Type\Field\Field;
22
use Youshido\GraphQL\Type\Object\AbstractEnumType;
23
use Youshido\GraphQL\Type\Object\InputObjectType;
24
use Youshido\GraphQL\Type\Object\ObjectType;
25
use Youshido\GraphQL\Type\Scalar\AbstractScalarType;
26
use Youshido\GraphQL\Type\TypeInterface;
27
use Youshido\GraphQL\Type\TypeMap;
28
use Youshido\GraphQL\Validator\Exception\ResolveException;
29
use Youshido\GraphQL\Validator\ResolveValidator\ResolveValidatorInterface;
30
31
class Processor
32
{
33
34
    const TYPE_NAME_QUERY = '__typename';
35
36
    /** @var  array */
37
    protected $data;
38
39
    /** @var ResolveValidatorInterface */
40
    protected $resolveValidator;
41
42
    /** @var Schema */
43
    protected $schema;
44
45
    /** @var PropertyAccessor */
46
    protected $propertyAccessor;
47
48
    /** @var Request */
49
    protected $request;
50
51 17
    public function __construct(ResolveValidatorInterface $validator)
52
    {
53 17
        $this->resolveValidator = $validator;
54
55 17
        $this->propertyAccessor = PropertyAccess::createPropertyAccessor();
56 17
    }
57
58 17
    public function processQuery($queryString, $variables = [])
59
    {
60 17
        $this->resolveValidator->clearErrors();
61 17
        $this->data = [];
62
63 1
        try {
64 17
            $this->parseAndCreateRequest($queryString, $variables);
65
66 17
            if ($this->request->hasQueries()) {
67 17
                foreach ($this->request->getQueries() as $query) {
68 17
                    if ($queryResult = $this->executeQuery($query, $this->getSchema()->getQueryType())) {
69 16
                        $this->data = array_merge($this->data, $queryResult);
70 16
                    };
71 17
                }
72 17
            }
73
74 17
            if ($this->request->hasMutations()) {
75
                foreach ($this->request->getMutations() as $mutation) {
76
                    if ($mutationResult = $this->executeMutation($mutation, $this->getSchema()->getMutationType())) {
77
                        $this->data = array_merge($this->data, $mutationResult);
78 1
                    }
79
                }
80
            }
81 17
        } catch (\Exception $e) {
82
            $this->resolveValidator->clearErrors();
83
84
            $this->resolveValidator->addError($e);
85
        }
86 17
    }
87
88
    /**
89
     * @param Mutation        $mutation
90
     * @param InputObjectType $objectType
91
     *
92
     * @return array|bool|mixed
93
     */
94
    protected function executeMutation($mutation, $objectType)
95
    {
96
        if (!$this->checkFieldExist($objectType, $mutation)) {
97
98
            return null;
99
        }
100
101
        /** @var Field $field */
102
        $field = $objectType->getConfig()->getField($mutation->getName());
103
104
        if (!$this->resolveValidator->validateArguments($field, $mutation, $this->request)) {
105
            return null;
106
        }
107
108
        $alias         = $mutation->hasAlias() ? $mutation->getAlias() : $mutation->getName();
109
        $resolvedValue = $this->resolveValue($field, null, $mutation);
110
111
        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...
112
            $this->resolveValidator->addError(new ResolveException(sprintf('Not valid resolved value for mutation "%s"', $field->getType()->getName())));
113
114
            return [$alias => null];
115
        }
116
117
        $value = null;
118
        if ($mutation->hasFields()) {
119
            $outputType = $field->getType()->getConfig()->getOutputType();
120
121
            $value = $this->processQueryFields($mutation, $outputType, $resolvedValue, []);
122
        }
123
124
        return [$alias => $value];
125
    }
126
127 17
    protected function parseAndCreateRequest($query, $variables = [])
128
    {
129 17
        $parser = new Parser();
130
131 17
        $parser->setSource($query);
132 17
        $data = $parser->parse();
133
134 17
        $this->request = new Request($data);
135 17
        $this->request->setVariables($variables);
136 17
    }
137
138
    /**
139
     * @param Query|Field $query
140
     * @param ObjectType  $currentLevelSchema
141
     * @param null        $contextValue
142
     * @return array|bool|mixed
143
     */
144 17
    protected function executeQuery($query, $currentLevelSchema, $contextValue = null)
145
    {
146 17
        if (!$this->checkFieldExist($currentLevelSchema, $query)) {
147
            return null;
148
        }
149
150
        /** @var Field $field */
151 17
        $field = $currentLevelSchema->getConfig()->getField($query->getName());
152 17
        if (get_class($query) == 'Youshido\GraphQL\Parser\Ast\Field') {
153 15
            $alias            = $query->getName();
154 15
            $preResolvedValue = $this->getPreResolvedValue($contextValue, $query);
155
156 15
            if ($field->getConfig()->getType()->getKind() == TypeMap::KIND_LIST) {
157 1
                if(!is_array($preResolvedValue)){
158
                    $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...
159
                    $this->resolveValidator->addError(new ResolveException('Not valid resolve value for list type'));
160
                }
161
162
163 1
                $listValue = [];
164 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...
165
                    /** @var TypeInterface $type */
166 1
                    $type = $field->getType()->getConfig()->getItem();
167
168 1
                    if ($type->getKind() == TypeMap::KIND_ENUM) {
169
                        /** @var $type AbstractEnumType */
170 1
                        if(!$type->isValidValue($resolvedValueItem)) {
171
                            $this->resolveValidator->addError(new ResolveException('Not valid value for enum type'));
172
173
                            $listValue = null;
174
                            break;
175
                        }
176
177 1
                        $listValue[] = $type->resolve($resolvedValueItem);
178 1
                    } else {
179
                        /** @var AbstractScalarType $type */
180
                        $listValue[] = $type->serialize($preResolvedValue);
181
                    }
182 1
                }
183
184 1
                $value = $listValue;
185 1
            } else {
186 15
                if ($field->getType()->getKind() == TypeMap::KIND_ENUM) {
187
                    if(!$field->getType()->isValidValue($preResolvedValue)) {
188
                        $this->resolveValidator->addError(new ResolveException('Not valid value for enum type'));
189
                        $value = null;
190
                    } else {
191
                        $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\StarWars\Schema\DroidType, Youshido\Tests\StarWars\Schema\EpisodeEnum, Youshido\Tests\StarWars\Schema\HumanType, Youshido\Tests\StarWars\Schema\QueryType, Youshido\Tests\Type\Union\Schema\FirstType, Youshido\Tests\Type\Union\Schema\QueryType, Youshido\Tests\Type\Union\Schema\SecondType.

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...
192
                    }
193
                } else {
194 15
                    $value = $field->getType()->serialize($preResolvedValue);
195
                }
196
            }
197 15
        } else {
198 17
            if (!$this->resolveValidator->validateArguments($field, $query, $this->request)) {
199 1
                return null;
200
            }
201
202 16
            $resolvedValue = $this->resolveValue($field, $contextValue, $query);
203 16
            $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...
204
205 16
            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...
206
                $this->resolveValidator->addError(new ResolveException(sprintf('Not valid resolved value for query "%s"', $field->getType()->getName())));
207
208
                return [$alias => null];
209
            }
210
211 16
            $value = [];
212 16
            if ($resolvedValue) {
213 15
                if ($field->getType()->getKind() == TypeMap::KIND_LIST) {
214 6
                    foreach ($resolvedValue as $resolvedValueItem) {
215 6
                        $value[] = [];
216 6
                        $index   = count($value) - 1;
217
218 6
                        if(in_array($field->getConfig()->getType()->getConfig()->getItem()->getKind(), [TypeMap::KIND_UNION, TypeMap::KIND_INTERFACE]) ) {
219 4
                            $type = $field->getConfig()->getType()->getConfig()->getItemConfig()->resolveType($resolvedValueItem);
220 4
                        } else {
221 2
                            $type = $field->getType();
222
                        }
223
224 6
                        $value[$index] = $this->processQueryFields($query, $type, $resolvedValueItem, $value[$index]);
225 6
                    }
226 6
                } else {
227 13
                    $value = $this->processQueryFields($query, $field->getType(), $resolvedValue, $value);
228
                }
229 15
            } else {
230 3
                $value = $resolvedValue;
231
            }
232
        }
233
234 16
        return [$alias => $value];
235
    }
236
237
    /**
238
     * @param $objectType InputObjectType|ObjectType
239
     * @param $query      Mutation|Query
240
     * @return null
241
     */
242 17
    private function checkFieldExist($objectType, $query)
243
    {
244 17
        if (!$objectType->getConfig()->hasField($query->getName())) {
245
            $this->resolveValidator->addError(new ResolveException(sprintf('Field "%s" not found in schema', $query->getName())));
246
247
            return false;
248
        }
249
250 17
        return true;
251
    }
252
253
    /**
254
     * @param $value
255
     * @param $query Field
256
     *
257
     * @throws \Exception
258
     *
259
     * @return mixed
260
     */
261 15
    protected function getPreResolvedValue($value, $query)
262
    {
263 15
        if (is_array($value)) {
264 11
            if (array_key_exists($query->getName(), $value)) {
265 11
                return $value[$query->getName()];
266
            } else {
267
                throw new \Exception('Not found in resolve result', $query->getName());
268
            }
269 4
        } elseif (is_object($value)) {
270 4
            return $this->propertyAccessor->getValue($value, $query->getName());
271
        }
272
273
        return $value;
274
    }
275
276
    /**
277
     * @param $field        Field
278
     * @param $contextValue mixed
279
     * @param $query        Query
280
     *
281
     * @return mixed
282
     */
283 16
    protected function resolveValue($field, $contextValue, $query)
284
    {
285 16
        $resolvedValue = $field->getConfig()->resolve($contextValue, $this->parseArgumentsValues($field, $query));
286
287 16
        if(in_array($field->getType()->getKind(), [TypeMap::KIND_UNION, TypeMap::KIND_INTERFACE])){
288 6
            $resolvedType = $field->getType()->resolveType($resolvedValue);
289 6
            $field->setType($resolvedType);
290 6
        }
291
292 16
        return $resolvedValue;
293
    }
294
295
    /**
296
     * @param $field     Field
297
     * @param $query     Query
298
     *
299
     * @return array
300
     */
301 16
    public function parseArgumentsValues($field, $query)
302
    {
303 16
        if ($query instanceof \Youshido\GraphQL\Parser\Ast\Field) {
304
            return [];
305
        }
306
307 16
        $args      = [];
308 16
        foreach ($query->getArguments() as $argument) {
309 5
            $args[$argument->getName()] = $field->getConfig()->getArgument($argument->getName())->getType()->parseValue($argument->getValue()->getValue());
310 16
        }
311
312 16
        return $args;
313
    }
314
315
    /**
316
     * @param $query         Query
317
     * @param $queryType     ObjectType|TypeInterface|Field
318
     * @param $resolvedValue mixed
319
     * @param $value         array
320
     *
321
     * @throws \Exception
322
     *
323
     * @return array
324
     */
325 15
    protected function processQueryFields($query, $queryType, $resolvedValue, $value)
326
    {
327 15
        foreach ($query->getFields() as $field) {
328 15
            if ($field instanceof FragmentReference) {
329
                if (!$fragment = $this->request->getFragment($field->getName())) {
330
                    throw new \Exception(sprintf('Fragment reference "%s" not found', $field->getName()));
331
                }
332
333
                if ($fragment->getModel() !== $queryType->getName()) {
334
                    throw new \Exception(sprintf('Fragment reference "%s" not found on model "%s"', $field->getName(), $queryType->getName()));
335
                }
336
337
                foreach ($fragment->getFields() as $fragmentField) {
338
                    $value = $this->collectValue($value, $this->executeQuery($fragmentField, $queryType, $resolvedValue));
339
                }
340 15
            } elseif ($field instanceof TypedFragmentReference) {
341 2
                if ($field->getTypeName() !== $queryType->getName()) {
342 1
                    continue;
343
                }
344
345 2
                foreach ($field->getFields() as $fragmentField) {
346 2
                    $value = $this->collectValue($value, $this->executeQuery($fragmentField, $queryType, $resolvedValue));
0 ignored issues
show
Documentation introduced by
$fragmentField is of type object<Youshido\GraphQL\Parser\Ast\Field>, but the function expects a object<Youshido\GraphQL\...aphQL\Type\Field\Field>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
347 2
                }
348 15
            } elseif ($field->getName() == self::TYPE_NAME_QUERY) {
349 1
                $value = $this->collectValue($value, [$field->getAlias() ?: $field->getName() => $queryType->getName()]);
350 1
            } else {
351 15
                $value = $this->collectValue($value, $this->executeQuery($field, $queryType, $resolvedValue));
352
            }
353 15
        }
354
355 15
        return $value;
356
    }
357
358 15
    protected function collectValue($value, $queryValue)
359
    {
360 15
        if ($queryValue && is_array($queryValue)) {
361 15
            $value = array_merge($value, $queryValue);
362 15
        } else {
363
            $value = $queryValue;
364
        }
365
366 15
        return $value;
367
    }
368
369 17
    public function getSchema()
370
    {
371 17
        return $this->schema;
372
    }
373
374 17
    public function setSchema(Schema $schema)
375
    {
376 17
        $this->schema = $schema;
377
378 17
        $__schema = new SchemaType();
379 17
        $__schema->setSchema($schema);
380
381 17
        $__type = new TypeDefinitionType();
382 17
        $__type->setSchema($schema);
383
384 17
        $this->schema->addQuery('__schema', $__schema);
385 17
        $this->schema->addQuery('__type', $__type);
386 17
    }
387
388
    /**
389
     * @return ResolveValidatorInterface
390
     */
391
    public function getResolveValidator()
392
    {
393
        return $this->resolveValidator;
394
    }
395
396 17
    public function getResponseData()
397
    {
398 17
        $result = [];
399
400 17
        if (!empty($this->data)) {
401 16
            $result['data'] = $this->data;
402 16
        }
403
404 17
        if ($this->resolveValidator->hasErrors()) {
405 1
            $result['errors'] = $this->resolveValidator->getErrorsArray();
406 1
        }
407
408 17
        return $result;
409
    }
410
}
411