Executor   F
last analyzed

Complexity

Total Complexity 91

Size/Duplication

Total Lines 518
Duplicated Lines 2.7 %

Coupling/Cohesion

Components 1
Dependencies 15

Test Coverage

Coverage 95.1%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
wmc 91
c 3
b 0
f 0
lcom 1
cbo 15
dl 14
loc 518
ccs 194
cts 204
cp 0.951
rs 1.5789

15 Methods

Rating   Name   Duplication   Size   Complexity  
B execute() 0 21 5
C buildExecutionContext() 0 32 11
A executeOperation() 0 10 2
A getOperationRootType() 0 16 4
A executeFieldsSerially() 0 14 3
A executeFields() 0 4 1
C collectFields() 6 54 15
A shouldIncludeNode() 8 19 4
B doesFragmentConditionMatch() 0 18 5
A getFieldEntryKey() 0 4 2
B resolveField() 0 23 4
A resolveFieldOrError() 0 17 3
C completeField() 0 67 17
B defaultResolveFn() 0 16 8
B getFieldDefinition() 0 18 7

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 Executor 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 Executor, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Fubhy\GraphQL\Executor;
4
5
use Fubhy\GraphQL\Language\Node;
6
use Fubhy\GraphQL\Language\Node\Document;
7
use Fubhy\GraphQL\Language\Node\Field;
8
use Fubhy\GraphQL\Language\Node\FragmentDefinition;
9
use Fubhy\GraphQL\Language\Node\OperationDefinition;
10
use Fubhy\GraphQL\Language\Node\SelectionSet;
11
use Fubhy\GraphQL\Schema;
12
use Fubhy\GraphQL\Type\Definition\FieldDefinition;
13
use Fubhy\GraphQL\Type\Definition\Types\EnumType;
14
use Fubhy\GraphQL\Type\Definition\Types\InterfaceType;
15
use Fubhy\GraphQL\Type\Definition\Types\ListModifier;
16
use Fubhy\GraphQL\Type\Definition\Types\NonNullModifier;
17
use Fubhy\GraphQL\Type\Definition\Types\ObjectType;
18
use Fubhy\GraphQL\Type\Definition\Types\ScalarType;
19
use Fubhy\GraphQL\Type\Definition\Types\TypeInterface;
20
use Fubhy\GraphQL\Type\Definition\Types\UnionType;
21
use Fubhy\GraphQL\Type\Directives\Directive;
22
use Fubhy\GraphQL\Type\Introspection;
23
use Fubhy\GraphQL\Utility\TypeInfo;
24
25
/**
26
 * Terminology
27
 *
28
 * "Definitions" are the generic name for top-level statements in the document.
29
 * Examples of this include:
30
 * 1) Operations (such as a query)
31
 * 2) Fragments
32
 *
33
 * "Operations" are a generic name for requests in the document.
34
 * Examples of this include:
35
 * 1) query,
36
 * 2) mutation
37
 *
38
 * "Selections" are the statements that can appear legally and at
39
 * single level of the query. These include:
40
 * 1) field references e.g "a"
41
 * 2) fragment "spreads" e.g. "...c"
42
 * 3) inline fragment "spreads" e.g. "...on Type { a }"
43
 */
