Passed
Pull Request — master (#164)
by Christoffer
02:18
created

Schema::buildTypeMap()   B

Complexity

Conditions 2
Paths 2

Size

Total Lines 24
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 24
rs 8.9713
c 0
b 0
f 0
cc 2
eloc 11
nc 2
nop 0
1
<?php
2
3
namespace Digia\GraphQL\Schema;
4
5
use Digia\GraphQL\Config\ConfigAwareInterface;
6
use Digia\GraphQL\Config\ConfigAwareTrait;
7
use Digia\GraphQL\Error\InvariantException;
8
use Digia\GraphQL\Language\Node\NameAwareInterface;
9
use Digia\GraphQL\Language\Node\NodeTrait;
10
use Digia\GraphQL\Language\Node\SchemaDefinitionNode;
11
use function Digia\GraphQL\Type\__Schema;
12
use Digia\GraphQL\Type\Definition\AbstractTypeInterface;
13
use Digia\GraphQL\Type\Definition\Argument;
14
use Digia\GraphQL\Type\Definition\Directive;
15
use Digia\GraphQL\Type\Definition\DirectiveInterface;
16
use Digia\GraphQL\Type\Definition\InputObjectType;
17
use Digia\GraphQL\Type\Definition\InterfaceType;
18
use Digia\GraphQL\Type\Definition\ObjectType;
19
use Digia\GraphQL\Type\Definition\TypeInterface;
20
use Digia\GraphQL\Type\Definition\UnionType;
21
use Digia\GraphQL\Type\Definition\WrappingTypeInterface;
22
use function Digia\GraphQL\Util\find;
23
use function Digia\GraphQL\Util\invariant;
24
25
/**
26
 * Schema Definition
27
 *
28
 * A Schema is created by supplying the root types of each type of operation,
29
 * query and mutation (optional). A schema definition is then supplied to the
30
 * validator and executor.
31
 *
32
 * Example:
33
 *
34
 *     $MyAppSchema = GraphQLSchema([
35
 *       'query'    => $MyAppQueryRootType,
36
 *       'mutation' => $MyAppMutationRootType,
37
 *     ])
38
 *
39
 * Note: If an array of `directives` are provided to GraphQLSchema, that will be
40
 * the exact list of directives represented and allowed. If `directives` is not
41
 * provided then a default set of the specified directives (e.g. @include and
42
 * @skip) will be used. If you wish to provide *additional* directives to these
43
 * specified directives, you must explicitly declare them. Example:
44
 *
45
 *     $MyAppSchema = GraphQLSchema([
46
 *       ...
47
 *       'directives' => \array_merge(specifiedDirectives(), [$myCustomDirective]),
48
 *     ])
49
 */
50
51
/**
52
 * Class Schema
53
 *
54
 * @package Digia\GraphQL\Type
55
 * @property SchemaDefinitionNode $astNode
56
 */
57
class Schema implements SchemaInterface, ConfigAwareInterface
58
{
59
    use ConfigAwareTrait;
60
    use NodeTrait;
61
62
    /**
63
     * @var TypeInterface|null
64
     */
65
    protected $query;
66
67
    /**
68
     * @var TypeInterface|null
69
     */
70
    protected $mutation;
71
72
    /**
73
     * @var TypeInterface|null
74
     */
75
    protected $subscription;
76
77
    /**
78
     * @var TypeInterface
79
     */
80
    protected $types = [];
81
82
    /**
83
     * @var array
84
     */
85
    protected $directives = [];
86
87
    /**
88
     * @var bool
89
     */
90
    protected $assumeValid = false;
91
92
    /**
93
     * @var array
94
     */
95
    protected $typeMap = [];
96
97
    /**
98
     * @var array
99
     */
100
    protected $implementations = [];
101
102
    /**
103
     * @var array
104
     */
105
    protected $possibleTypesMap = [];
106
107
    /**
108
     * @inheritdoc
109
     */
110
    public function getQueryType(): ?TypeInterface
111
    {
112
        return $this->query;
113
    }
114
115
    /**
116
     * @inheritdoc
117
     */
118
    public function getMutationType(): ?TypeInterface
119
    {
120
        return $this->mutation;
121
    }
122
123
    /**
124
     * @inheritdoc
125
     */
126
    public function getSubscriptionType(): ?TypeInterface
127
    {
128
        return $this->subscription;
129
    }
130
131
    /**
132
     * @inheritdoc
133
     */
134
    public function getDirective(string $name): ?Directive
135
    {
136
        return find($this->directives, function (Directive $directive) use ($name) {
137
            return $directive->getName() === $name;
138
        });
139
    }
140
141
    /**
142
     * @inheritdoc
143
     */
144
    public function getDirectives(): array
145
    {
146
        return $this->directives;
147
    }
148
149
    /**
150
     * @inheritdoc
151
     */
152
    public function getTypeMap(): array
153
    {
154
        return $this->typeMap;
155
    }
156
157
    /**
158
     * @inheritdoc
159
     */
160
    public function getAssumeValid(): bool
161
    {
162
        return $this->assumeValid;
163
    }
164
165
    /**
166
     * @inheritdoc
167
     */
168
    public function isPossibleType(AbstractTypeInterface $abstractType, TypeInterface $possibleType): bool
169
    {
170
        /** @noinspection PhpUndefinedMethodInspection */
171
        $abstractTypeName = $abstractType->getName();
0 ignored issues
show
Bug introduced by
The method getName() does not exist on Digia\GraphQL\Type\Defin...n\AbstractTypeInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Digia\GraphQL\Type\Defin...n\AbstractTypeInterface. ( Ignorable by Annotation )

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

171
        /** @scrutinizer ignore-call */ 
172
        $abstractTypeName = $abstractType->getName();
Loading history...
172
        /** @noinspection PhpUndefinedMethodInspection */
173
        $possibleTypeName = $possibleType->getName();
0 ignored issues
show
Bug introduced by
The method getName() does not exist on Digia\GraphQL\Type\Definition\TypeInterface. It seems like you code against a sub-type of said class. However, the method does not exist in Digia\GraphQL\Type\Defin...n\AbstractTypeInterface or Digia\GraphQL\Type\Defin...n\WrappingTypeInterface. Are you sure you never get one of those? ( Ignorable by Annotation )

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

173
        /** @scrutinizer ignore-call */ 
174
        $possibleTypeName = $possibleType->getName();
Loading history...
174
175
        if (!isset($this->possibleTypesMap[$abstractTypeName])) {
176
            $possibleTypes = $this->getPossibleTypes($abstractType);
177
178
            invariant(
179
                \is_array($possibleTypes),
180
                \sprintf(
181
                    'Could not find possible implementing types for %s ' .
182
                    'in schema. Check that schema.types is defined and is an array of ' .
183
                    'all possible types in the schema.',
184
                    $abstractTypeName
185
                )
186
            );
187
188
            $this->possibleTypesMap[$abstractTypeName] = \array_reduce($possibleTypes,
189
                function (array $map, TypeInterface $type) {
190
                    /** @var NameAwareInterface $type */
191
                    $map[$type->getName()] = true;
192
                    return $map;
193
                }, []);
194
        }
195
196
        return isset($this->possibleTypesMap[$abstractTypeName][$possibleTypeName]);
197
    }
198
199
    /**
200
     * @inheritdoc
201
     */
202
    public function getPossibleTypes(AbstractTypeInterface $abstractType): ?array
203
    {
204
        if ($abstractType instanceof UnionType) {
205
            return $abstractType->getTypes();
206
        }
207
208
        return $this->implementations[$abstractType->getName()] ?? null;
209
    }
210
211
    /**
212
     * @inheritdoc
213
     */
214
    public function getType(string $name): ?TypeInterface
215
    {
216
        return $this->typeMap[$name] ?? null;
217
    }
218
219
    /**
220
     * @inheritdoc
221
     */
222
    protected function beforeConfig(): void
223
    {
224
        $this->setDirectives(specifiedDirectives());
225
    }
226
227
    /**
228
     * @throws InvariantException
229
     */
230
    protected function afterConfig(): void
231
    {
232
        $this->buildTypeMap();
233
        $this->buildImplementations();
234
    }
235
236
    /**
237
     *
238
     */
239
    protected function buildTypeMap(): void
240
    {
241
        $initialTypes = [
242
            $this->query,
243
            $this->mutation,
244
            $this->subscription,
245
            __Schema(), // Introspection
246
        ];
247
248
        if ($this->types) {
249
            $initialTypes = \array_merge($initialTypes, $this->types);
0 ignored issues
show
Bug introduced by
$this->types of type Digia\GraphQL\Type\Definition\TypeInterface is incompatible with the type null|array expected by parameter $array2 of array_merge(). ( Ignorable by Annotation )

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

249
            $initialTypes = \array_merge($initialTypes, /** @scrutinizer ignore-type */ $this->types);
Loading history...
250
        }
251
252
        // Keep track of all types referenced within the schema.
253
        $typeMap = [];
254
255
        // First by deeply visiting all initial types.
256
        $typeMap = \array_reduce($initialTypes, [$this, 'typeMapReducer'], $typeMap);
257
258
        // Then by deeply visiting all directive types.
259
        $typeMap = \array_reduce($this->directives, [$this, 'typeMapDirectiveReducer'], $typeMap);
260
261
        // Storing the resulting map for reference by the schema.
262
        $this->typeMap = $typeMap;
263
    }
264
265
    /**
266
     * @throws InvariantException
267
     */
268
    protected function buildImplementations()
269
    {
270
        $implementations = [];
271
272
        // Keep track of all implementations by interface name.
273
        foreach ($this->typeMap as $typeName => $type) {
274
            if ($type instanceof ObjectType) {
275
                foreach ($type->getInterfaces() as $interface) {
276
                    if (!($interface instanceof InterfaceType)) {
277
                        continue;
278
                    }
279
280
                    $interfaceName = $interface->getName();
281
282
                    if (!isset($implementations[$interfaceName])) {
283
                        $implementations[$interfaceName] = [];
284
                    }
285
286
                    $implementations[$interfaceName][] = $type;
287
                }
288
            }
289
        }
290
291
        $this->implementations = $implementations;
292
    }
293
294
    /**
295
     * @param TypeInterface|null $query
296
     * @return Schema
297
     */
298
    protected function setQuery(?TypeInterface $query): Schema
299
    {
300
        $this->query = $query;
301
        return $this;
302
    }
303
304
    /**
305
     * @param TypeInterface|null $mutation
306
     * @return Schema
307
     */
308
    protected function setMutation(?TypeInterface $mutation): Schema
309
    {
310
        $this->mutation = $mutation;
311
        return $this;
312
    }
313
314
    /**
315
     * @param TypeInterface|null $subscription
316
     * @return Schema
317
     */
318
    protected function setSubscription(?TypeInterface $subscription): Schema
319
    {
320
        $this->subscription = $subscription;
321
        return $this;
322
    }
323
324
    /**
325
     * @param array $types
326
     * @return Schema
327
     */
328
    protected function setTypes(array $types): Schema
329
    {
330
        $this->types = $types;
0 ignored issues
show
Documentation Bug introduced by
It seems like $types of type array is incompatible with the declared type Digia\GraphQL\Type\Definition\TypeInterface of property $types.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
331
        return $this;
332
    }
333
334
    /**
335
     * @param DirectiveInterface[] $directives
336
     * @return Schema
337
     */
338
    protected function setDirectives(array $directives): Schema
339
    {
340
        $this->directives = $directives;
341
        return $this;
342
    }
343
344
    /**
345
     * @param bool $assumeValid
346
     * @return Schema
347
     */
348
    protected function setAssumeValid(bool $assumeValid): Schema
349
    {
350
        $this->assumeValid = $assumeValid;
351
        return $this;
352
    }
353
354
    /**
355
     * @param array              $map
356
     * @param TypeInterface|NameAwareInterface|null $type
357
     * @return array
358
     * @throws InvariantException
359
     */
360
    protected function typeMapReducer(array $map, ?TypeInterface $type): array
361
    {
362
        if (null === $type) {
363
            return $map;
364
        }
365
366
        if ($type instanceof WrappingTypeInterface) {
367
            return $this->typeMapReducer($map, $type->getOfType());
368
        }
369
370
        $typeName = $type->getName();
371
372
        if (isset($map[$typeName])) {
373
            invariant(
374
                $type === $map[$typeName],
375
                \sprintf(
376
                    'Schema must contain unique named types but contains multiple types named "%s".',
377
                    $type->getName()
378
                )
379
            );
380
381
            return $map;
382
        }
383
384
        $map[$typeName] = $type;
385
386
        $reducedMap = $map;
387
388
        if ($type instanceof UnionType) {
389
            $reducedMap = \array_reduce($type->getTypes(), [$this, 'typeMapReducer'], $reducedMap);
390
        }
391
392
        if ($type instanceof ObjectType) {
393
            $reducedMap = \array_reduce($type->getInterfaces(), [$this, 'typeMapReducer'], $reducedMap);
394
        }
395
396
        if ($type instanceof ObjectType || $type instanceof InterfaceType) {
397
            foreach ($type->getFields() as $field) {
398
                if ($field->hasArguments()) {
399
                    $fieldArgTypes = \array_map(function (Argument $argument) {
400
                        return $argument->getType();
401
                    }, $field->getArguments());
402
403
                    $reducedMap = \array_reduce($fieldArgTypes, [$this, 'typeMapReducer'], $reducedMap);
404
                }
405
406
                $reducedMap = $this->typeMapReducer($reducedMap, $field->getType());
407
            }
408
        }
409
410
        if ($type instanceof InputObjectType) {
411
            foreach ($type->getFields() as $field) {
412
                $reducedMap = $this->typeMapReducer($reducedMap, $field->getType());
413
            }
414
        }
415
416
        return $reducedMap;
417
    }
418
419
    /**
420
     * Note: We do not type-hint the `$directive`, because we want the `SchemaValidator` to catch these errors.
421
     *
422
     * @param array      $map
423
     * @param mixed|null $directive
424
     * @return array
425
     */
426
    protected function typeMapDirectiveReducer(array $map, $directive): array
427
    {
428
        if (!($directive instanceof Directive) ||
429
            ($directive instanceof Directive && !$directive->hasArguments())) {
430
            return $map;
431
        }
432
433
        return \array_reduce($directive->getArguments(), function ($map, Argument $argument) {
434
            return $this->typeMapReducer($map, $argument->getType());
435
        }, $map);
436
    }
437
438
}
439