Completed
Pull Request — master (#82)
by Sebastian
03:30
created

Processor   F

Complexity

Total Complexity 93

Size/Duplication

Total Lines 489
Duplicated Lines 7.36 %

Coupling/Cohesion

Components 1
Dependencies 30

Test Coverage

Coverage 95.36%

Importance

Changes 5
Bugs 2 Features 1
Metric Value
wmc 93
lcom 1
cbo 30
dl 36
loc 489
ccs 226
cts 237
cp 0.9536
rs 1.3043
c 5
b 2
f 1

21 Methods

Rating   Name   Duplication   Size   Complexity  
A getExecutionContext() 0 4 1
A getMaxComplexity() 0 4 1
A setMaxComplexity() 0 4 1
A resolveQuery() 0 14 3
B getVariableReferenceArgumentValue() 0 14 5
A __construct() 0 9 2
B processPayload() 0 27 6
A getResponseData() 0 14 3
C resolveField() 13 59 14
A prepareAstArguments() 0 10 3
C prepareArgumentValue() 0 44 14
B resolveObject() 0 22 4
C collectResult() 23 56 10
A resolveScalar() 0 11 1
C resolveList() 0 65 10
B resolveComposite() 0 28 3
A parseAndCreateRequest() 0 13 2
A doResolve() 0 8 2
B parseArgumentsValues() 0 25 5
A getAlias() 0 4 2
A createResolveInfo() 0 4 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Processor often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Processor, and based on these observations, apply Extract Interface, too.

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
                }
251
252 2
                return $result;
253
254 15
            case TypeMap::KIND_SCALAR:
255 2
            case TypeMap::KIND_ENUM:
256
                /** @var $argumentValue AstLiteral|VariableReference */
257 15
                if ($argumentValue instanceof VariableReference) {
258 4
                    return $this->getVariableReferenceArgumentValue($argumentValue, $argumentType, $request);
259 12
                } else if ($argumentValue instanceof AstLiteral) {
260 10
                    return $argumentValue->getValue();
261
                } else {
262 3
                    return $argumentValue;
263
                }
264
        }
265
266
        throw new ResolveException('Argument type not supported');
267
    }
268
269 4
    private function getVariableReferenceArgumentValue(VariableReference $variableReference, AbstractType $argumentType, Request $request)
270
    {
271 4
        $variable = $variableReference->getVariable();
272 4
        if ($variable->getTypeName() != $argumentType->getName()) {
273 1
            throw new ResolveException(sprintf('Invalid variable "%s" type, allowed type is "%s"', $variable->getName(), $argumentType->getName()));
274
        }
275
276 3
        $requestValue = $request->getVariable($variable->getName());
277 3
        if (!$request->hasVariable($variable->getName()) || (null === $requestValue && $variable->isNullable())) {
278
            throw  new ResolveException(sprintf('Variable "%s" does not exist in request', $variable->getName()));
279
        }
280
281 3
        return $requestValue;
282
    }
283
284 25
    protected function resolveObject(FieldInterface $field, AstFieldInterface $ast, $parentValue, $fromUnion = false)
285
    {
286 25
        if (!$fromUnion) {
287 20
            $resolvedValue = $this->doResolve($field, $ast, $parentValue);
288
        } else {
289 7
            $resolvedValue = $parentValue;
290
        }
291
292 25
        $this->resolveValidator->assertValidResolvedValueForField($field, $resolvedValue);
293
294 25
        if (null === $resolvedValue) {
295 5
            return null;
296
        }
297
        /** @var AbstractObjectType $type */
298 24
        $type = $field->getType()->getNullableType();
299
300
        try {
301 24
            return $this->collectResult($field, $type, $ast, $resolvedValue);
302 4
        } catch (\Exception $e) {
303 4
            return null;
304
        }
305
    }
306
307 24
    private function collectResult(FieldInterface $field, AbstractObjectType $type, $ast, $resolvedValue)
