Completed
Push — master ( d3aaf0...75a8b3 )
by Alexandr
03:30
created

Processor::resolveQuery()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 18
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 4

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 18
ccs 11
cts 11
cp 1
rs 9.2
cc 4
eloc 11
nc 4
nop 1
crap 4
1
<?php
2
/**
3
 * Date: 03.11.16
4
 *
5
 * @author Portey Vasil <[email protected]>
6
 */
7
8
namespace Youshido\GraphQL\Execution;
9
10
11
use Youshido\GraphQL\Exception\ResolveException;
12
use Youshido\GraphQL\Execution\Container\Container;
13
use Youshido\GraphQL\Execution\Context\ExecutionContext;
14
use Youshido\GraphQL\Execution\Visitor\MaxComplexityQueryVisitor;
15
use Youshido\GraphQL\Field\Field;
16
use Youshido\GraphQL\Field\FieldInterface;
17
use Youshido\GraphQL\Field\InputField;
18
use Youshido\GraphQL\Parser\Ast\ArgumentValue\InputList as AstInputList;
19
use Youshido\GraphQL\Parser\Ast\ArgumentValue\InputObject as AstInputObject;
20
use Youshido\GraphQL\Parser\Ast\ArgumentValue\Literal as AstLiteral;
21
use Youshido\GraphQL\Parser\Ast\ArgumentValue\VariableReference;
22
use Youshido\GraphQL\Parser\Ast\Field as AstField;
23
use Youshido\GraphQL\Parser\Ast\FragmentReference;
24
use Youshido\GraphQL\Parser\Ast\Interfaces\FieldInterface as AstFieldInterface;
25
use Youshido\GraphQL\Parser\Ast\Mutation as AstMutation;
26
use Youshido\GraphQL\Parser\Ast\Query as AstQuery;
27
use Youshido\GraphQL\Parser\Ast\TypedFragmentReference;
28
use Youshido\GraphQL\Parser\Parser;
29
use Youshido\GraphQL\Schema\AbstractSchema;
30
use Youshido\GraphQL\Type\AbstractType;
31
use Youshido\GraphQL\Type\InputObject\AbstractInputObjectType;
32
use Youshido\GraphQL\Type\InterfaceType\AbstractInterfaceType;
33
use Youshido\GraphQL\Type\ListType\AbstractListType;
34
use Youshido\GraphQL\Type\Object\AbstractObjectType;
35
use Youshido\GraphQL\Type\Scalar\AbstractScalarType;
36
use Youshido\GraphQL\Type\TypeMap;
37
use Youshido\GraphQL\Type\Union\AbstractUnionType;
38
use Youshido\GraphQL\Validator\RequestValidator\RequestValidator;
39
use Youshido\GraphQL\Validator\ResolveValidator\ResolveValidator;
40
use Youshido\GraphQL\Validator\ResolveValidator\ResolveValidatorInterface;
41
42
class Processor
43
{
44
45
    const TYPE_NAME_QUERY = '__typename';
46
47
    /** @var ExecutionContext */
48
    protected $executionContext;
49
50
    /** @var ResolveValidatorInterface */
51
    protected $resolveValidator;
52
53
    /** @var  array */
54
    protected $data;
55
56
    /** @var int */
57
    protected $maxComplexity;
58
59 47
    public function __construct(AbstractSchema $schema)
60
    {
61 47
        if (empty($this->executionContext)) {
62 47
            $this->executionContext = new ExecutionContext($schema);
63 47
            $this->executionContext->setContainer(new Container());
64 47
        }
65
66 47
        $this->resolveValidator = new ResolveValidator($this->executionContext);
0 ignored issues
show
Unused Code introduced by
The call to ResolveValidator::__construct() has too many arguments starting with $this->executionContext.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
67 47
    }
68
69 45
    public function processPayload($payload, $variables = [], $reducers = [])
70
    {
71 45
        $this->data = [];
72
73
        try {
74 45
            $this->parseAndCreateRequest($payload, $variables);
75
76 44
            if ($this->maxComplexity) {
77 1
                $reducers[] = new MaxComplexityQueryVisitor($this->maxComplexity);
78 1
            }
79
80 44
            if ($reducers) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $reducers of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
81 2
                $reducer = new Reducer();
82 2
                $reducer->reduceQuery($this->executionContext, $reducers);
83 2
            }
84
85 44
            foreach ($this->executionContext->getRequest()->getAllOperations() as $query) {
86 44
                if ($operationResult = $this->resolveQuery($query)) {
87 44
                    $this->data = array_merge($this->data, $operationResult);
88 44
                };
89 44
            }
90 45
        } catch (\Exception $e) {
91 5
            $this->executionContext->addError($e);
92
        }
93
94 45
        return $this;
95
    }
