Completed
Pull Request — master (#113)
by
unknown
07:37
created

Processor::getAlias()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 2
eloc 2
nc 2
nop 1
crap 2
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 54
    public function __construct(AbstractSchema $schema)
60
    {
61 54
        if (empty($this->executionContext)) {
62 54
            $this->executionContext = new ExecutionContext($schema);
63 54
            $this->executionContext->setContainer(new Container());
64
        }
65
66 54
        $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 54
    }
68
69 52
    public function processPayload($payload, $variables = [], $reducers = [])
70
    {
71 52
        $this->data = [];
72
73
        try {
74 52
            $this->parseAndCreateRequest($payload, $variables);
75
76 51
            if ($this->maxComplexity) {
77 1
                $reducers[] = new MaxComplexityQueryVisitor($this->maxComplexity);
78
            }
79
80 51
            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 51
            foreach ($this->executionContext->getRequest()->getAllOperations() as $query) {
86 51
                if ($operationResult = $this->resolveQuery($query)) {
87 51
                    $this->data = array_merge($this->data, $operationResult);
88
                };
89
            }
90 5
        } catch (\Exception $e) {
91 5
            $this->executionContext->addError($e);
92
        }
93
94 52
        return $this;
95
    }
96
97 52
    public function getResponseData()
98
    {
99 52
        $result = [];
100
101 52
        if (!empty($this->data)) {
102 51
            $result['data'] = $this->data;
103
        }
104
105 52
        if ($this->executionContext->hasErrors()) {
106 18
            $result['errors'] = $this->executionContext->getErrorsArray();
107
        }
108
109 52
        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 51
    protected function resolveQuery(AstQuery $query)
139
    {
140 51
        $schema = $this->executionContext->getSchema();
141 51
        $type   = $query instanceof AstMutation ? $schema->getMutationType() : $schema->getQueryType();
142 51
        $field  = new Field([
143 51
            'name' => $query instanceof AstMutation ? 'mutation' : 'query',
144 51
            'type' => $type
145
        ]);
146
147 51
        if (self::TYPE_NAME_QUERY == $query->getName()) {
148 1
            return [$this->getAlias($query) => $type->getName()];
149
        }
150
151 51
        $this->resolveValidator->assetTypeHasField($type, $query);
152 51
        $value = $this->resolveField($field, $query);
153
154 51
        return [$this->getAlias($query) => $value];
155
    }
156
157 51
    protected function resolveField(FieldInterface $field, AstFieldInterface $ast, $parentValue = null, $fromObject = false)
158
    {
159
        try {
160
            /** @var AbstractObjectType $type */
161 51
            $type        = $field->getType();
162 51
            $nonNullType = $type->getNullableType();
163
164 51
            if (self::TYPE_NAME_QUERY == $ast->getName()) {
165 2
                return $nonNullType->getName();
166
            }
167
168 51
            $this->resolveValidator->assetTypeHasField($nonNullType, $ast);
169
170 51
            $targetField = $nonNullType->getField($ast->getName());
171
172 51
            $this->prepareAstArguments($targetField, $ast, $this->executionContext->getRequest());
173 50
            $this->resolveValidator->assertValidArguments($targetField, $ast, $this->executionContext->getRequest());
174
175 46
            switch ($kind = $targetField->getType()->getNullableType()->getKind()) {
176 46
                case TypeMap::KIND_ENUM:
177 46
                case TypeMap::KIND_SCALAR:
178 40
                    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 40
                    return $this->resolveScalar($targetField, $ast, $parentValue);
183
184 30 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 22
                    if (!$ast instanceof AstQuery) {
187 1
                        throw new ResolveException(sprintf('You have to specify fields for "%s"', $ast->getName()), $ast->getLocation());
188
                    }
189
190 22
                    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 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...
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 51
    private function prepareAstArguments(FieldInterface $field, AstFieldInterface $query, Request $request)
218
    {
219 51
        foreach ($query->getArguments() as $astArgument) {
220 28
            if ($field->hasArgument($astArgument->getName())) {
221 28
                $argumentType = $field->getArgument($astArgument->getName())->getType()->getNullableType();
222
223 28
                $astArgument->setValue($this->prepareArgumentValue($astArgument->getValue(), $argumentType, $request));
224
            }
225
        }
226 50
    }
227
228 28
    private function prepareArgumentValue($argumentValue, AbstractType $argumentType, Request $request)
229
    {
230 28
        switch ($argumentType->getKind()) {
231 28
            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
                    }
239 1
                } else if ($argumentValue instanceof VariableReference) {
240 1
                    return $this->getVariableReferenceArgumentValue($argumentValue, $argumentType, $request);
241
                }
242
243 5
                return $result;
244
245 27
            case TypeMap::KIND_INPUT_OBJECT:
246
                /** @var $argumentType AbstractInputObjectType */
247 4
                $result = [];
248 4
                if ($argumentValue instanceof AstInputObject) {
249 3
                    foreach ($argumentType->getFields() as $field) {
250
                        /** @var $field Field */
251 3
                        if ($field->getConfig()->has('default')) {
252 3
                            $result[$field->getName()] = $field->getType()->getNullableType()->parseInputValue($field->getConfig()->get('default'));
253
                        }
254
                    }
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
                        } else {
259 3
                            $result[$key] = $item;
260
                        }
261
                    }
262 1
                } else if ($argumentValue instanceof VariableReference) {
263
                    return $this->getVariableReferenceArgumentValue($argumentValue, $argumentType, $request);
264 1
                } else if (is_array($argumentValue)) {
265
                    return $argumentValue;
266
                }
267
268 4
                return $result;
269
270 26
            case TypeMap::KIND_SCALAR:
271 3
            case TypeMap::KIND_ENUM:
272
                /** @var $argumentValue AstLiteral|VariableReference */
273 26
                if ($argumentValue instanceof VariableReference) {
274 4
                    return $this->getVariableReferenceArgumentValue($argumentValue, $argumentType, $request);
275 23
                } else if ($argumentValue instanceof AstLiteral) {
276 17
                    return $argumentValue->getValue();
277
                } else {
278 7
                    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
            ) {
294 1
                throw new ResolveException(sprintf('Invalid variable "%s" type, allowed type is "%s"', $variable->getName(), $argumentType->getNamedType()->getNullableType()->getName()), $variable->getLocation());
295
            }
296
        } 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 27
    protected function resolveObject(FieldInterface $field, AstFieldInterface $ast, $parentValue, $fromUnion = false)
311
    {
312 27
        $resolvedValue = $parentValue;
313 27
        if (!$fromUnion) {
314 22
            $resolvedValue = $this->doResolve($field, $ast, $parentValue);
315
        }
316
317 27
        $this->resolveValidator->assertValidResolvedValueForField($field, $resolvedValue);
318
319 27
        if (null === $resolvedValue) {
320 5
            return null;
321
        }
322
        /** @var AbstractObjectType $type */
323 26
        $type = $field->getType()->getNullableType();
324
325
        try {
326 26
            return $this->collectResult($field, $type, $ast, $resolvedValue);
327 4
        } catch (\Exception $e) {
328 4
            return null;
329
        }
330
    }
331
332 26
    private function collectResult(FieldInterface $field, AbstractObjectType $type, $ast, $resolvedValue)
333
    {
334
        /** @var AstQuery $ast */
335 26
        $result = [];
336
337 26
        foreach ($ast->getFields() as $astField) {
338
            switch (true) {
339 26
                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 1
                                break;
349
                            }
350
                        }
351
352 2
                        continue;
353
                    }
354
355 2
                    $result = array_merge($result, $this->collectResult($field, $type, $astField, $resolvedValue));
356
357 2
                    break;
358
359 26
                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
                        }
373
374 1
                        continue;
375
                    }