308
    {
309
        /** @var AstQuery $ast */
310 24
        $result = [];
311
312 24
        foreach ($ast->getFields() as $astField) {
313
            switch (true) {
314 24
                case $astField instanceof TypedFragmentReference:
315 1
                    $astName = $astField->getTypeName();
316 1
                    $typeName = $type->getName();
317
318 1 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...
319 1
                        foreach ($type->getInterfaces() as $interface) {
320
                            if ($interface->getName() === $astName) {
321
                                $result = array_merge($result, $this->collectResult($field, $type, $astField, $resolvedValue));
322
323
                                break;
324
                            }
325
                        }
326
327 1
                        continue;
328
                    }
329
330 1
                    $result = array_merge($result, $this->collectResult($field, $type, $astField, $resolvedValue));
331
332 1
                    break;
333
334 24
                case $astField instanceof FragmentReference:
335 3
                    $astFragment = $this->executionContext->getRequest()->getFragment($astField->getName());
336 3
                    $astFragmentModel = $astFragment->getModel();
337 3
                    $typeName = $type->getName();
338
339 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...
340 1
                        foreach ($type->getInterfaces() as $interface) {
341 1
                            if ($interface->getName() === $astFragmentModel) {
342
                                $result = array_merge($result, $this->collectResult($field, $type, $astFragment, $resolvedValue));
343
344 1
                                break;
345
                            }
346
347
                        }
348
349 1
                        continue;
350
                    }
351
352 3
                    $result = array_merge($result, $this->collectResult($field, $type, $astFragment, $resolvedValue));
353
354 3
                    break;
355
356
                default:
357 24
                    $result[$this->getAlias($astField)] = $this->resolveField($field, $astField, $resolvedValue, true);
358
            }
359
        }
360
361 24
        return $result;
362
    }
363
364 28
    protected function resolveScalar(FieldInterface $field, AstFieldInterface $ast, $parentValue)
365
    {
366 28
        $resolvedValue = $this->doResolve($field, $ast, $parentValue);
367
368 28
        $this->resolveValidator->assertValidResolvedValueForField($field, $resolvedValue);
369
370
        /** @var AbstractScalarType $type */
371 27
        $type = $field->getType()->getNullableType();
372
373 27
        return $type->serialize($resolvedValue);
374
    }
375
376 14
    protected function resolveList(FieldInterface $field, AstFieldInterface $ast, $parentValue)
377
    {
378
        /** @var AstQuery $ast */
379 14
        $resolvedValue = $this->doResolve($field, $ast, $parentValue);
380
381 14
        $this->resolveValidator->assertValidResolvedValueForField($field, $resolvedValue);
382
383 13
        if (null === $resolvedValue) {
384 5
            return null;
385
        }
386
387
        /** @var AbstractListType $type */
388 12
        $type     = $field->getType()->getNullableType();
389 12
        $itemType = $type->getNamedType();
390
391 12
        $fakeAst = clone $ast;
392 12
        if ($fakeAst instanceof AstQuery) {
393 12
            $fakeAst->setArguments([]);
394
        }
395
396 12
        $fakeField = new Field([
397 12
            'name' => $field->getName(),
398 12
            'type' => $itemType,
399
        ]);
400
401 12
        $result = [];
402 12
        foreach ($resolvedValue as $resolvedValueItem) {
403
            try {
404 11
                $fakeField->getConfig()->set('resolve', function () use ($resolvedValueItem) {
405 11
                    return $resolvedValueItem;
406 11
                });
407
408 11
                switch ($itemType->getNullableType()->getKind()) {
409 11
                    case TypeMap::KIND_ENUM:
410 10
                    case TypeMap::KIND_SCALAR:
411 4
                        $value = $this->resolveScalar($fakeField, $fakeAst, $resolvedValueItem);
412
413 3
                        break;
414
415
416 8
                    case TypeMap::KIND_OBJECT:
417 5
                        $value = $this->resolveObject($fakeField, $fakeAst, $resolvedValueItem);
418
419 5
                        break;
420
421 3
                    case TypeMap::KIND_UNION:
422 3
                    case TypeMap::KIND_INTERFACE:
423 3
                        $value = $this->resolveComposite($fakeField, $fakeAst, $resolvedValueItem);
424
425 3
                        break;
426
427
                    default:
428 10
                        $value = null;
429
                }
430 2
            } catch (\Exception $e) {
431 2
                $this->executionContext->addError($e);
432
433 2
                $value = null;
434
            }
435
436 11
            $result[] = $value;
437
        }
438
439 12
        return $result;
440
    }