96
97 45
    public function getResponseData()
98
    {
99 45
        $result = [];
100
101 45
        if (!empty($this->data)) {
102 44
            $result['data'] = $this->data;
103 44
        }
104
105 45
        if ($this->executionContext->hasErrors()) {
106 17
            $result['errors'] = $this->executionContext->getErrorsArray();
107 17
        }
108
109 45
        return $result;
110
    }
111
112
    /**
113
     * You can access ExecutionContext to check errors and inject dependencies
114
     *
115
     * @return ExecutionContext
116
     */
117 10
    public function getExecutionContext()
118
    {
119 10
        return $this->executionContext;
120
    }
121
122
    /**
123
     * @return int
124
     */
125
    public function getMaxComplexity()
126
    {
127
        return $this->maxComplexity;
128
    }
129
130
    /**
131
     * @param int $maxComplexity
132
     */
133 1
    public function setMaxComplexity($maxComplexity)
134
    {
135 1
        $this->maxComplexity = $maxComplexity;
136 1
    }
137
138 44
    protected function resolveQuery(AstQuery $query)
139
    {
140 44
        $schema = $this->executionContext->getSchema();
141 44
        $type   = $query instanceof AstMutation ? $schema->getMutationType() : $schema->getQueryType();
142 44
        $field  = new Field([
143 44
            'name' => $query instanceof AstMutation ? 'mutation' : 'query',
144
            'type' => $type
145 44
        ]);
146
147 44
        if (self::TYPE_NAME_QUERY == $query->getName()) {
148 1
            return [$this->getAlias($query) => $type->getName()];
149
        }
150
151 44
        $this->resolveValidator->assetTypeHasField($type, $query);
152 44
        $value = $this->resolveField($field, $query);
153
154 44
        return [$this->getAlias($query) => $value];
155
    }
156
157 44
    protected function resolveField(FieldInterface $field, AstFieldInterface $ast, $parentValue = null, $fromObject = false)
158
    {
159
        try {
160
            /** @var AbstractObjectType $type */
161 44
            $type        = $field->getType();
162 44
            $nonNullType = $type->getNullableType();
163
164 44
            if (self::TYPE_NAME_QUERY == $ast->getName()) {
165 2
                return $nonNullType->getName();
166
            }
167
168 44
            $this->resolveValidator->assetTypeHasField($nonNullType, $ast);
169
170 44
            $targetField = $nonNullType->getField($ast->getName());
171
172 44
            $this->prepareAstArguments($targetField, $ast, $this->executionContext->getRequest());
173 43
            $this->resolveValidator->assertValidArguments($targetField, $ast, $this->executionContext->getRequest());
174
175 39
            switch ($kind = $targetField->getType()->getNullableType()->getKind()) {
176 39
                case TypeMap::KIND_ENUM:
177 39
                case TypeMap::KIND_SCALAR:
178 33
                    if ($ast instanceof AstQuery && $ast->hasFields()) {
179 2
                        throw new ResolveException(sprintf('You can\'t specify fields for scalar type "%s"', $targetField->getType()->getNullableType()->getName()), $ast->getLocation());
180
                    }
181
182 33
                    return $this->resolveScalar($targetField, $ast, $parentValue);
183
184 29 View Code Duplication
                case TypeMap::KIND_OBJECT:
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...
185
                    /** @var $type AbstractObjectType */
186 21
                    if (!$ast instanceof AstQuery) {
187 1
                        throw new ResolveException(sprintf('You have to specify fields for "%s"', $ast->getName()), $ast->getLocation());
188
                    }
189
190 21
                    return $this->resolveObject($targetField, $ast, $parentValue);
191
192 17
                case TypeMap::KIND_LIST:
193 14
                    return $this->resolveList($targetField, $ast, $parentValue);
194
195 6
                case TypeMap::KIND_UNION:
196 6 View Code Duplication
                case TypeMap::KIND_INTERFACE:
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...
197 6
                    if (!$ast instanceof AstQuery) {
198
                        throw new ResolveException(sprintf('You have to specify fields for "%s"', $ast->getName()), $ast->getLocation());
199
                    }
200
201 6
                    return $this->resolveComposite($targetField, $ast, $parentValue);
202
203
                default:
204
                    throw new ResolveException(sprintf('Resolving type with kind "%s" not supported', $kind));
205
            }
206 15
        } catch (\Exception $e) {
207 15
            $this->executionContext->addError($e);
208
209 15
            if ($fromObject) {
210 4
                throw $e;
211
            }
212
213 13
            return null;
214
        }
215
    }