44
class Executor
45
{
46
    protected static $UNDEFINED;
47
48
    /**
49
     * @param \Fubhy\GraphQL\Schema $schema
50
     * @param $root
51
     * @param \Fubhy\GraphQL\Language\Node\Document $ast
52
     * @param string|null $operation
53
     * @param array|null $args
54
     *
55
     * @return array
56
     */
57 300
    public static function execute(Schema $schema, $root, Document $ast, $operation = NULL, array $args = NULL)
58
    {
59 300
        if (!self::$UNDEFINED) {
60 3
            self::$UNDEFINED = new \stdClass();
61 3
        }
62
63
        try {
64 300
            $errors = new \ArrayObject();
65 300
            $context = self::buildExecutionContext($schema, $root, $ast, $operation, $args, $errors);
66 279
            $data = self::executeOperation($context, $root, $context->operation);
67 300
        } catch (\Exception $e) {
68 30
            $errors[] = $e;
69
        }
70
71 300
        $result = ['data' => isset($data) ? $data : NULL];
72 300
        if (count($errors) > 0) {
73 57
            $result['errors'] = $errors->getArrayCopy();
74 60
        }
75
76 300
        return $result;
77
    }
78
79
    /**
80
     * Constructs a ExecutionContext object from the arguments passed to
81
     * execute, which we will pass throughout the other execution methods.
82
     *
83
     * @param Schema $schema
84
     * @param $root
85
     * @param Document $ast
86
     * @param string|null $operationName
87
     * @param array $args
88
     * @param $errors
89
     *
90
     * @return ExecutionContext
91
     *
92
     * @throws \Exception
93
     */
94 300
    protected static function buildExecutionContext(Schema $schema, $root, Document $ast, $operationName = NULL, array $args = NULL, &$errors)
95
    {
96 300
        $operations = [];
97 300
        $fragments = [];
98
99 300
        foreach ($ast->get('definitions') as $statement) {
100 300
            switch ($statement::KIND) {
101 300
                case Node::KIND_OPERATION_DEFINITION:
102 300
                    $operations[$statement->get('name') ? $statement->get('name')->get('value') : ''] = $statement;
103 300
                    break;
104
105 33
                case Node::KIND_FRAGMENT_DEFINITION:
106 33
                    $fragments[$statement->get('name')->get('value')] = $statement;
107 33
                    break;
108
            }
109 300
        }
110
111 300
        if (!$operationName && count($operations) !== 1) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $operationName of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
112 3
            throw new \Exception('Must provide operation name if query contains multiple operations');
113
        }
114
115 297
        $name = $operationName ?: key($operations);
116 297
        if (!isset($operations[$name])) {
117
            throw new \Exception('Unknown operation name: ' . $name);
118
        }
119
120 297
        $operation = $operations[$name];
121 297
        $variables = Values::getVariableValues($schema, $operation->get('variableDefinitions') ?: [], $args ?: []);
122 279
        $context = new ExecutionContext($schema, $fragments, $root, $operation, $variables, $errors);
123
124 279
        return $context;
125
    }
126
127
    /**
128
     * Implements the "Evaluating operations" section of the spec.
129
     */
130 279
    protected static function executeOperation(ExecutionContext $context, $root, OperationDefinition $operation)
131
    {
132 279
        $type = self::getOperationRootType($context->schema, $operation);
133 279
        $fields = self::collectFields($context, $type, $operation->get('selectionSet'), new \ArrayObject(), new \ArrayObject());
134 279
        if ($operation->get('operation') === 'mutation') {
135 6
            return self::executeFieldsSerially($context, $type, $root, $fields->getArrayCopy());
136
        }
137
138 273
        return self::executeFields($context, $type, $root, $fields);
139
    }
140
141
142
    /**
143
     * Extracts the root type of the operation from the schema.
144
     *
145
     * @param Schema $schema
146
     * @param OperationDefinition $operation
147
     *
148
     * @return ObjectType
149
     *
150
     * @throws \Exception
151
     */
152 279
    protected static function getOperationRootType(Schema $schema, OperationDefinition $operation)
153
    {
154 279
        switch ($operation->get('operation')) {
155 279
            case 'query':
156 273
                return $schema->getQueryType();
157 6
            case 'mutation':
158 6
                $mutationType = $schema->getMutationType();
159 6
                if (!$mutationType) {
160
                    throw new \Exception('Schema is not configured for mutations.');
161
                }
162
163 6
                return $mutationType;
164
        }
165
166
        throw new \Exception('Can only execute queries and mutations.');
167
    }
168
169
    /**
170
     * Implements the "Evaluating selection sets" section of the spec for "write" mode.
171
     */
172 279
    protected static function executeFieldsSerially(ExecutionContext $context, ObjectType $parent, $source, $fields)
173
    {
174 279
        $results = [];
175 279
        foreach ($fields as $response => $asts) {
176 279
            $result = self::resolveField($context, $parent, $source, $asts);
177
178 273
            if ($result !== self::$UNDEFINED) {
179
                // Undefined means that field is not defined in schema.
180 270
                $results[$response] = $result;
181 270
            }
182 273
        }
183
184 273
        return $results;
185
    }
186
187
    /**
188
     * Implements the "Evaluating selection sets" section of the spec for "read" mode.
189
     *
190
     * @param ExecutionContext $context
191
     * @param ObjectType $parent
192
     * @param $source
193
     * @param $fields
194
     *
195
     * @return array
196
     */
197 273
    protected static function executeFields(ExecutionContext $context, ObjectType $parent, $source, $fields)
198
    {
199 273
        return self::executeFieldsSerially($context, $parent, $source, $fields);
200
    }
201
202
    /**
203
     * Given a selectionSet, adds all of the fields in that selection to
204
     * the passed in map of fields, and returns it at the end.
205
     *
206
     * @param ExecutionContext $context
207
     * @param ObjectType $type
208
     * @param SelectionSet $set
209
     * @param $fields
210
     * @param $visited
211
     *
212
     * @return \ArrayObject
213
     */