441
442 7
    protected function resolveComposite(FieldInterface $field, AstFieldInterface $ast, $parentValue)
443
    {
444
        /** @var AstQuery $ast */
445 7
        $resolvedValue = $this->doResolve($field, $ast, $parentValue);
446
447 7
        $this->resolveValidator->assertValidResolvedValueForField($field, $resolvedValue);
448
449
        /** @var AbstractUnionType $type */
450 7
        $type         = $field->getType()->getNullableType();
451 7
        $resolvedType = $type->resolveType($resolvedValue);
452
453 7
        if (!$resolvedType) {
454
            throw new ResolveException('Resoling function must return type');
455
        }
456
457 7
        if ($type instanceof AbstractInterfaceType) {
458 6
            $this->resolveValidator->assertTypeImplementsInterface($resolvedType, $type);
459
        } else {
460 1
            $this->resolveValidator->assertTypeInUnionTypes($resolvedType, $type);
461
        }
462
463 7
        $fakeField = new Field([
464 7
            'name' => $field->getName(),
465 7
            'type' => $resolvedType,
466
        ]);
467
468 7
        return $this->resolveObject($fakeField, $ast, $resolvedValue, true);
469
    }
470
471 37
    protected function parseAndCreateRequest($payload, $variables = [])
472
    {
473 37
        if (empty($payload)) {
474 1
            throw new \InvalidArgumentException('Must provide an operation.');
475
        }
476
477 37
        $parser  = new Parser();
478 37
        $request = new Request($parser->parse($payload), $variables);
479
480 37
        (new RequestValidator())->validate($request);
481
482 36
        $this->executionContext->setRequest($request);
483 36
    }
484
485 33
    protected function doResolve(FieldInterface $field, AstFieldInterface $ast, $parentValue = null)
486
    {
487
        /** @var AstQuery|AstField $ast */
488 33
        $arguments = $this->parseArgumentsValues($field, $ast);
489 33
        $astFields = $ast instanceof AstQuery ? $ast->getFields() : [];
490
491 33
        return $field->resolve($parentValue, $arguments, $this->createResolveInfo($field, $astFields));
492
    }
493
494 33
    protected function parseArgumentsValues(FieldInterface $field, AstFieldInterface $ast)
495
    {
496 33
        $values   = [];
497 33
        $defaults = [];
498
499 33
        foreach ($field->getArguments() as $argument) {
500
            /** @var $argument InputField */
501 19
            if ($argument->getConfig()->has('default')) {
502 19
                $defaults[$argument->getName()] = $argument->getConfig()->getDefaultValue();
503
            }
504
        }
505
506 33
        foreach ($ast->getArguments() as $astArgument) {
507 13
            $argument     = $field->getArgument($astArgument->getName());
508 13
            $argumentType = $argument->getType()->getNullableType();
509
510 13
            $values[$argument->getName()] = $argumentType->parseValue($astArgument->getValue());
511
512 13
            if (isset($defaults[$argument->getName()])) {
513 13
                unset($defaults[$argument->getName()]);
514
            }
515
        }
516
517 33
        return array_merge($values, $defaults);
518
    }
519
520 36
    private function getAlias(AstFieldInterface $ast)
521
    {
522 36
        return $ast->getAlias() ?: $ast->getName();
523
    }
524
525 33
    protected function createResolveInfo(FieldInterface $field, array $astFields)
526
    {
527 33
        return new ResolveInfo($field, $astFields, $this->executionContext);
528
    }
529
530
}
531