376
377 4
                    $result = array_merge($result, $this->collectResult($field, $type, $astFragment, $resolvedValue));
378
379 4
                    break;
380
381
                default:
382 26
                    $result[$this->getAlias($astField)] = $this->resolveField($field, $astField, $resolvedValue, true);
383
            }
384
        }
385
386 26
        return $result;
387
    }
388
389 40
    protected function resolveScalar(FieldInterface $field, AstFieldInterface $ast, $parentValue)
390
    {
391 40
        $resolvedValue = $this->doResolve($field, $ast, $parentValue);
392
393 40
        $this->resolveValidator->assertValidResolvedValueForField($field, $resolvedValue);
394
395
        /** @var AbstractScalarType $type */
396 39
        $type = $field->getType()->getNullableType();
397
398 39
        return $type->serialize($resolvedValue);
399
    }
400
401 14
    protected function resolveList(FieldInterface $field, AstFieldInterface $ast, $parentValue)
402
    {
403
        /** @var AstQuery $ast */
404 14
        $resolvedValue = $this->doResolve($field, $ast, $parentValue);
405
406 14
        $this->resolveValidator->assertValidResolvedValueForField($field, $resolvedValue);
407
408 12
        if (null === $resolvedValue) {
409 5
            return null;
410
        }
411
412
        /** @var AbstractListType $type */
413 11
        $type     = $field->getType()->getNullableType();
414 11
        $itemType = $type->getNamedType();
415
416 11
        $fakeAst = clone $ast;
417 11
        if ($fakeAst instanceof AstQuery) {
418 11
            $fakeAst->setArguments([]);
419
        }
420
421 11
        $fakeField = new Field([
422 11
            'name' => $field->getName(),
423 11
            'type' => $itemType,
424
        ]);
425
426 11
        $result = [];
427 11
        foreach ($resolvedValue as $resolvedValueItem) {
428
            try {
429 10
                $fakeField->getConfig()->set('resolve', function () use ($resolvedValueItem) {
430 10
                    return $resolvedValueItem;
431 10
                });
432
433 10
                switch ($itemType->getNullableType()->getKind()) {
434 10
                    case TypeMap::KIND_ENUM:
435 9
                    case TypeMap::KIND_SCALAR:
436 3
                        $value = $this->resolveScalar($fakeField, $fakeAst, $resolvedValueItem);
437
438 2
                        break;
439
440
441 8
                    case TypeMap::KIND_OBJECT:
442 6
                        $value = $this->resolveObject($fakeField, $fakeAst, $resolvedValueItem);
443
444 6
                        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 9
                        $value = null;
454
                }
455 1
            } catch (\Exception $e) {
456 1
                $this->executionContext->addError($e);
457
458 1
                $value = null;
459
            }
460
461 10
            $result[] = $value;
462
        }
463
464 11
        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
        } 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
        ]);