214 279
    protected static function collectFields(ExecutionContext $context, ObjectType $type, SelectionSet $set, $fields, $visited)
215
    {
216 279
        $count = count($set->get('selections'));
217 279
        for ($i = 0; $i < $count; $i++) {
218 279
            $selection = $set->get('selections')[$i];
219 279
            switch ($selection::KIND) {
220 279
                case Node::KIND_FIELD:
221 279
                    if (!self::shouldIncludeNode($context, $selection->get('directives'))) {
222 3
                        continue;
223
                    }
224
225 279
                    $name = self::getFieldEntryKey($selection);
226 279
                    if (!isset($fields[$name])) {
227 279
                        $fields[$name] = new \ArrayObject();
228 279
                    }
229
230 279
                    $fields[$name][] = $selection;
231 279
                    break;
232
233 39
                case Node::KIND_INLINE_FRAGMENT:
234 15 View Code Duplication
                    if (!self::shouldIncludeNode($context, $selection->get('directives')) || !self::doesFragmentConditionMatch($context, $selection, $type)) {
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...
235 12
                        continue;
236
                    }
237
238 15
                    self::collectFields(
239 15
                        $context,
240 15
                        $type,
241 15
                        $selection->get('selectionSet'),
242 15
                        $fields,
243
                        $visited
244 15
                    );
245
246 15
                    break;
247
248 30
                case Node::KIND_FRAGMENT_SPREAD:
249 30
                    $fragName = $selection->get('name')->get('value');
250 30
                    if (!empty($visited[$fragName]) || !self::shouldIncludeNode($context, $selection->get('directives'))) {
251 6
                        continue;
252
                    }
253
254 30
                    $visited[$fragName] = TRUE;
255 30
                    $fragment = isset($context->fragments[$fragName]) ? $context->fragments[$fragName] : NULL;
256 30 View Code Duplication
                    if (!$fragment || !self::shouldIncludeNode($context, $fragment->get('directives')) || !self::doesFragmentConditionMatch($context, $fragment, $type)) {
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...
257 6
                        continue;
258
                    }
259
260 27
                    self::collectFields($context, $type, $fragment->get('selectionSet'), $fields, $visited);
261
262 27
                    break;
263
            }
264 279
        }
265
266 279
        return $fields;
267
    }
268
269
    /**
270
     * Determines if a field should be included based on @if and @unless directives.
271
     */
272 279
    protected static function shouldIncludeNode(ExecutionContext $exeContext, $directives)
273
    {
274 279
        $skip = Directive::skipDirective();
275 279
        $include = Directive::includeDirective();
276
277 279
        foreach ($directives as $directive) {
278 12 View Code Duplication
            if ($directive->get('name')->get('value') === $skip->getName()) {
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...
279 12
                $values = Values::getArgumentValues($skip->getArguments(), $directive->get('arguments'), $exeContext->variables);
280 12
                return empty($values['if']);
281
            }
282
283 12 View Code Duplication
            if ($directive->get('name')->get('value') === $include->getName()) {
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...
284 12
                $values = Values::getArgumentValues($skip->getArguments(), $directive->get('arguments'), $exeContext->variables);
285 12
                return !empty($values['if']);
286
            }
287 279
        }
288
289 279
        return TRUE;
290
    }
291
292
    /**
293
     * Determines if a fragment is applicable to the given type.
294
     *
295
     * @param ExecutionContext $context
296
     * @param $fragment
297
     * @param ObjectType $type
298
     *
299
     * @return bool
300
     */
301 39
    protected static function doesFragmentConditionMatch(ExecutionContext $context, $fragment, ObjectType $type)
302
    {
303 39
        $typeCondition = $fragment->get('typeCondition');
304 39
        if (!$typeCondition) {
305
            return TRUE;
306
        }
307
308 39
        $conditionalType = TypeInfo::typeFromAST($context->schema, $typeCondition);
309 39
        if ($conditionalType->getName() === $type->getName()) {
310 36
            return TRUE;
311
        }
312
313 12
        if ($conditionalType instanceof InterfaceType || $conditionalType instanceof UnionType) {
314 3
            return $conditionalType->isPossibleType($type);
315
        }
316
317 12
        return FALSE;
318
    }
319
320
    /**
321
     * Implements the logic to compute the key of a given field's entry
322
     *
323
     * @param Field $node
324
     *
325
     * @return string
326
     */
