Completed
Push — master ( 3d21e8...29ef50 )
by Alexandr
9s
created

Processor::resolveComposite()   B

Complexity

Conditions 3
Paths 3

Size

Total Lines 28
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 3.0021

Importance

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