216
217 44
    private function prepareAstArguments(FieldInterface $field, AstFieldInterface $query, Request $request)
218
    {
219 44
        foreach ($query->getArguments() as $astArgument) {
220 22
            if ($field->hasArgument($astArgument->getName())) {
221 22
                $argumentType = $field->getArgument($astArgument->getName())->getType()->getNullableType();
222
223 22
                $astArgument->setValue($this->prepareArgumentValue($astArgument->getValue(), $argumentType, $request));
224 21
            }
225 43
        }
226 43
    }
227
228 22
    private function prepareArgumentValue($argumentValue, AbstractType $argumentType, Request $request)
229
    {
230 22
        switch ($argumentType->getKind()) {
231 22
            case TypeMap::KIND_LIST:
232
                /** @var $argumentType AbstractListType */
233 5
                $result = [];
234 5
                if ($argumentValue instanceof AstInputList || is_array($argumentValue)) {
235 5
                    $list = is_array($argumentValue) ? $argumentValue : $argumentValue->getValue();
236 5
                    foreach ($list as $item) {
237 5
                        $result[] = $this->prepareArgumentValue($item, $argumentType->getItemType()->getNullableType(), $request);
238 5
                    }
239 5
                } else if ($argumentValue instanceof VariableReference) {
240
                    return $this->getVariableReferenceArgumentValue($argumentValue, $argumentType, $request);
241
                }
242
243 5
                return $result;
244
245 22
            case TypeMap::KIND_INPUT_OBJECT:
246
                /** @var $argumentType AbstractInputObjectType */
247 3
                $result = [];
248 3
                if ($argumentValue instanceof AstInputObject) {
249 3
                    foreach($argumentType->getFields() as $field) {
250
                        /** @var $field Field */
251 3
                        if ($field->getConfig()->has('default')) {
252 1
                            $result[$field->getName()] = $field->getConfig()->get('default');
253 1
                        }
254 3
                    }
255 3
                    foreach ($argumentValue->getValue() as $key => $item) {
256 3
                        if ($argumentType->hasField($key)) {
257 3
                            $result[$key] = $this->prepareArgumentValue($item, $argumentType->getField($key)->getType()->getNullableType(), $request);
258 3
                        } else {
259
                            $result[$key] = $item;
260
                        }
261 3
                    }
262 3
                } else if ($argumentValue instanceof VariableReference) {
263
                    return $this->getVariableReferenceArgumentValue($argumentValue, $argumentType, $request);
264
                }
265
266 3
                return $result;
267
268 22
            case TypeMap::KIND_SCALAR:
269 22
            case TypeMap::KIND_ENUM:
270
                /** @var $argumentValue AstLiteral|VariableReference */
271 22
                if ($argumentValue instanceof VariableReference) {
272 4
                    return $this->getVariableReferenceArgumentValue($argumentValue, $argumentType, $request);
273 19
                } else if ($argumentValue instanceof AstLiteral) {
274 13
                    return $argumentValue->getValue();
275
                } else {
276 7
                    return $argumentValue;
277
                }
278
        }
279
280
        throw new ResolveException('Argument type not supported');
281
    }
282
283 4
    private function getVariableReferenceArgumentValue(VariableReference $variableReference, AbstractType $argumentType, Request $request)
284
    {
285 4
        $variable = $variableReference->getVariable();
286 4
        if ($argumentType->getKind() == TypeMap::KIND_LIST) {
287
            if ($variable->getTypeName() != $argumentType->getNamedType()->getName() || !$variable->isArray()) {
288
                throw new ResolveException(sprintf('Invalid variable "%s" type, allowed type is "%s"', $variable->getName(), $argumentType->getName()), $variable->getLocation());
289
            }
290
        } else {
291 4
            if ($variable->getTypeName() != $argumentType->getName()) {
292 1
                throw new ResolveException(sprintf('Invalid variable "%s" type, allowed type is "%s"', $variable->getName(), $argumentType->getName()), $variable->getLocation());
293
            }
294
        }
295
296 3
        $requestValue = $request->getVariable($variable->getName());
297 3
        if (!$request->hasVariable($variable->getName()) || (null === $requestValue && $variable->isNullable())) {
298
            throw  new ResolveException(sprintf('Variable "%s" does not exist in request', $variable->getName()), $variable->getLocation());
299
        }
300
301 3
        return $requestValue;
302
    }
303
304 26
    protected function resolveObject(FieldInterface $field, AstFieldInterface $ast, $parentValue, $fromUnion = false)
