Passed
Push — master ( bda88f...2ab509 )
by Quang
06:38
created

ExecutionStrategy::getFieldDefinition()   B

Complexity

Conditions 7
Paths 5

Size

Total Lines 17
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 8
nc 5
nop 3
dl 0
loc 17
rs 8.2222
c 0
b 0
f 0
1
<?php
2
3
namespace Digia\GraphQL\Execution;
4
5
use Digia\GraphQL\Error\GraphQLError;
6
use Digia\GraphQL\Execution\Resolver\ResolveInfo;
7
use Digia\GraphQL\Language\AST\Node\FieldNode;
8
use Digia\GraphQL\Language\AST\Node\FragmentDefinitionNode;
9
use Digia\GraphQL\Language\AST\Node\OperationDefinitionNode;
10
use Digia\GraphQL\Language\AST\Node\SelectionSetNode;
11
use Digia\GraphQL\Language\AST\NodeKindEnum;
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 array
43
     */
44
    protected $finalResult;
45
46
    /**
47
     * AbstractStrategy constructor.
48
     * @param ExecutionContext        $context
49
     *
50
     * @param OperationDefinitionNode $operation
51
     */
52
    public function __construct(
53
        ExecutionContext $context,
54
        OperationDefinitionNode $operation,
55
        $rootValue
56
    ) {
57
        $this->context   = $context;
58
        $this->operation = $operation;
59
        $this->rootValue = $rootValue;
60
    }
61
62
    /**
63
     * @return array|null
64
     */
65
    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...
66
67
    /**
68
     * @param ObjectType       $runtimeType
69
     * @param SelectionSetNode $selectionSet
70
     * @param                  $fields
71
     * @param                  $visitedFragmentNames
72
     * @return \ArrayObject
73
     * @throws \Exception
74
     */
75
    protected function collectFields(
76
        ObjectType $runtimeType,
77
        SelectionSetNode $selectionSet,
78
        $fields,
79
        $visitedFragmentNames
80
    ) {
81
        foreach ($selectionSet->getSelections() as $selection) {
82
            /** @var FieldNode $selection */
83
            switch ($selection->getKind()) {
84
                case NodeKindEnum::FIELD:
85
                    $name = $this->getFieldNameKey($selection);
86
                    if (!isset($runtimeType->getFields()[$selection->getNameValue()])) {
87
                        continue 2;
88
                    }
89
                    if (!isset($fields[$name])) {
90
                        $fields[$name] = new \ArrayObject();
91
                    }
92
                    $fields[$name][] = $selection;
93
                    break;
94
                case NodeKindEnum::INLINE_FRAGMENT:
95
                    //TODO check if should include this node
96
                    $this->collectFields(
97
                        $runtimeType,
98
                        $selection->getSelectionSet(),
99
                        $fields,
100
                        $visitedFragmentNames
101
                    );
102
                    break;
103
                case NodeKindEnum::FRAGMENT_SPREAD:
104
                    //TODO check if should include this node
105
                    if (!empty($visitedFragmentNames[$selection->getNameValue()])) {
106
                        continue 2;
107
                    }
108
                    $visitedFragmentNames[$selection->getNameValue()] = true;
109
                    /** @var FragmentDefinitionNode $fragment */
110
                    $fragment = $this->context->getFragments()[$selection->getNameValue()];
111
                    $this->collectFields(
112
                        $runtimeType,
113
                        $fragment->getSelectionSet(),
114
                        $fields,
115
                        $visitedFragmentNames
116
                    );
117
                    break;
118
            }
119
        }
120
121
        return $fields;
122
    }
123
124
    /**
125
     * @TODO: consider to move this to FieldNode
126
     * @param FieldNode $node
127
     * @return string
128
     */
129
    private function getFieldNameKey(FieldNode $node)
130
    {
131
        return $node->getAlias() ? $node->getAlias()->getValue() : $node->getNameValue();
132
    }
133
134
    /**
135
     * Implements the "Evaluating selection sets" section of the spec
136
     * for "read" mode.
137
     * @param ObjectType $parentType
138
     * @param            $source
139
     * @param            $path
140
     * @param            $fields
141
     *
142
     * @return array
143
     *
144
     * @throws GraphQLError|\Exception
145
     * @throws \TypeError
146
     */
147
    protected function executeFields(
148
        ObjectType $parentType,
149
        $source,
150
        $path,
151
        $fields
152
    ): array {
153
        $finalResults = [];
154
155
        foreach ($fields as $fieldName => $fieldNodes) {
156
            $fieldPath   = $path;
157
            $fieldPath[] = $fieldName;
158
159
            $result = $this->resolveField($parentType,
160
                $source,
161
                $fieldNodes,
162
                $fieldPath
163
            );
164
165
            $finalResults[$fieldName] = $result;
166
        }
167
168
        return $finalResults;
169
    }
170
171
    /**
172
     * @param Schema     $schema
173
     * @param ObjectType $parentType
174
     * @param string     $fieldName
175
     * @return \Digia\GraphQL\Type\Definition\Field|null
176
     * @throws \Exception
177
     * @throws \TypeError
178
     */
179
    public function getFieldDefinition(Schema $schema, ObjectType $parentType, string $fieldName)
