Passed
Push — master ( c11f95...986482 )
by Vladimir
09:44
created

QueryPlan::analyzeQueryPlan()   B

Complexity

Conditions 7
Paths 12

Size

Total Lines 36
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 7.0052

Importance

Changes 1
Bugs 0 Features 1
Metric Value
eloc 21
c 1
b 0
f 1
dl 0
loc 36
rs 8.6506
ccs 20
cts 21
cp 0.9524
cc 7
nc 12
nop 2
crap 7.0052
1
<?php
2
3
declare(strict_types=1);
4
5
namespace GraphQL\Type\Definition;
6
7
use GraphQL\Error\Error;
8
use GraphQL\Executor\Values;
9
use GraphQL\Language\AST\FieldNode;
10
use GraphQL\Language\AST\FragmentDefinitionNode;
11
use GraphQL\Language\AST\FragmentSpreadNode;
12
use GraphQL\Language\AST\InlineFragmentNode;
13
use GraphQL\Language\AST\SelectionSetNode;
14
use GraphQL\Type\Schema;
15
use function array_diff_key;
16
use function array_filter;
17
use function array_intersect_key;
18
use function array_key_exists;
19
use function array_keys;
20
use function array_merge;
21
use function array_merge_recursive;
22
use function array_unique;
23
use function array_values;
24
use function count;
25
use function in_array;
26
use function is_array;
27
use function is_numeric;
28
29
class QueryPlan
30
{
31
    /** @var string[][] */
32
    private $types = [];
33
34
    /** @var Schema */
35
    private $schema;
36
37
    /** @var mixed[] */
38
    private $queryPlan = [];
39
40
    /** @var mixed[] */
41
    private $variableValues;
42
43
    /** @var FragmentDefinitionNode[] */
44
    private $fragments;
45
46
    /** @var bool */
47
    private $groupImplementorFields;
48
49
    /**
50
     * @param FieldNode[]              $fieldNodes
51
     * @param mixed[]                  $variableValues
52
     * @param FragmentDefinitionNode[] $fragments
53
     * @param mixed[]                  $options
54
     */
55 5
    public function __construct(ObjectType $parentType, Schema $schema, iterable $fieldNodes, array $variableValues, array $fragments, array $options = [])
56
    {
57 5
        $this->schema                 = $schema;
58 5
        $this->variableValues         = $variableValues;
59 5
        $this->fragments              = $fragments;
60 5
        $this->groupImplementorFields = in_array('group-implementor-fields', $options, true);
61 5
        $this->analyzeQueryPlan($parentType, $fieldNodes);
62 5
    }
63
64
    /**
65
     * @return mixed[]
66
     */
67 5
    public function queryPlan() : array
68
    {
69 5
        return $this->queryPlan;
70
    }
71
72
    /**
73
     * @return string[]
74
     */
75 5
    public function getReferencedTypes() : array
76
    {
77 5
        return array_keys($this->types);
78
    }
79
80 3
    public function hasType(string $type) : bool
81
    {
82
        return count(array_filter($this->getReferencedTypes(), static function (string $referencedType) use ($type) {
83 3
                return $type === $referencedType;
84 3
        })) > 0;
85
    }
86
87
    /**
88
     * @return string[]
89
     */
90 5
    public function getReferencedFields() : array
91
    {
92 5
        return array_values(array_unique(array_merge(...array_values($this->types))));
93
    }
94
95 3
    public function hasField(string $field) : bool
96
    {
97
        return count(array_filter($this->getReferencedFields(), static function (string $referencedField) use ($field) {
98 3
            return $field === $referencedField;
99 3
        })) > 0;
100
    }
101
102
    /**
103
     * @return string[]
104
     */
105 5
    public function subFields(string $typename) : array
106
    {
107 5
        if (! array_key_exists($typename, $this->types)) {
108
            return [];
109
        }
110
111 5
        return $this->types[$typename];
112
    }
113
114
    /**
115
     * @param FieldNode[] $fieldNodes
116
     */
117 5
    private function analyzeQueryPlan(ObjectType $parentType, iterable $fieldNodes) : void
118
    {
119 5
        $queryPlan    = [];
120 5
        $implementors = [];
121
        /** @var FieldNode $fieldNode */
122 5
        foreach ($fieldNodes as $fieldNode) {
123 5
            if (! $fieldNode->selectionSet) {
124
                continue;
125
            }
126
127 5
            $type = $parentType->getField($fieldNode->name->value)->getType();
128 5
            if ($type instanceof WrappingType) {
129 1
                $type = $type->getWrappedType();
130
            }
131
132 5
            $subfields = $this->analyzeSelectionSet($fieldNode->selectionSet, $type, $implementors);
133
134 5
            $this->types[$type->name] = array_unique(array_merge(
135 5
                array_key_exists($type->name, $this->types) ? $this->types[$type->name] : [],
136 5
                array_keys($subfields)
137
            ));
138
139 5
            $queryPlan = array_merge_recursive(
140 5
                $queryPlan,
141 5
                $subfields
142
            );
143
        }
144
145 5
        if ($this->groupImplementorFields) {
146 2
            $this->queryPlan = ['fields' => $queryPlan];
147
148 2
            if ($implementors) {
149 2
                $this->queryPlan['implementors'] = $implementors;
150
            }
151
        } else {
152 3
            $this->queryPlan = $queryPlan;
153
        }
154 5
    }
155
156
    /**
157
     * @param InterfaceType|ObjectType $parentType
158
     * @param mixed[]                  $implementors
159
     *
160
     * @return mixed[]
161
     *
162
     * @throws Error
163
     */
164 5
    private function analyzeSelectionSet(SelectionSetNode $selectionSet, Type $parentType, array &$implementors = []) : array
165
    {
166 5
        $fields = [];
167 5
        foreach ($selectionSet->selections as $selectionNode) {
168 5
            if ($selectionNode instanceof FieldNode) {
169 5
                $fieldName     = $selectionNode->name->value;
170 5
                $type          = $parentType->getField($fieldName);
0 ignored issues
show
Bug introduced by
The method getField() does not exist on GraphQL\Type\Definition\Type. It seems like you code against a sub-type of GraphQL\Type\Definition\Type such as GraphQL\Type\Definition\InterfaceType or GraphQL\Type\Definition\ObjectType or GraphQL\Type\Definition\InputObjectType. ( Ignorable by Annotation )

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

170
                /** @scrutinizer ignore-call */ 
171
                $type          = $parentType->getField($fieldName);
Loading history...
171 5
                $selectionType = $type->getType();
172
173 5
                $subfields = [];
174 5
                if ($selectionNode->selectionSet) {
175 2
                    $subfields = $this->analyzeSubFields($selectionType, $selectionNode->selectionSet);
176
                }
177
178 5
                $fields[$fieldName] = [
179 5
                    'type' => $selectionType,
180 5
                    'fields' => $subfields ?? [],
181 5
                    'args' => Values::getArgumentValues($type, $selectionNode, $this->variableValues),
182
                ];
183 5
            } elseif ($selectionNode instanceof FragmentSpreadNode) {
184 4
                $spreadName = $selectionNode->name->value;
185 4
                if (isset($this->fragments[$spreadName])) {
186 4
                    $fragment  = $this->fragments[$spreadName];
187 4
                    $type      = $this->schema->getType($fragment->typeCondition->name->value);
188 4
                    $subfields = $this->analyzeSubFields($type, $fragment->selectionSet);
189 4
                    $fields    = $this->mergeFields($parentType, $type, $fields, $subfields, $implementors);
190
                }
191 5
            } elseif ($selectionNode instanceof InlineFragmentNode) {
192 5
                $type      = $this->schema->getType($selectionNode->typeCondition->name->value);
193 5
                $subfields = $this->analyzeSubFields($type, $selectionNode->selectionSet);
194 5
                $fields    = $this->mergeFields($parentType, $type, $fields, $subfields, $implementors);
195
            }
196
        }
197
198 5
        return $fields;
199
    }
200
201
    /**
202
     * @return mixed[]
203
     */
204 5
    private function analyzeSubFields(Type $type, SelectionSetNode $selectionSet) : array
205
    {
206 5
        if ($type instanceof WrappingType) {
207 2
            $type = $type->getWrappedType();
208
        }
209
210 5
        $subfields = [];
211 5
        if ($type instanceof ObjectType) {
212 5
            $subfields                = $this->analyzeSelectionSet($selectionSet, $type);
213 5
            $this->types[$type->name] = array_unique(array_merge(
214 5
                array_key_exists($type->name, $this->types) ? $this->types[$type->name] : [],
215 5
                array_keys($subfields)
216
            ));
217
        }
218
219 5
        return $subfields;
220
    }
221
222
    /**
223
     * @param mixed[] $fields
224
     * @param mixed[] $subfields
225
     * @param mixed[] $implementors
226
     *
227
     * @return mixed[]
228
     */
229 5
    private function mergeFields(Type $parentType, Type $type, array $fields, array $subfields, array &$implementors) : array
230
    {
231 5
        if ($this->groupImplementorFields && $parentType instanceof AbstractType && ! $type instanceof AbstractType) {
232 2
            $implementors[$type->name] = [
233 2
                'type'   => $type,
234 2
                'fields' => $this->arrayMergeDeep(
235 2
                    $implementors[$type->name]['fields'] ?? [],
236 2
                    array_diff_key($subfields, $fields)
237
                ),
238
            ];
239
240 2
            $fields = $this->arrayMergeDeep(
241 2
                $fields,
242 2
                array_intersect_key($subfields, $fields)
243
            );
244
        } else {
245 3
            $fields = $this->arrayMergeDeep(
246 3
                $subfields,
247 3
                $fields
248
            );
249
        }
250
251 5
        return $fields;
252
    }
253
254
    /**
255
     * similar to array_merge_recursive this merges nested arrays, but handles non array values differently
256
     * while array_merge_recursive tries to merge non array values, in this implementation they will be overwritten
257
     *
258
     * @see https://stackoverflow.com/a/25712428
259
     *
260
     * @param mixed[] $array1
261
     * @param mixed[] $array2
262
     *
263
     * @return mixed[]
264
     */
265 5
    private function arrayMergeDeep(array $array1, array $array2) : array
266
    {
267 5
        $merged = $array1;
268
269 5
        foreach ($array2 as $key => & $value) {
270 5
            if (is_numeric($key)) {
271
                if (! in_array($value, $merged, true)) {
272
                    $merged[] = $value;
273
                }
274 5
            } elseif (is_array($value) && isset($merged[$key]) && is_array($merged[$key])) {
275 1
                $merged[$key] = $this->arrayMergeDeep($merged[$key], $value);
276
            } else {
277 5
                $merged[$key] = $value;
278
            }
279
        }
280
281 5
        return $merged;
282
    }
283
}
284