Completed
Pull Request — master (#80)
by Christoffer
02:18
created

ExecutionStrategy::collectAndExecuteSubFields()   B

Complexity

Conditions 4
Paths 6

Size

Total Lines 25
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 11
nc 6
nop 5
dl 0
loc 25
rs 8.5806
c 0
b 0
f 0
1
<?php
2
3
namespace Digia\GraphQL\Execution;
4
5
use Digia\GraphQL\Error\InvalidTypeException;
6
use Digia\GraphQL\Execution\Resolver\ResolveInfo;
7
use Digia\GraphQL\Language\Node\FieldNode;
8
use Digia\GraphQL\Language\Node\FragmentDefinitionNode;
9
use Digia\GraphQL\Language\Node\NodeKindEnum;
10
use Digia\GraphQL\Language\Node\OperationDefinitionNode;
11
use Digia\GraphQL\Language\Node\SelectionSetNode;
12
use Digia\GraphQL\Type\Definition\Field;
13
use Digia\GraphQL\Type\Definition\ObjectType;
14
use Digia\GraphQL\Type\Schema;
15
use function Digia\GraphQL\Type\SchemaMetaFieldDefinition;
16
use function Digia\GraphQL\Type\TypeMetaFieldDefinition;
17
use function Digia\GraphQL\Type\TypeNameMetaFieldDefinition;
18
19
/**
20
 * Class AbstractStrategy
21
 * @package Digia\GraphQL\Execution\Strategies
22
 */
23
abstract class ExecutionStrategy
24
{
25
    /**
26
     * @var ExecutionContext
27
     */
28
    protected $context;
29
30
    /**
31
     * @var OperationDefinitionNode
32
     */
33
    protected $operation;
34
35
    /**
36
     * @var mixed
37
     */
38
    protected $rootValue;
39
40
41
    /**
42
     * @var ValuesResolver
43
     */
44
    protected $valuesResolver;
45
46
    /**
47
     * @var array
48
     */
49
    protected $finalResult;
50
51
    /**
52
     * AbstractStrategy constructor.
53
     * @param ExecutionContext        $context
54
     *
55
     * @param OperationDefinitionNode $operation
56
     */
57
    public function __construct(
58
        ExecutionContext $context,
59
        OperationDefinitionNode $operation,
60
        $rootValue,
61
        $valueResolver
62
    ) {
63
        $this->context        = $context;
64
        $this->operation      = $operation;
65
        $this->rootValue      = $rootValue;
66
        $this->valuesResolver = $valueResolver;
67
    }
68
69
    /**
70
     * @return array|null
71
     */
72
    abstract function execute(): ?array;
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
73
74
    /**
75
     * @param ObjectType       $runtimeType
76
     * @param SelectionSetNode $selectionSet
77
     * @param                  $fields
78
     * @param                  $visitedFragmentNames
79
     * @return \ArrayObject
80
     */
81
    protected function collectFields(
82
        ObjectType $runtimeType,
83
        SelectionSetNode $selectionSet,
84
        $fields,
85
        $visitedFragmentNames
86
    ) {
87
        foreach ($selectionSet->getSelections() as $selection) {
88
            /** @var FieldNode $selection */
89
            switch ($selection->getKind()) {
90
                case NodeKindEnum::FIELD:
91
                    $name = $this->getFieldNameKey($selection);
92
                    if (!isset($runtimeType->getFields()[$selection->getNameValue()])) {
93
                        continue 2;
94
                    }
95
                    if (!isset($fields[$name])) {
96
                        $fields[$name] = new \ArrayObject();
97
                    }
98
                    $fields[$name][] = $selection;
99
                    break;
100
                case NodeKindEnum::INLINE_FRAGMENT:
101
                    //TODO check if should include this node
102
                    $this->collectFields(
103
                        $runtimeType,
104
                        $selection->getSelectionSet(),
105
                        $fields,
106
                        $visitedFragmentNames
107
                    );
108
                    break;
109
                case NodeKindEnum::FRAGMENT_SPREAD:
110
                    //TODO check if should include this node
111
                    if (!empty($visitedFragmentNames[$selection->getNameValue()])) {
112
                        continue 2;
113
                    }
114
                    $visitedFragmentNames[$selection->getNameValue()] = true;
115
                    /** @var FragmentDefinitionNode $fragment */
116
                    $fragment = $this->context->getFragments()[$selection->getNameValue()];
117
                    $this->collectFields(
118
                        $runtimeType,
119
                        $fragment->getSelectionSet(),
120
                        $fields,
121
                        $visitedFragmentNames
122
                    );
123
                    break;
124
            }
125
        }
126
127
        return $fields;
128
    }
129
130
    /**
131
     * @TODO: consider to move this to FieldNode
132
     * @param FieldNode $node
133
     * @return string
134
     */
135
    private function getFieldNameKey(FieldNode $node)
136
    {
137
        return $node->getAlias() ? $node->getAlias()->getValue() : $node->getNameValue();
138
    }
139
140
    /**
141
     * Implements the "Evaluating selection sets" section of the spec
142
     * for "read" mode.
143
     * @param ObjectType $parentType
144
     * @param            $source
145
     * @param            $path
146
     * @param            $fields
147
     *
148
     * @return array
149
     * @throws InvalidTypeException
150
     */
151
    protected function executeFields(
152
        ObjectType $parentType,
153
        $source,
154
        $path,
155
        $fields
156
    ): array {
157
        $finalResults = [];
158
159
        foreach ($fields as $fieldName => $fieldNodes) {
160
            $fieldPath   = $path;
161
            $fieldPath[] = $fieldName;
162
163
            $result = $this->resolveField($parentType,
164
                $source,
165
                $fieldNodes,
166
                $fieldPath
167
            );
168
169
            $finalResults[$fieldName] = $result;
170
        }
171
172
        return $finalResults;
173
    }
174
175
    /**
176
     * @param Schema     $schema
177
     * @param ObjectType $parentType
178
     * @param string     $fieldName
179
     * @return \Digia\GraphQL\Type\Definition\Field|null
180
     * @throws InvalidTypeException
181
     */
182
    public function getFieldDefinition(Schema $schema, ObjectType $parentType, string $fieldName)
183
    {
184
        $schemaMetaFieldDifinition   = SchemaMetaFieldDefinition();
185
        $typeMetaFieldDefinition     = TypeMetaFieldDefinition();
186
        $typeNameMetaFieldDefinition = TypeNameMetaFieldDefinition();
187
188
        if ($fieldName === $schemaMetaFieldDifinition->getName() && $schema->getQuery() === $parentType) {
189
            return $schemaMetaFieldDifinition;
190
        }
191
192
        if ($fieldName === $typeMetaFieldDefinition->getName() && $schema->getQuery() === $parentType) {
193
            return $typeMetaFieldDefinition;
194
        }
195
196
        if ($fieldName === $typeNameMetaFieldDefinition->getName()) {
197
            return $typeNameMetaFieldDefinition;
198
        }
199
200
        $fields = $parentType->getFields();
201
202
        return $fields[$fieldName] ?? null;
203
    }
204
205
206
    /**
207
     * @param ObjectType $parentType
208
     * @param            $source
209
     * @param            $fieldNodes
210
     * @param            $path
211
     *
212
     * @return mixed
213
     * @throws InvalidTypeException
214
     */
215
    protected function resolveField(
216
        ObjectType $parentType,
217
        $source,
218
        $fieldNodes,
219
        $path
220
    ) {
221
        /** @var FieldNode $fieldNode */
222
        $fieldNode = $fieldNodes[0];
223
224
        $field = $this->getFieldDefinition($this->context->getSchema(), $parentType, $fieldNode->getNameValue());
225
226
        if (!$field) {
227
            return null;
228
        }
229
230
        $info = $this->buildResolveInfo($fieldNodes, $fieldNode, $field, $parentType, $path, $this->context);
231
232
        $resolveFunction = $this->determineResolveFunction($field, $parentType, $this->context);
233
234
        $result = $this->resolveOrError(
235
            $field,
236
            $fieldNode,
237
            $resolveFunction,
238
            $source,
239
            $this->context,
240
            $info
241
        );
242
243
        $result = $this->collectAndExecuteSubFields(
244
            $parentType,
245
            $fieldNodes,
246
            $info,
247
            $path,
248
            $result// $result is passed as $source
249
        );
250
251
        return $result;
252
    }
253
254
    /**
255
     * @param array            $fieldNodes
256
     * @param FieldNode        $fieldNode
257
     * @param Field            $field
258
     * @param ObjectType       $parentType
259
     * @param                  $path
260
     * @param ExecutionContext $context
261
     * @return ResolveInfo
262
     */
263
    private function buildResolveInfo(
264
        \ArrayAccess $fieldNodes,
265
        FieldNode $fieldNode,
266
        Field $field,
267
        ObjectType $parentType,
268
        $path,
269
        ExecutionContext $context
270
    ) {
271
        return new ResolveInfo([
272
            'fieldName'      => $fieldNode->getNameValue(),
273
            'fieldNodes'     => $fieldNodes,
274
            'returnType'     => $field->getType(),
275
            'parentType'     => $parentType,
276
            'path'           => $path,
277
            'schema'         => $context->getSchema(),
278
            'fragments'      => $context->getFragments(),
279
            'rootValue'      => $context->getRootValue(),
280
            'operation'      => $context->getOperation(),
281
            'variableValues' => $context->getVariableValues(),
282
        ]);
283
    }
284
285
    /**
286
     * @param Field            $field
287
     * @param ObjectType       $parentType
288
     * @param ExecutionContext $context
289
     * @return callable|mixed|null
290
     */
291
    private function determineResolveFunction(Field $field, ObjectType $parentType, ExecutionContext $context)
0 ignored issues
show
Unused Code introduced by
The parameter $context is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

291
    private function determineResolveFunction(Field $field, ObjectType $parentType, /** @scrutinizer ignore-unused */ ExecutionContext $context)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
292
    {
293
        if ($field->hasResolve()) {
294
            return $field->getResolve();
295
        }
296
297
        if ($parentType->hasResolve()) {
298
            return $parentType->getResolve();
299
        }
300
301
        return $this->context->getFieldResolver();
302
    }
303
304
305
    /**
306
     * @param Field            $field
307
     * @param FieldNode        $fieldNode
308
     * @param callable         $resolveFunction
309
     * @param                  $source
310
     * @param ExecutionContext $context
311
     * @param ResolveInfo      $info
312
     * @return array|\Throwable
313
     */
314
    private function resolveOrError(
315
        Field $field,
316
        FieldNode $fieldNode,
317
        callable $resolveFunction,
318
        $source,
319
        ExecutionContext $context,
320
        ResolveInfo $info
321
    ) {
322
        try {
323
            $args = $this->valuesResolver->coerceArgumentValues($field, $fieldNode, $context->getVariableValues());
324
325
            return $resolveFunction($source, $args, $context->getContextValue(), $info);
326
        } catch (\Throwable $error) {
327
            return $error;
328
        }
329
    }
330
331
    /**
332
     * @param ObjectType  $returnType
333
     * @param FieldNode[] $fieldNodes
334
     * @param ResolveInfo $info
335
     * @param array       $path
336
     * @param             $result
337
     * @return array|\stdClass
338
     * @throws InvalidTypeException
339
     */
340
    private function collectAndExecuteSubFields(
341
        ObjectType $returnType,
342
        $fieldNodes,
343
        ResolveInfo $info,
0 ignored issues
show
Unused Code introduced by
The parameter $info is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

343
        /** @scrutinizer ignore-unused */ ResolveInfo $info,

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
344
        $path,
345
        &$result
346
    ) {
347
        $subFields = new \ArrayObject();
348
349
        foreach ($fieldNodes as $fieldNode) {
350
            if ($fieldNode->getSelectionSet() !== null) {
351
                $subFields = $this->collectFields(
352
                    $returnType,
353
                    $fieldNode->getSelectionSet(),
354
                    $subFields,
355
                    new \ArrayObject()
356
                );
357
            }
358
        }
359
360
        if ($subFields->count()) {
361
            return $this->executeFields($returnType, $result, $path, $subFields);
362
        }
363
364
        return $result;
365
    }
366
}
367