Completed
Pull Request — master (#15)
by
unknown
03:53
created

Processor::processFieldAST()   B

Complexity

Conditions 4
Paths 3

Size

Total Lines 27
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 4.1054

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 27
ccs 13
cts 16
cp 0.8125
rs 8.5806
cc 4
eloc 17
nc 3
nop 3
crap 4.1054
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\Execution;
11
12
use Youshido\GraphQL\Execution\Context\ExecutionContext;
13
use Youshido\GraphQL\Execution\Visitor\AbstractQueryVisitor;
14
use Youshido\GraphQL\Field\AbstractField;
15
use Youshido\GraphQL\Field\Field;
16
use Youshido\GraphQL\Introspection\Field\SchemaField;
17
use Youshido\GraphQL\Introspection\Field\TypeDefinitionField;
18
use Youshido\GraphQL\Parser\Ast\Field as FieldAst;
19
use Youshido\GraphQL\Parser\Ast\Fragment;
20
use Youshido\GraphQL\Parser\Ast\FragmentInterface;
21
use Youshido\GraphQL\Parser\Ast\FragmentReference;
22
use Youshido\GraphQL\Parser\Ast\Mutation;
23
use Youshido\GraphQL\Parser\Ast\Query;
24
use Youshido\GraphQL\Parser\Ast\TypedFragmentReference;
25
use Youshido\GraphQL\Parser\Parser;
26
use Youshido\GraphQL\Schema\AbstractSchema;
27
use Youshido\GraphQL\Type\AbstractType;
28
use Youshido\GraphQL\Type\Object\AbstractObjectType;
29
use Youshido\GraphQL\Type\TypeInterface;
30
use Youshido\GraphQL\Type\TypeMap;
31
use Youshido\GraphQL\Type\TypeService;
32
use Youshido\GraphQL\Type\Union\AbstractUnionType;
33
use Youshido\GraphQL\Validator\Exception\ResolveException;
34
use Youshido\GraphQL\Validator\ResolveValidator\ResolveValidator;
35
use Youshido\GraphQL\Validator\ResolveValidator\ResolveValidatorInterface;
36
use Youshido\GraphQL\Validator\SchemaValidator\SchemaValidator;
37
38
class Processor
39
{
40
41
    const TYPE_NAME_QUERY = '__typename';
42
43
    /** @var  array */
44
    protected $data;
45
46
    /** @var ResolveValidatorInterface */
47
    protected $resolveValidator;
48
49
    /** @var ExecutionContext */
50
    protected $executionContext;
51
52
    /** @var #maxComplexity */
53
    protected $maxComplexity;
54
55 24
    public function __construct(AbstractSchema $schema)
56
    {
57 24
        (new SchemaValidator())->validate($schema);
58
59 23
        $this->introduceIntrospectionFields($schema);
60 23
        $this->executionContext = new ExecutionContext();
61 23
        $this->executionContext->setSchema($schema);
62
63 23
        $this->resolveValidator = new ResolveValidator($this->executionContext);
64 23
    }
65
66
67 23
    public function processPayload($payload, $variables = [], $reducers = [])
68
    {
69 23
        if ($this->executionContext->hasErrors()) {
70 4
            $this->executionContext->clearErrors();
71
        }
72
73 23
        $this->data = [];
74
75
        try {
76 23
            $this->parseAndCreateRequest($payload, $variables);
77
78 23
            $queryType    = $this->executionContext->getSchema()->getQueryType();
79 23
            $mutationType = $this->executionContext->getSchema()->getMutationType();
80
81 23
            if ($this->maxComplexity) {
82 1
                $reducers[] = new \Youshido\GraphQL\Execution\Visitor\MaxComplexityQueryVisitor($this->maxComplexity);
83
            }
84
85 23
            $this->reduceQuery($queryType, $mutationType, $reducers);
86
87 23
            foreach ($this->executionContext->getRequest()->getOperationsInOrder() as $operation) {
88 23
                if ($operationResult = $this->executeOperation($operation, $operation instanceof Mutation ? $mutationType : $queryType)) {
89 23
                    $this->data = array_merge($this->data, $operationResult);
90
                };
91
            }
92
93 4
        } catch (\Exception $e) {
94 4
            $this->executionContext->addError($e);
95
        }
96
97 23
        return $this;
98
    }
99
100 23
    protected function parseAndCreateRequest($payload, $variables = [])
101
    {
102 23
        if (empty($payload)) {
103 1
            throw new \Exception('Must provide an operation.');
104
        }
105 23
        $parser = new Parser();
106
107 23
        $data = $parser->parse($payload);
108 23
        $this->executionContext->setRequest(new Request($data, $variables));
109 23
    }
110
111
    /**
112
     * @param Query|Field        $query
113
     * @param AbstractObjectType $currentLevelSchema
114
     * @return array|bool|mixed
115
     */
116 23
    protected function executeOperation(Query $query, $currentLevelSchema)
117
    {
118 23
        if (!$this->resolveValidator->objectHasField($currentLevelSchema, $query)) {
119 1
            return null;
120
        }
121
122
        /** @var AbstractField $field */
123 23
        $operationField = $currentLevelSchema->getField($query->getName());
124 23
        $alias          = $query->getAlias() ?: $query->getName();
125
126 23
        if (!$this->resolveValidator->validateArguments($operationField, $query, $this->executionContext->getRequest())) {
127 3
            return null;
128
        }
129
130 21
        return [$alias => $this->processQueryAST($query, $operationField)];
131
    }
132
133
    /**
134
     * @param Query         $query
135
     * @param AbstractField $field
136
     * @param               $contextValue
137
     * @return array|mixed|null
138
     */
139 21
    protected function processQueryAST(Query $query, AbstractField $field, $contextValue = null)
140
    {
141 21
        if (!$this->resolveValidator->validateArguments($field, $query, $this->executionContext->getRequest())) {
142
            return null;
143
        }
144
145 21
        $resolvedValue = $this->resolveFieldValue($field, $contextValue, $query);
146
147 21
        if (!$this->resolveValidator->isValidValueForField($field, $resolvedValue)) {
148 2
            return null;
149
        }
150
151 21
        return $this->collectValueForQueryWithType($query, $field->getType(), $resolvedValue);
152
    }
153
154
    /**
155
     * @param Query|Mutation $query
156
     * @param AbstractType   $fieldType
157
     * @param mixed          $resolvedValue
158
     * @return array|mixed
159
     */
160 21
    protected function collectValueForQueryWithType(Query $query, AbstractType $fieldType, $resolvedValue)
161
    {
162 21
        $fieldType = $this->resolveValidator->resolveTypeIfAbstract($fieldType, $resolvedValue);
163 21
        if (is_null($resolvedValue)) return null;
164
165 19
        $value = [];
166 19
        if ($fieldType->getKind() == TypeMap::KIND_LIST) {
167 8
            foreach ($resolvedValue as $resolvedValueItem) {
0 ignored issues
show
Bug introduced by
The expression $resolvedValue of type object|integer|double|string|array|boolean 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...
168 7
                $value[] = [];
169 7
                $index   = count($value) - 1;
170
171
172 7
                $namedType = $fieldType->getNamedType();
173 7
                $namedType = $this->resolveValidator->resolveTypeIfAbstract($namedType, $resolvedValueItem);
174 7
                if (!$namedType->isValidValue($resolvedValueItem)) {
175 1
                    $this->executionContext->addError(new ResolveException(sprintf('Not valid resolve value in %s field', $query->getName())));
176 1
                    $value[$index] = null;
177 1
                    continue;
178
                }
179
180 8
                $value[$index] = $this->processQueryFields($query, $namedType, $resolvedValueItem, $value[$index]);
181
            }
182
        } else {
183 19
            if (!$query->hasFields()) {
184 2
                return $this->getOutputValue($fieldType, $resolvedValue);
185
            }
186
187 19
            $value = $this->processQueryFields($query, $fieldType, $resolvedValue, $value);
188
        }
189
190 19
        return $value;
191
    }
192
193
    /**
194
     * @param FieldAst      $fieldAst
195
     * @param AbstractField $field
196
     *
197
     * @param mixed         $contextValue
198
     * @return array|mixed|null
199
     * @throws ResolveException
200
     * @throws \Exception
201
     */
202 18
    protected function processFieldAST(FieldAst $fieldAst, AbstractField $field, $contextValue)
203
    {
204 18
        $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...
205 18
        $fieldType        = $field->getType();
206 18
        $preResolvedValue = $this->getPreResolvedValue($contextValue, $fieldAst, $field);
207
208 18
        if ($fieldType->getKind() == TypeMap::KIND_LIST) {
209 1
            $listValue = [];
210 1
            foreach ($preResolvedValue as $resolvedValueItem) {
211 1
                $type = $fieldType->getNamedType();
212
213 1
                if (!$type->isValidValue($resolvedValueItem)) {
214
                    $this->executionContext->addError(new ResolveException(sprintf('Not valid resolve value in %s field', $field->getName())));
215
216
                    $listValue = null;
217
                    break;
218
                }
219 1
                $listValue[] = $this->getOutputValue($type, $resolvedValueItem);
220
            }
221
222 1
            $value = $listValue;
223
        } else {
224 18
            $value = $this->getFieldValidatedValue($field, $preResolvedValue);
225
        }
226
227 18
        return $value;
228
    }
229
230
    /**
231
     * @param AbstractField $field
232
     * @param mixed         $contextValue
233
     * @param Query         $query
234
     *
235
     * @return mixed
236
     */
237 21
    protected function resolveFieldValue(AbstractField $field, $contextValue, Query $query)
238
    {
239 21
        $resolveInfo = new ResolveInfo($field, $query->getFields(), $field->getType(), $this->executionContext);
240
241 21
        if ($resolveFunc = $field->getConfig()->getResolveFunction()) {
242 19
            return $resolveFunc($contextValue, $this->parseArgumentsValues($field, $query), $resolveInfo);
243 9
        } elseif ($propertyValue = TypeService::getPropertyValue($contextValue, $field->getName())) {
244 1
            return $propertyValue;
245
        } else {
246 8
            return $field->resolve($contextValue, $this->parseArgumentsValues($field, $query), $resolveInfo);
247
        }
248
    }
249
250
    /**
251
     * @param               $contextValue
252
     * @param FieldAst      $fieldAst
253
     * @param AbstractField $field
254
     *
255
     * @throws \Exception
256
     *
257
     * @return mixed
258
     */
259 18
    protected function getPreResolvedValue($contextValue, FieldAst $fieldAst, AbstractField $field)
260
    {
261 18
        $resolved      = false;
262 18
        $resolverValue = null;
263
264 18
        if (is_array($contextValue) && array_key_exists($fieldAst->getName(), $contextValue)) {
265 14
            $resolverValue = $contextValue[$fieldAst->getName()];
266 14
            $resolved      = true;
267 9
        } elseif (is_object($contextValue)) {
268 6
            $resolverValue = TypeService::getPropertyValue($contextValue, $fieldAst->getName());
269 6
            $resolved      = true;
270
        }
271
272 18
        if (!$resolved && $field->getType()->getNamedType()->getKind() == TypeMap::KIND_SCALAR) {
273 3
            $resolved = true;
274
        }
275
276 18
        if ($resolveFunction = $field->getConfig()->getResolveFunction()) {
277 3
            $resolveInfo = new ResolveInfo($field, [$fieldAst], $field->getType(), $this->executionContext);
278
279 3
            if (!$this->resolveValidator->validateArguments($field, $fieldAst, $this->executionContext->getRequest())) {
280
                throw new \Exception(sprintf('Not valid arguments for the field "%s"', $fieldAst->getName()));
281
282
            } else {
283 3
                $resolverValue = $resolveFunction($resolved ? $resolverValue : $contextValue, $fieldAst->getKeyValueArguments(), $resolveInfo);
284
            }
285
286
        }
287
288 18
        if (!$resolverValue && !$resolved) {
289 1
            throw new \Exception(sprintf('Property "%s" not found in resolve result', $fieldAst->getName()));
290
        }
291
292 18
        return $resolverValue;
293
    }
294
295
    /**
296
     * @param $field     AbstractField
297
     * @param $query     Query
298
     *
299
     * @return array
300
     */
301 21
    protected function parseArgumentsValues(AbstractField $field, Query $query)
302
    {
303 21
        $args = [];
304 21
        foreach ($query->getArguments() as $argument) {
305 9
            if ($configArgument = $field->getConfig()->getArgument($argument->getName())) {
306 9
                $args[$argument->getName()] = $configArgument->getType()->parseValue($argument->getValue()->getValue());
307
            }
308
        }
309
310 21
        return $args;
311
    }
312
313
    /**
314
     * @param $query         Query|FragmentInterface
315
     * @param $queryType     AbstractObjectType|TypeInterface|Field|AbstractType
316
     * @param $resolvedValue mixed
317
     * @param $value         array
318
     *
319
     * @throws \Exception
320
     *
321
     * @return array
322
     */
323 19
    protected function processQueryFields($query, AbstractType $queryType, $resolvedValue, $value)
324
    {
325 19
        foreach ($query->getFields() as $fieldAst) {
326 19
            $fieldResolvedValue = null;
327
328 19
            if ($fieldAst instanceof FragmentInterface) {
329
                /** @var TypedFragmentReference $fragment */
330 3
                $fragment = $fieldAst;
331 3
                if ($fieldAst instanceof FragmentReference) {
332
                    /** @var Fragment $fragment */
333 2
                    $fragment = $this->executionContext->getRequest()->getFragment($fieldAst->getName());
334 2
                    $this->resolveValidator->assertValidFragmentForField($fragment, $fieldAst, $queryType);
335 1
                } elseif ($fragment->getTypeName() !== $queryType->getName()) {
336 1
                    continue;
337
                }
338
339 3
                $fragmentValue      = $this->processQueryFields($fragment, $queryType, $resolvedValue, $value);
340 3
                $fieldResolvedValue = is_array($fragmentValue) ? $fragmentValue : [];
341
            } else {
342 19
                $alias       = $fieldAst->getAlias() ?: $fieldAst->getName();
343 19
                $currentType = $queryType->getNullableType();
344
345 19
                if ($fieldAst->getName() == self::TYPE_NAME_QUERY) {
346 1
                    $fieldResolvedValue = [$alias => $queryType->getName()];
347
                } else {
348 19
                    if (!$this->resolveValidator->objectHasField($currentType, $fieldAst)) {
349 2
                        $fieldResolvedValue = null;
350
                    } else {
351 19
                        if ($fieldAst instanceof Query) {
352 10
                            $queryAst           = $currentType->getField($fieldAst->getName());
353 10
                            $fieldValue         = $queryAst ? $this->processQueryAST($fieldAst, $queryAst, $resolvedValue) : null;
354 10
                            $fieldResolvedValue = [$alias => $fieldValue];
355
                        } elseif ($fieldAst instanceof FieldAst) {
356
                            $fieldResolvedValue = [
357 18
                                $alias => $this->processFieldAST($fieldAst, $currentType->getField($fieldAst->getName()), $resolvedValue)
358
                            ];
359
                        }
360
                    }
361
362
363
                }
364
            }
365
366 19
            $value = $this->collectValue($value, $fieldResolvedValue);
367
        }
368
369 19
        return $value;
370
    }
371
372 18
    protected function getFieldValidatedValue(AbstractField $field, $value)
373
    {
374 18
        return ($this->resolveValidator->isValidValueForField($field, $value)) ? $this->getOutputValue($field->getType(), $value) : null;
375
    }
376
377 18
    protected function getOutputValue(AbstractType $type, $value)
378
    {
379 18
        return in_array($type->getKind(), [TypeMap::KIND_OBJECT, TypeMap::KIND_NON_NULL]) ? $value : $type->serialize($value);
380
    }
381
382 19
    protected function collectValue($value, $queryValue)
383
    {
384 19
        if ($queryValue && is_array($queryValue)) {
385 19
            $value = array_merge(is_array($value) ? $value : [], $queryValue);
386
        } else {
387 2
            $value = $queryValue;
388
        }
389
390 19
        return $value;
391
    }
392
393 23
    protected function introduceIntrospectionFields(AbstractSchema $schema)
394
    {
395 23
        $schemaField = new SchemaField();
396 23
        $schemaField->setSchema($schema);
397
398 23
        $schema->addQueryField($schemaField);
399 23
        $schema->addQueryField(new TypeDefinitionField());
400 23
    }
401
402 23
    public function getResponseData()
403
    {
404 23
        $result = [];
405
406 23
        if (!empty($this->data)) {
407 21
            $result['data'] = $this->data;
408
        }
409
410 23
        if ($this->executionContext->hasErrors()) {
411 7
            $result['errors'] = $this->executionContext->getErrorsArray();
412
        }
413
414 23
        return $result;
415
    }
416
417 1
    public function setMaxComplexity($max) {
418 1
        $this->maxComplexity = $max;
419 1
    }
420
421
    /**
422
     * @param AbstractType $queryType
423
     * @param AbstractType $mutationType
424
     * @param array        $reducers
425
     */
426 23
    protected function reduceQuery($queryType, $mutationType, array $reducers) {
427 23
        foreach ($reducers as $reducer) {
428 1
            foreach ($this->executionContext->getRequest()->getOperationsInOrder() as $operation) {
429 1
                $this->doVisit($operation, $operation instanceof Mutation ? $mutationType : $queryType, $reducer);
0 ignored issues
show
Compatibility introduced by
$operation instanceof \Y...tationType : $queryType of type object<Youshido\GraphQL\Type\AbstractType> is not a sub-type of object<Youshido\GraphQL\...ect\AbstractObjectType>. It seems like you assume a child class of the class Youshido\GraphQL\Type\AbstractType to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
430
            }
431
        }
432 23
    }
433
434
    /**
435
     * @param Query|Field          $query
436
     * @param AbstractObjectType   $currentLevelSchema
437
     * @param AbstractQueryVisitor $reducer
438
     */
439 1
    protected function doVisit(Query $query, $currentLevelSchema, $reducer)
440
    {
441 1
        if (!($currentLevelSchema instanceof AbstractObjectType) || !$currentLevelSchema->hasField($query->getName())) {
442
            return;
443
        }
444
445 1
        if ($operationField = $currentLevelSchema->getField($query->getName())) {
446
447 1
            $coroutine = $this->walkQuery($query, $operationField);
448
449
            do {
450 1
                list($queryField, $astField, $childCost) = isset($results) ? $results : $coroutine->current();
451
452 1
                $cost = $reducer->visit($queryField->getKeyValueArguments(), $astField->getConfig(), $childCost);
453
454 1
            } while ($results = $coroutine->send($cost));
455
456 1
            $reducer->visit($query->getKeyValueArguments(), $operationField->getConfig(), $cost);
457
        }
458 1
    }
459
460
    /**
461
     * @param Query         $query
462
     * @param AbstractField $currentLevelAST
463
     *
464
     * @return \Generator
465
     */
466 1
    protected function walkQuery($query, AbstractField $currentLevelAST) {
467 1
        foreach ($query->getFields() as $queryField) {
468 1
            if ($queryField instanceof FragmentInterface) {
469
                if ($queryField instanceof FragmentReference) {
470
                    $queryField = $this->executionContext->getRequest()->getFragment($queryField->getName());
471
                }
472
473
                foreach ($this->walkQuery($queryField, $currentLevelAST) as $childResults) {
0 ignored issues
show
Documentation introduced by
$queryField is of type object<Youshido\GraphQL\Parser\Ast\Fragment>|null, but the function expects a object<Youshido\GraphQL\Parser\Ast\Query>.

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...
474
                    yield $childResults;
475
                }
476
            } else {
477 1
                $fieldType = $currentLevelAST->getType()->getNamedType();
478 1
                if ($fieldType instanceof AbstractUnionType) {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
479
                    // TODO
480 1
                } elseif ($fieldType instanceof AbstractObjectType && $fieldAst = $fieldType->getField($queryField->getName())) {
481 1
                    $childScore = 0;
482 1
                    if ($queryField instanceof Query) {
483 1
                        foreach ($this->walkQuery($queryField, $fieldAst) as $childResults) {
484
                            // add child score to this node's score
485
                            $childResults[2] += $childScore;
486
                            // pass control to visitor to generate scores
487
                            $childScore += (yield $childResults);
488
                        }
489
                    }
490
491 1
                    yield [$queryField, $fieldAst, $childScore];
492
                }
493
            }
494
        }
495 1
    }
496
}
497