327 279
    protected static function getFieldEntryKey(Field $node)
328
    {
329 279
        return $node->get('alias') ? $node->get('alias')->get('value') : $node->get('name')->get('value');
330
    }
331
332
    /**
333
     * A wrapper function for resolving the field, that catches the error
334
     * and adds it to the context's global if the error is not rethrowable.
335
     *
336
     * @param ExecutionContext $context
337
     * @param ObjectType $parent
338
     * @param $source
339
     * @param $asts
340
     *
341
     * @return array|mixed|null|string
342
     *
343
     * @throws \Exception
344
     */
345 279
    protected static function resolveField(ExecutionContext $context, ObjectType $parent, $source, $asts)
346
    {
347 279
        $definition = self::getFieldDefinition($context->schema, $parent, $asts[0]);
348 279
        if (!$definition) {
349 12
            return self::$UNDEFINED;
350
        }
351
352
        // If the field type is non-nullable, then it is resolved without any
353
        // protection from errors.
354 276
        if ($definition->getType() instanceof NonNullModifier) {
355 102
            return self::resolveFieldOrError($context, $parent, $source, $asts, $definition);
356
        }
357
358
        // Otherwise, error protection is applied, logging the error and
359
        // resolving a null value for this field if one is encountered.
360
        try {
361 270
            $result = self::resolveFieldOrError($context, $parent, $source, $asts, $definition);
362 252
            return $result;
363 27
        } catch (\Exception $error) {
364 27
            $context->errors[] = $error;
365 27
            return NULL;
366
        }
367
    }
368
369
    /**
370
     * Resolves the field on the given source object.
371
     *
372
     * In particular, this figures out the object that the field returns using
373
     * the resolve function, then calls completeField to coerce scalars or
374
     * execute the sub selection set for objects.
375
     *
376
     * @param ExecutionContext $context
377
     * @param ObjectType $parent
378
     * @param $source
379
     * @param $asts
380
     * @param FieldDefinition $definition
381
     *
382
     * @return array|mixed|null|string
383
     *
384
     * @throws \Exception
385
     */
386 276
    protected static function resolveFieldOrError(ExecutionContext $context, ObjectType $parent, $source, $asts, FieldDefinition $definition)
387
    {
388 276
        $ast = $asts[0];
389 276
        $type = $definition->getType();
390 276
        $resolver = $definition->getResolveCallback() ?: [__CLASS__, 'defaultResolveFn'];
391 276
        $data = $definition->getResolveData();
392
        $args = Values::getArgumentValues($definition->getArguments(), $ast->get('arguments'), $context->variables);
393
394 276
        try {
395 276
            // @todo Change the resolver function syntax to use a value object.
396 15
            $result = call_user_func($resolver, $source, $args, $context->root, $ast, $type, $parent, $context->schema, $data);
397
        } catch (\Exception $error) {
398
            throw $error;
399 270
        }
400
401
        return self::completeField($context, $type, $asts, $result);
402
    }
403
404
    /**
405
     * Implements the instructions for completeValue as defined in the
406
     * "Field entries" section of the spec.
407
     *
408
     * If the field type is Non-Null, then this recursively completes the value
409
     * for the inner type. It throws a field error if that completion returns null,
410
     * as per the "Nullability" section of the spec.
411
     *
412
     * If the field type is a List, then this recursively completes the value
413
     * for the inner type on each item in the list.
414
     *
415
     * If the field type is a Scalar or Enum, ensures the completed value is a legal
416
     * value of the type by calling the `coerce` method of GraphQL type definition.
417
     *
418
     * Otherwise, the field type expects a sub-selection set, and will complete the
419
     * value by evaluating all sub-selections.
420
     *
421
     * @param ExecutionContext $context
422
     * @param TypeInterface $type
423
     * @param $asts
424
     * @param $result
425
     *
426
     * @return array|mixed|null|string
427
     *
428 270
     * @throws \Exception
429
     */
430
    protected static function completeField(ExecutionContext $context, TypeInterface $type, $asts, &$result)