492
493 7
        return $this->resolveObject($fakeField, $ast, $resolvedValue, true);
494
    }
495
496 52
    protected function parseAndCreateRequest($payload, $variables = [])
497
    {
498 52
        if (empty($payload)) {
499 1
            throw new \InvalidArgumentException('Must provide an operation.');
500
        }
501
502 52
        $parser  = new Parser();
503 52
        $request = new Request($parser->parse($payload), $variables);
504
505 52
        (new RequestValidator())->validate($request);
506
507 51
        $this->executionContext->setRequest($request);
508 51
    }
509
510 46
    protected function doResolve(FieldInterface $field, AstFieldInterface $ast, $parentValue = null)
511
    {
512
        /** @var AstQuery|AstField $ast */
513 46
        $arguments = $this->parseArgumentsValues($field, $ast);
514 46
        $astFields = $ast instanceof AstQuery ? $ast->getFields() : [];
515
516 46
        return $field->resolve($parentValue, $arguments, $this->createResolveInfo($field, $astFields));
517
    }
518
519 46
    protected function parseArgumentsValues(FieldInterface $field, AstFieldInterface $ast)
520
    {
521 46
        $values   = [];
522 46
        $defaults = [];
523
524 46
        foreach ($field->getArguments() as $argument) {
525
            /** @var $argument InputField */
526 32
            if ($argument->getConfig()->has('default')) {
527 32
                $defaults[$argument->getName()] = $argument->getConfig()->getDefaultValue();
528
            }
529
        }
530
531 46
        foreach ($ast->getArguments() as $astArgument) {
532 25
            $argument     = $field->getArgument($astArgument->getName());
533 25
            $argumentType = $argument->getType()->getNullableType();
534
535 25
            $values[$argument->getName()] = $argumentType->parseValue($astArgument->getValue());
536
537 25
            if (isset($defaults[$argument->getName()])) {
538 25
                unset($defaults[$argument->getName()]);
539
            }
540
        }
541
542 46
        return array_merge($values, $defaults);
543
    }
544
545 51
    private function getAlias(AstFieldInterface $ast)
546
    {
547 51
        return $ast->getAlias() ?: $ast->getName();
548
    }
549
550 46
    protected function createResolveInfo(FieldInterface $field, array $astFields)
551
    {
552 46
        return new ResolveInfo($field, $astFields, $this->executionContext);
553
    }
554
555
}
556