Completed
Push — master ( da1622...d3aaf0 )
by Alexandr
02:54
created

Processor::getResponseData()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 3

Importance

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