431
    {
432 270
        // If field type is NonNullModifier, complete for inner type, and throw field error
433 102
        // if result is null.
434
        if ($type instanceof NonNullModifier) {
435 102
            $completed = self::completeField($context, $type->getWrappedType(), $asts, $result);
436 18
437
            if ($completed === NULL) {
438
                throw new \Exception('Cannot return null for non-nullable type.');
439 90
            }
440
441
            return $completed;
442
        }
443 270
444 72
        // If result is null-like, return null.
445
        if (!isset($result)) {
446
            return NULL;
447
        }
448 255
449 90
        // If field type is List, complete each item in the list with the inner type
450
        if ($type instanceof ListModifier) {
451 90
            $itemType = $type->getWrappedType();
452
453
            if (!(is_array($result) || $result instanceof \ArrayObject)) {
454
                throw new \Exception('User Error: expected iterable, but did not find one.');
455 90
            }
456 90
457 90
            $tmp = [];
458 90
            foreach ($result as $item) {
459
                $tmp[] = self::completeField($context, $itemType, $asts, $item);
460 84
            }
461
462
            return $tmp;
463
        }
464
465 255
        // If field type is Scalar or Enum, coerce to a valid value, returning
466 231
        // null if coercion is not possible.
467
        if ($type instanceof ScalarType || $type instanceof EnumType) {
468
            if (!method_exists($type, 'coerce')) {
469
                throw new \Exception('Missing coerce method on type.');
470 231
            }
471
472
            return $type->coerce($result);
473
        }
474
475 162
        // Field type must be Object, Interface or Union and expect
476 162
        // sub-selections.
477
        $objectType = $type instanceof ObjectType ? $type : ($type instanceof InterfaceType || $type instanceof UnionType ? $type->resolveType($result) : NULL);
478
        if (!$objectType) {
479
            return NULL;
480
        }
481 162
482 162
        // Collect sub-fields to execute to complete this value.
483
        $subFieldASTs = new \ArrayObject();
484 162
        $visitedFragmentNames = new \ArrayObject();
485 162
486 162
        $count = count($asts);
487
        for ($i = 0; $i < $count; $i++) {
488 162
            $selectionSet = $asts[$i]->get('selectionSet');
489 162
490 162
            if ($selectionSet) {
491 162
                $subFieldASTs = self::collectFields($context, $objectType, $selectionSet, $subFieldASTs, $visitedFragmentNames);
492
            }
493 162
        }
494
495
        return self::executeFields($context, $objectType, $result, $subFieldASTs);
496
    }
497
498
    /**
499
     * If a resolve function is not given, then a default resolve behavior is used
500
     * which takes the property of the source object of the same name as the field
501
     * and returns it as the result, or if it's a function, returns the result
502
     * of calling that function.
503
     *
504
     * @param $source
505
     * @param $args
506
     * @param $root
507
     * @param $ast
508
     *
509 147
     * @return mixed|null
510
     */
511 147
    public static function defaultResolveFn($source, $args, $root, $ast)
512 147
    {
513
        $property = NULL;
514 147
        $key = $ast->get('name')->get('value');
515 132
516 132
        if ((is_array($source) || $source instanceof \ArrayAccess) && isset($source[$key])) {
517 15
            $property = $source[$key];
518 15
        }
519 15
        else if (is_object($source) && property_exists($source, $key)) {
520 15
            if ($key !== 'ofType') {
521 15
                $property = $source->{$key};
522
            }
523 147
        }
524
525
        return is_callable($property) ? call_user_func($property, $source) : $property;
526
    }
527
528
    /**
529
     * This method looks up the field on the given type defintion.
530
     * It has special casing for the two introspection fields, __schema
531
     * and __typename. __typename is special because it can always be
532
     * queried as a field, even in situations where no other fields
533
     * are allowed, like on a Union. __schema could get automatically
534
     * added to the query type, but that would require mutating type
535
     * definitions, which would cause issues.
536
     *
537
     * @param Schema $schema
538
     * @param ObjectType $parent
539
     * @param Field $ast
540
     *
541 279
     * @return FieldDefinition
542
     */
543 279
    protected static function getFieldDefinition(Schema $schema, ObjectType $parent, Field $ast)
544 279
    {
545 279
        $name = $ast->get('name')->get('value');
546 279
        $schemaMeta = Introspection::schemaMetaFieldDefinition();
547
        $typeMeta = Introspection::typeMetaFieldDefinition();
548 279
        $typeNameMeta = Introspection::typeNameMetaFieldDefinition();
549 15
550 279
        if ($name === $schemaMeta->getName() && $schema->getQueryType() === $parent) {
551 42
            return $schemaMeta;
552 279
        } else if ($name === $typeMeta->getName() && $schema->getQueryType() === $parent) {
553 18
            return $typeMeta;
554
        } else if ($name === $typeNameMeta->getName()) {
555
            return $typeNameMeta;
556 279
        }
557 279
558
        $tmp = $parent->getFields();
559
        return isset($tmp[$name]) ? $tmp[$name] : NULL;
560
    }
561
}
562