Passed
Push — master ( f24e00...29eba8 )
by Vladimir
10:15
created

QueryPlan::arrayMergeDeep()   B

Complexity

Conditions 7
Paths 5

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 7.392

Importance

Changes 0
Metric Value
eloc 10
dl 0
loc 17
ccs 8
cts 10
cp 0.8
rs 8.8333
c 0
b 0
f 0
cc 7
nc 5
nop 2
crap 7.392
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_filter;
16
use function array_key_exists;
17
use function array_keys;
18
use function array_merge;
19
use function array_merge_recursive;
20
use function array_unique;
21
use function array_values;
22
use function count;
23
use function in_array;
24
use function is_array;
25
use function is_numeric;
26
27
class QueryPlan
28
{
29
    /** @var string[][] */
30
    private $types = [];
31
32
    /** @var Schema */
33
    private $schema;
34
35
    /** @var mixed[] */
36
    private $queryPlan = [];
37
38
    /** @var mixed[] */
39
    private $variableValues;
40
41
    /** @var FragmentDefinitionNode[] */
42
    private $fragments;
43
44
    /**
45
     * @param FieldNode[]              $fieldNodes
46
     * @param mixed[]                  $variableValues
47
     * @param FragmentDefinitionNode[] $fragments
48
     */
49 2
    public function __construct(ObjectType $parentType, Schema $schema, iterable $fieldNodes, array $variableValues, array $fragments)
50
    {
51 2
        $this->schema         = $schema;
52 2
        $this->variableValues = $variableValues;
53 2
        $this->fragments      = $fragments;
54 2
        $this->analyzeQueryPlan($parentType, $fieldNodes);
55 2
    }
56
57
    /**
58
     * @return mixed[]
59
     */
60 2
    public function queryPlan() : array
61
    {
62 2
        return $this->queryPlan;
63
    }
64
65
    /**
66
     * @return string[]
67
     */
68 2
    public function getReferencedTypes() : array
69
    {
70 2
        return array_keys($this->types);
71
    }
72
73 2
    public function hasType(string $type) : bool
74
    {
75
        return count(array_filter($this->getReferencedTypes(), static function (string $referencedType) use ($type) {
76 2
                return $type === $referencedType;
77 2
        })) > 0;
78
    }
79
80
    /**
81
     * @return string[]
82
     */
83 2
    public function getReferencedFields() : array
84
    {
85 2
        return array_values(array_unique(array_merge(...array_values($this->types))));
86
    }
87
88 2
    public function hasField(string $field) : bool
89
    {
90
        return count(array_filter($this->getReferencedFields(), static function (string $referencedField) use ($field) {
91 2
            return $field === $referencedField;
92 2
        })) > 0;
93
    }
94
95
    /**
96
     * @return string[]
97
     */
98 2
    public function subFields(string $typename) : array
99
    {
100 2
        if (! array_key_exists($typename, $this->types)) {
101
            return [];
102
        }
103
104 2
        return $this->types[$typename];
105
    }
106
107
    /**
108
     * @param FieldNode[] $fieldNodes
109
     */
110 2
    private function analyzeQueryPlan(ObjectType $parentType, iterable $fieldNodes) : void
111
    {
112 2
        $queryPlan = [];
113
        /** @var FieldNode $fieldNode */
114 2
        foreach ($fieldNodes as $fieldNode) {
115 2
            if (! $fieldNode->selectionSet) {
116
                continue;
117
            }
118
119 2
            $type = $parentType->getField($fieldNode->name->value)->getType();
120 2
            if ($type instanceof WrappingType) {
121
                $type = $type->getWrappedType();
122
            }
123
124 2
            $subfields = $this->analyzeSelectionSet($fieldNode->selectionSet, $type);
0 ignored issues
show
Bug introduced by
It seems like $type can also be of type GraphQL\Type\Definition\EnumType and GraphQL\Type\Definition\InputObjectType and GraphQL\Type\Definition\InterfaceType and GraphQL\Type\Definition\ScalarType and GraphQL\Type\Definition\UnionType; however, parameter $parentType of GraphQL\Type\Definition\...::analyzeSelectionSet() does only seem to accept GraphQL\Type\Definition\ObjectType, maybe add an additional type check? ( Ignorable by Annotation )

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

124
            $subfields = $this->analyzeSelectionSet($fieldNode->selectionSet, /** @scrutinizer ignore-type */ $type);
Loading history...
125
126 2
            $this->types[$type->name] = array_unique(array_merge(
127 2
                array_key_exists($type->name, $this->types) ? $this->types[$type->name] : [],
128 2
                array_keys($subfields)
129
            ));
130
131 2
            $queryPlan = array_merge_recursive(
132 2
                $queryPlan,
133 2
                $subfields
134
            );
135
        }
136
137 2
        $this->queryPlan = $queryPlan;
138 2
    }
139
140
    /**
141
     * @return mixed[]
142
     *
143
     * @throws Error
144
     */
145 2
    private function analyzeSelectionSet(SelectionSetNode $selectionSet, ObjectType $parentType) : array
146
    {
147 2
        $fields = [];
148 2
        foreach ($selectionSet->selections as $selectionNode) {
149 2
            if ($selectionNode instanceof FieldNode) {
150 2
                $fieldName     = $selectionNode->name->value;
151 2
                $type          = $parentType->getField($fieldName);
152 2
                $selectionType = $type->getType();
153
154 2
                $subfields = [];
155 2
                if ($selectionNode->selectionSet) {
156 2
                    $subfields = $this->analyzeSubFields($selectionType, $selectionNode->selectionSet);
157
                }
158
159 2
                $fields[$fieldName] = [
160 2
                    'type' => $selectionType,
161 2
                    'fields' => $subfields ?? [],
162 2
                    'args' => Values::getArgumentValues($type, $selectionNode, $this->variableValues),
163
                ];
164 2
            } elseif ($selectionNode instanceof FragmentSpreadNode) {
165 2
                $spreadName = $selectionNode->name->value;
166 2
                if (isset($this->fragments[$spreadName])) {
167 2
                    $fragment  = $this->fragments[$spreadName];
168 2
                    $type      = $this->schema->getType($fragment->typeCondition->name->value);
169 2
                    $subfields = $this->analyzeSubFields($type, $fragment->selectionSet);
170
171 2
                    $fields = $this->arrayMergeDeep(
172 2
                        $subfields,
173 2
                        $fields
174
                    );
175
                }
176 2
            } elseif ($selectionNode instanceof InlineFragmentNode) {
177 2
                $type      = $this->schema->getType($selectionNode->typeCondition->name->value);
178 2
                $subfields = $this->analyzeSubFields($type, $selectionNode->selectionSet);
179
180 2
                $fields = $this->arrayMergeDeep(
181 2
                    $subfields,
182 2
                    $fields
183
                );
184
            }
185
        }
186 2
        return $fields;
187
    }
188
189
    /**
190
     * @return mixed[]
191
     */
192 2
    private function analyzeSubFields(Type $type, SelectionSetNode $selectionSet) : array
193
    {
194 2
        if ($type instanceof WrappingType) {
195 2
            $type = $type->getWrappedType();
196
        }
197
198 2
        $subfields = [];
199 2
        if ($type instanceof ObjectType) {
200 2
            $subfields                = $this->analyzeSelectionSet($selectionSet, $type);
201 2
            $this->types[$type->name] = array_unique(array_merge(
202 2
                array_key_exists($type->name, $this->types) ? $this->types[$type->name] : [],
203 2
                array_keys($subfields)
204
            ));
205
        }
206
207 2
        return $subfields;
208
    }
209
210
    /**
211
     * similar to array_merge_recursive this merges nested arrays, but handles non array values differently
212
     * while array_merge_recursive tries to merge non array values, in this implementation they will be overwritten
213
     *
214
     * @see https://stackoverflow.com/a/25712428
215
     *
216
     * @param mixed[] $array1
217
     * @param mixed[] $array2
218
     *
219
     * @return mixed[]
220
     */
221 2
    private function arrayMergeDeep(array $array1, array $array2) : array
222
    {
223 2
        $merged = $array1;
224
225 2
        foreach ($array2 as $key => & $value) {
226 2
            if (is_numeric($key)) {
227
                if (! in_array($value, $merged, true)) {
228
                    $merged[] = $value;
229
                }
230 2
            } elseif (is_array($value) && isset($merged[$key]) && is_array($merged[$key])) {
231 1
                $merged[$key] = $this->arrayMergeDeep($merged[$key], $value);
232
            } else {
233 2
                $merged[$key] = $value;
234
            }
235
        }
236
237 2
        return $merged;
238
    }
239
}
240