305
    {
306 26
        if (!$fromUnion) {
307 21
            $resolvedValue = $this->doResolve($field, $ast, $parentValue);
308 21
        } else {
309 7
            $resolvedValue = $parentValue;
310
        }
311
312 26
        $this->resolveValidator->assertValidResolvedValueForField($field, $resolvedValue);
313
314 26
        if (null === $resolvedValue) {
315 5
            return null;
316
        }
317
        /** @var AbstractObjectType $type */
318 25
        $type = $field->getType()->getNullableType();
319
320
        try {
321 25
            return $this->collectResult($field, $type, $ast, $resolvedValue);
322 4
        } catch (\Exception $e) {
323 4
            return null;
324
        }
325
    }
326
327 25
    private function collectResult(FieldInterface $field, AbstractObjectType $type, $ast, $resolvedValue)
328
    {
329
        /** @var AstQuery $ast */
330 25
        $result = [];
331
332 25
        foreach ($ast->getFields() as $astField) {
333 25
            switch (true) {
334 25
                case $astField instanceof TypedFragmentReference:
335 2
                    $astName  = $astField->getTypeName();
336 2
                    $typeName = $type->getName();
337
338 2 View Code Duplication
                    if ($typeName !== $astName) {
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...
339 2
                        foreach ($type->getInterfaces() as $interface) {
340 1
                            if ($interface->getName() === $astName) {
341
                                $result = array_merge($result, $this->collectResult($field, $type, $astField, $resolvedValue));
342
343
                                break;
344
                            }
345 2
                        }
346
347 2
                        continue;
348
                    }
349
350 2
                    $result = array_merge($result, $this->collectResult($field, $type, $astField, $resolvedValue));
351
352 2
                    break;
353
354 25
                case $astField instanceof FragmentReference:
355 4
                    $astFragment      = $this->executionContext->getRequest()->getFragment($astField->getName());
356 4
                    $astFragmentModel = $astFragment->getModel();
357 4
                    $typeName         = $type->getName();
358
359 4 View Code Duplication
                    if ($typeName !== $astFragmentModel) {
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...
360 1
                        foreach ($type->getInterfaces() as $interface) {
361 1
                            if ($interface->getName() === $astFragmentModel) {
362 1
                                $result = array_merge($result, $this->collectResult($field, $type, $astFragment, $resolvedValue));
363
364 1
                                break;
365
                            }
366
367 1
                        }
368
369 1
                        continue;
370
                    }
371
372 4
                    $result = array_merge($result, $this->collectResult($field, $type, $astFragment, $resolvedValue));
373
374 4
                    break;
375
376 25
                default:
377 25
                    $result[$this->getAlias($astField)] = $this->resolveField($field, $astField, $resolvedValue, true);
378 25
            }
379 25
        }
380
381 25
        return $result;
382
    }
383
384 33
    protected function resolveScalar(FieldInterface $field, AstFieldInterface $ast, $parentValue)
385
    {
386 33
        $resolvedValue = $this->doResolve($field, $ast, $parentValue);
387
388 33
        $this->resolveValidator->assertValidResolvedValueForField($field, $resolvedValue);
389
390
        /** @var AbstractScalarType $type */
391 32
        $type = $field->getType()->getNullableType();
392
393 32
        return $type->serialize($resolvedValue);
394
    }
395
396 14
    protected function resolveList(FieldInterface $field, AstFieldInterface $ast, $parentValue)
397
    {
398
        /** @var AstQuery $ast */
399 14
        $resolvedValue = $this->doResolve($field, $ast, $parentValue);
400
401 14
        $this->resolveValidator->assertValidResolvedValueForField($field, $resolvedValue);
402
403 12
        if (null === $resolvedValue) {
404 5
            return null;
405
        }
406
407
        /** @var AbstractListType $type */
408 11
        $type     = $field->getType()->getNullableType();
409 11
        $itemType = $type->getNamedType();
410
411 11
        $fakeAst = clone $ast;
412 11
        if ($fakeAst instanceof AstQuery) {
413 11
            $fakeAst->setArguments([]);
414 11
        }
415
416 11
        $fakeField = new Field([
417 11
            'name' => $field->getName(),
418 11
            'type' => $itemType,
419 11
        ]);
420
421 11
        $result = [];
422 11
        foreach ($resolvedValue as $resolvedValueItem) {
423
            try {
424 10
                $fakeField->getConfig()->set('resolve', function () use ($resolvedValueItem) {
425 10
                    return $resolvedValueItem;
426 10
                });
427
428 10
                switch ($itemType->getNullableType()->getKind()) {
429 10
                    case TypeMap::KIND_ENUM:
430 10
                    case TypeMap::KIND_SCALAR:
431 3
                        $value = $this->resolveScalar($fakeField, $fakeAst, $resolvedValueItem);
432
433 2
                        break;
434
435
436 8
                    case TypeMap::KIND_OBJECT:
437 6
                        $value = $this->resolveObject($fakeField, $fakeAst, $resolvedValueItem);
438
439 6
                        break;
440
441 3
                    case TypeMap::KIND_UNION:
442 3
                    case TypeMap::KIND_INTERFACE:
443 3
                        $value = $this->resolveComposite($fakeField, $fakeAst, $resolvedValueItem);
444
445 3
                        break;
446
447
                    default:
448
                        $value = null;
449 9
                }
450 10
            } catch (\Exception $e) {
451 1
                $this->executionContext->addError($e);
452
453 1
                $value = null;
454
            }
455
456 10
            $result[] = $value;
457 11
        }
458
459 11
        return $result;
460
    }
