Completed
Push — master ( 150b90...d63187 )
by Portey
04:50
created

Processor::resolveComposite()   B

Complexity

Conditions 3
Paths 3

Size

Total Lines 28
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 3.0032

Importance

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