180
    {
181
        if ($fieldName === SchemaMetaFieldDefinition()->getName() && $schema->getQuery() === $parentType) {
182
            return SchemaMetaFieldDefinition();
183
        }
184
185
        if ($fieldName === TypeMetaFieldDefinition()->getName() && $schema->getQuery() === $parentType) {
186
            return TypeNameMetaFieldDefinition();
187
        }
188
189
        if ($fieldName === TypeNameMetaFieldDefinition()->getName()) {
190
            return TypeNameMetaFieldDefinition();
191
        }
192
193
        $fields = $parentType->getFields();
194
195
        return isset($fields[$fieldName]) ? $fields[$fieldName] : null;
196
    }
197
198
199
    /**
200
     * @param ObjectType $parentType
201
     * @param            $source
202
     * @param            $fieldNodes
203
     * @param            $path
204
     *
205
     * @return mixed
206
     *
207
     * @throws GraphQLError|\Exception
208
     * @throws \TypeError
209
     */
210
    protected function resolveField(
211
        ObjectType $parentType,
212
        $source,
213
        $fieldNodes,
214
        $path
215
    ) {
216
        /** @var FieldNode $fieldNode */
217
        $fieldNode = $fieldNodes[0];
218
219
        $field = $this->getFieldDefinition($this->context->getSchema(), $parentType, $fieldNode->getNameValue());
220
221
        if (!$field) {
222
            return null;
223
        }
224
225
        $info = $this->buildResolveInfo($fieldNodes, $fieldNode, $field, $parentType, $path, $this->context);
226
227
        $resolveFunction = $this->determineResolveFunction($field, $parentType, $this->context);
228
229
        $result = $this->resolveOrError(
230
            $field,
231
            $fieldNode,
232
            $resolveFunction,
233
            $source,
234
            $this->context,
235
            $info
236
        );
237
238
        $result = $this->collectAndExecuteSubFields(
239
            $parentType,
240
            $fieldNodes,
241
            $info,
242
            $path,
243
            $result// $result is passed as $source
244
        );
245
246
        return $result;
247
    }
248
249
    /**
250
     * @param array            $fieldNodes
251
     * @param FieldNode        $fieldNode
252
     * @param Field            $field
253
     * @param ObjectType       $parentType
254
     * @param                  $path
255
     * @param ExecutionContext $context
256
     * @return ResolveInfo
257
     */
258
    private function buildResolveInfo(\ArrayAccess $fieldNodes, FieldNode $fieldNode, Field $field, ObjectType $parentType, $path, ExecutionContext $context)
259
    {
260
        return new ResolveInfo([
261
            'fieldName'      => $fieldNode->getNameValue(),
262
            'fieldNodes'     => $fieldNodes,
263
            'returnType'     => $field->getType(),
264
            'parentType'     => $parentType,
265
            'path'           => $path,
266
            'schema'         => $context->getSchema(),
267
            'fragments'      => $context->getFragments(),
268
            'rootValue'      => $context->getRootValue(),
269
            'operation'      => $context->getOperation(),
270
            'variableValues' => $context->getVariableValues(),
271
        ]);
272
    }
273
274
    /**
275
     * @param Field            $field
276
     * @param ObjectType       $parentType
277
     * @param ExecutionContext $context
278
     * @return callable|mixed|null
279
     */
280
    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

280
    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...
281
    {
282
        if ($field->hasResolve()) {
283
            return $field->getResolve();
284
        }
285
286
        if ($parentType->hasResolve()) {
287
            return $parentType->getResolve();
288
        }
289
290
        return $this->context->getFieldResolver();
291
    }
292
293
294
    /**
295
     * @param Field            $field
296
     * @param FieldNode        $fieldNode
297
     * @param callable         $resolveFunction
298
     * @param                  $source
299
     * @param ExecutionContext $context
300
     * @param ResolveInfo      $info
301
     * @return array|\Exception|\Throwable
302
     */
303
    private function resolveOrError(
304
        Field $field,
305
        FieldNode $fieldNode,
306
        callable $resolveFunction,
307
        $source,
308
        ExecutionContext $context,
309
        ResolveInfo $info
310
    ) {
311
        try {
312
            $args = getArgumentValues($field, $fieldNode, $context->getVariableValues());
313
314
            return $resolveFunction($source, $args, $context, $info);
315
        } catch (\Exception $error) {
316
            return $error;
317
        } catch (\Throwable $error) {
318
            return $error;
319
        }
320
    }
321
322
    /**
323
     * @param ObjectType  $returnType
324
     * @param FieldNode[] $fieldNodes
325
     * @param ResolveInfo $info
326
     * @param array       $path
327
     * @return array|\stdClass
328
     * @throws GraphQLError
329
     * @throws \Exception
330
     * @throws \TypeError
331
     */
332
    private function collectAndExecuteSubFields(
333
        ObjectType $returnType,
334
        $fieldNodes,
335
        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

335
        /** @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...
336
        $path,
337
        &$result
338
    )
339
    {
340
        $subFields = new \ArrayObject();
341
342
        foreach ($fieldNodes as $fieldNode) {
343
            if ($fieldNode->getSelectionSet() !== null) {
344
                $subFields = $this->collectFields(
345
                    $returnType,
346
                    $fieldNode->getSelectionSet(),
347
                    $subFields,
348
                    new \ArrayObject()
349
                );
350
            }
351
        }
352
353
        if($subFields->count()) {
354
            return $this->executeFields($returnType, $result, $path, $subFields);
355
        }
356
357
        return $result;
358
    }
359
}
360