461
462 7
    protected function resolveComposite(FieldInterface $field, AstFieldInterface $ast, $parentValue)
463
    {
464
        /** @var AstQuery $ast */
465 7
        $resolvedValue = $this->doResolve($field, $ast, $parentValue);
466
467 7
        $this->resolveValidator->assertValidResolvedValueForField($field, $resolvedValue);
468
469
        /** @var AbstractUnionType $type */
470 7
        $type         = $field->getType()->getNullableType();
471 7
        $resolvedType = $type->resolveType($resolvedValue);
472
473 7
        if (!$resolvedType) {
474
            throw new ResolveException('Resoling function must return type');
475
        }
476
477 7
        if ($type instanceof AbstractInterfaceType) {
478 6
            $this->resolveValidator->assertTypeImplementsInterface($resolvedType, $type);
479 6
        } else {
480 1
            $this->resolveValidator->assertTypeInUnionTypes($resolvedType, $type);
481
        }
482
483 7
        $fakeField = new Field([
484 7
            'name' => $field->getName(),
485 7
            'type' => $resolvedType,
486 7
        ]);
487
488 7
        return $this->resolveObject($fakeField, $ast, $resolvedValue, true);
489
    }
490
491 45
    protected function parseAndCreateRequest($payload, $variables = [])
492
    {
493 45
        if (empty($payload)) {
494 1
            throw new \InvalidArgumentException('Must provide an operation.');
495
        }
496
497 45
        $parser  = new Parser();
498 45
        $request = new Request($parser->parse($payload), $variables);
499
500 45
        (new RequestValidator())->validate($request);
501
502 44
        $this->executionContext->setRequest($request);
503 44
    }
504
505 39
    protected function doResolve(FieldInterface $field, AstFieldInterface $ast, $parentValue = null)
506
    {
507
        /** @var AstQuery|AstField $ast */
508 39
        $arguments = $this->parseArgumentsValues($field, $ast);
509 39
        $astFields = $ast instanceof AstQuery ? $ast->getFields() : [];
510
511 39
        return $field->resolve($parentValue, $arguments, $this->createResolveInfo($field, $astFields));
512
    }
513
514 39
    protected function parseArgumentsValues(FieldInterface $field, AstFieldInterface $ast)
515
    {
516 39
        $values   = [];
517 39
        $defaults = [];
518
519 39
        foreach ($field->getArguments() as $argument) {
520
            /** @var $argument InputField */
521 25
            if ($argument->getConfig()->has('default')) {
522 6
                $defaults[$argument->getName()] = $argument->getConfig()->getDefaultValue();
523 6
            }
524 39
        }
525
526 39
        foreach ($ast->getArguments() as $astArgument) {
527 19
            $argument     = $field->getArgument($astArgument->getName());
528 19
            $argumentType = $argument->getType()->getNullableType();
529
530 19
            $values[$argument->getName()] = $argumentType->parseValue($astArgument->getValue());
531
532 19
            if (isset($defaults[$argument->getName()])) {
533 3
                unset($defaults[$argument->getName()]);
534 3
            }
535 39
        }
536
537 39
        return array_merge($values, $defaults);
538
    }
539
540 44
    private function getAlias(AstFieldInterface $ast)
541
    {
542 44
        return $ast->getAlias() ?: $ast->getName();
543
    }
544
545 39
    protected function createResolveInfo(FieldInterface $field, array $astFields)
546
    {
547 39
        return new ResolveInfo($field, $astFields, $this->executionContext);
548
    }
549
550
}
551