Schema   A
last analyzed

Complexity

Total Complexity 39

Size/Duplication

Total Lines 347
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 109
c 1
b 0
f 0
dl 0
loc 347
rs 9.28
wmc 39

15 Methods

Rating   Name   Duplication   Size   Complexity  
A getDirectives() 0 3 1
C typeMapReducer() 0 60 14
A getAssumeValid() 0 3 1
A getSubscriptionType() 0 3 1
A buildTypeMap() 0 24 2
A buildImplementations() 0 24 6
A getMutationType() 0 3 1
A getType() 0 3 1
A getTypeMap() 0 3 1
A getPossibleTypes() 0 7 2
A typeMapDirectiveReducer() 0 9 2
A getQueryType() 0 3 1
A __construct() 0 23 2
A isPossibleType() 0 28 3
A getDirective() 0 4 1
1
<?php
2
3
namespace Digia\GraphQL\Schema;
4
5
use Digia\GraphQL\Error\InvariantException;
6
use Digia\GraphQL\Language\Node\ASTNodeTrait;
7
use Digia\GraphQL\Language\Node\InterfaceTypeExtensionNode;
8
use Digia\GraphQL\Language\Node\ObjectTypeExtensionNode;
9
use Digia\GraphQL\Language\Node\SchemaDefinitionNode;
10
use Digia\GraphQL\Type\Definition\Argument;
11
use Digia\GraphQL\Type\Definition\Directive;
12
use Digia\GraphQL\Type\Definition\ExtensionASTNodesTrait;
13
use Digia\GraphQL\Type\Definition\InputObjectType;
14
use Digia\GraphQL\Type\Definition\InterfaceType;
15
use Digia\GraphQL\Type\Definition\NamedTypeInterface;
16
use Digia\GraphQL\Type\Definition\ObjectType;
17
use Digia\GraphQL\Type\Definition\TypeInterface;
18
use Digia\GraphQL\Type\Definition\UnionType;
19
use Digia\GraphQL\Type\Definition\WrappingTypeInterface;
20
use function Digia\GraphQL\Type\__Schema;
21
use function Digia\GraphQL\Util\find;
22
23
/**
24
 * Schema Definition
25
 *
26
 * A Schema is created by supplying the root types of each type of operation,
27
 * query and mutation (optional). A schema definition is then supplied to the
28
 * validator and executor.
29
 *
30
 * Example:
31
 *
32
 *     $MyAppSchema = GraphQLSchema([
33
 *       'query'    => $MyAppQueryRootType,
34
 *       'mutation' => $MyAppMutationRootType,
35
 *     ])
36
 *
37
 * Note: If an array of `directives` are provided to GraphQLSchema, that will be
38
 * the exact list of directives represented and allowed. If `directives` is not
39
 * provided then a default set of the specified directives (e.g. @include and
40
 * @skip) will be used. If you wish to provide *additional* directives to these
41
 * specified directives, you must explicitly declare them. Example:
42
 *
43
 *     $MyAppSchema = GraphQLSchema([
44
 *       ...
45
 *       'directives' => \array_merge(specifiedDirectives(), [$myCustomDirective]),
46
 *     ])
47
 */
48
class Schema implements DefinitionInterface
49
{
50
    use ExtensionASTNodesTrait;
51
    use ASTNodeTrait;
52
53
    /**
54
     * @var ObjectType|null
55
     */
56
    protected $queryType;
57
58
    /**
59
     * @var ObjectType|null
60
     */
61
    protected $mutationType;
62
63
    /**
64
     * @var ObjectType|null
65
     */
66
    protected $subscriptionType;
67
68
    /**
69
     * @var TypeInterface[]
70
     */
71
    protected $types = [];
72
73
    /**
74
     * @var array
75
     */
76
    protected $directives = [];
77
78
    /**
79
     * @var bool
80
     */
81
    protected $assumeValid = false;
82
83
    /**
84
     * @var TypeInterface[]
85
     */
86
    protected $typeMap = [];
87
88
    /**
89
     * @var array
90
     */
91
    protected $implementations = [];
92
93
    /**
94
     * @var NamedTypeInterface[]
95
     */
96
    protected $possibleTypesMap = [];
97
98
    /**
99
     * Schema constructor.
100
     *
101
     * @param ObjectType|null                                        $queryType
102
     * @param ObjectType|null                                        $mutationType
103
     * @param ObjectType|null                                        $subscriptionType
104
     * @param TypeInterface[]                                        $types
105
     * @param Directive[]                                            $directives
106
     * @param bool                                                   $assumeValid
107
     * @param SchemaDefinitionNode|null                              $astNode
108
     * @param ObjectTypeExtensionNode[]|InterfaceTypeExtensionNode[] $extensionASTNodes
109
     * @throws InvariantException
110
     */
111
    public function __construct(
112
        ?ObjectType $queryType,
113
        ?ObjectType $mutationType,
114
        ?ObjectType $subscriptionType,
115
        array $types,
116
        array $directives,
117
        bool $assumeValid,
118
        ?SchemaDefinitionNode $astNode,
119
        array $extensionASTNodes
120
    ) {
121
        $this->queryType         = $queryType;
122
        $this->mutationType      = $mutationType;
123
        $this->subscriptionType  = $subscriptionType;
124
        $this->types             = $types;
125
        $this->directives        = !empty($directives)
126
            ? $directives
127
            : specifiedDirectives();
128
        $this->assumeValid       = $assumeValid;
129
        $this->astNode           = $astNode;
130
        $this->extensionAstNodes = $extensionASTNodes;
131
132
        $this->buildTypeMap();
133
        $this->buildImplementations();
134
    }
135
136
    /**
137
     * @return ObjectType|null
138
     */
139
    public function getQueryType(): ?ObjectType
140
    {
141
        return $this->queryType;
142
    }
143
144
    /**
145
     * @return ObjectType|null
146
     */
147
    public function getMutationType(): ?ObjectType
148
    {
149
        return $this->mutationType;
150
    }
151
152
    /**
153
     * @return ObjectType|null
154
     */
155
    public function getSubscriptionType(): ?ObjectType
156
    {
157
        return $this->subscriptionType;
158
    }
159
160
    /**
161
     * @param string $name
162
     * @return Directive|null
163
     */
164
    public function getDirective(string $name): ?Directive
165
    {
166
        return find($this->directives, function (Directive $directive) use ($name) {
167
            return $directive->getName() === $name;
168
        });
169
    }
170
171
    /**
172
     * @return array
173
     */
174
    public function getDirectives(): array
175
    {
176
        return $this->directives;
177
    }
178
179
    /**
180
     * @return array
181
     */
182
    public function getTypeMap(): array
183
    {
184
        return $this->typeMap;
185
    }
186
187
    /**
188
     * @return bool
189
     */
190
    public function getAssumeValid(): bool
191
    {
192
        return $this->assumeValid;
193
    }
194
195
    /**
196
     * @param NamedTypeInterface $abstractType
197
     * @param NamedTypeInterface $possibleType
198
     * @return bool
199
     * @throws InvariantException
200
     */
201
    public function isPossibleType(NamedTypeInterface $abstractType, NamedTypeInterface $possibleType): bool
202
    {
203
        $abstractTypeName = $abstractType->getName();
204
        $possibleTypeName = $possibleType->getName();
205
206
        if (!isset($this->possibleTypesMap[$abstractTypeName])) {
207
            $possibleTypes = $this->getPossibleTypes($abstractType);
208
209
            if (!\is_array($possibleTypes)) {
210
                throw new InvariantException(\sprintf(
211
                    'Could not find possible implementing types for %s ' .
212
                    'in schema. Check that schema.types is defined and is an array of ' .
213
                    'all possible types in the schema.',
214
                    $abstractTypeName
215
                ));
216
            }
217
218
            $this->possibleTypesMap[$abstractTypeName] = \array_reduce(
219
                $possibleTypes,
220
                function (array $map, NamedTypeInterface $type) {
221
                    $map[$type->getName()] = true;
222
                    return $map;
223
                },
224
                []
225
            );
226
        }
227
228
        return isset($this->possibleTypesMap[$abstractTypeName][$possibleTypeName]);
229
    }
230
231
    /**
232
     * @param NamedTypeInterface $abstractType
233
     * @return NamedTypeInterface[]|null
234
     * @throws InvariantException
235
     */
236
    public function getPossibleTypes(NamedTypeInterface $abstractType): ?array
237
    {
238
        if ($abstractType instanceof UnionType) {
239
            return $abstractType->getTypes();
240
        }
241
242
        return $this->implementations[$abstractType->getName()] ?? null;
243
    }
244
245
    /**
246
     * @param string $name
247
     * @return TypeInterface|null
248
     */
249
    public function getType(string $name): ?TypeInterface
250
    {
251
        return $this->typeMap[$name] ?? null;
252
    }
253
254
    /**
255
     *
256
     */
257
    protected function buildTypeMap(): void
258
    {
259
        $initialTypes = [
260
            $this->queryType,
261
            $this->mutationType,
262
            $this->subscriptionType,
263
            __Schema(), // Introspection schema
264
        ];
265
266
        if (!empty($this->types)) {
267
            $initialTypes = \array_merge($initialTypes, $this->types);
268
        }
269
270
        // Keep track of all types referenced within the schema.
271
        $typeMap = [];
272
273
        // First by deeply visiting all initial types.
274
        $typeMap = \array_reduce($initialTypes, [$this, 'typeMapReducer'], $typeMap);
275
276
        // Then by deeply visiting all directive types.
277
        $typeMap = \array_reduce($this->directives, [$this, 'typeMapDirectiveReducer'], $typeMap);
278
279
        // Storing the resulting map for reference by the schema.
280
        $this->typeMap = $typeMap;
281
    }
282
283
    /**
284
     * @throws InvariantException
285
     */
286
    protected function buildImplementations(): void
287
    {
288
        $implementations = [];
289
290
        // Keep track of all implementations by interface name.
291
        foreach ($this->typeMap as $typeName => $type) {
292
            if ($type instanceof ObjectType) {
293
                foreach ($type->getInterfaces() as $interface) {
294
                    if (!($interface instanceof InterfaceType)) {
295
                        continue;
296
                    }
297
298
                    $interfaceName = $interface->getName();
299
300
                    if (!isset($implementations[$interfaceName])) {
301
                        $implementations[$interfaceName] = [];
302
                    }
303
304
                    $implementations[$interfaceName][] = $type;
305
                }
306
            }
307
        }
308
309
        $this->implementations = $implementations;
310
    }
311
312
    /**
313
     * @param array              $map
314
     * @param TypeInterface|null $type
315
     * @return array
316
     * @throws InvariantException
317
     */
318
    protected function typeMapReducer(array $map, ?TypeInterface $type): array
319
    {
320
        if (null === $type) {
321
            return $map;
322
        }
323
324
        if ($type instanceof WrappingTypeInterface) {
325
            return $this->typeMapReducer($map, $type->getOfType());
326
        }
327
328
        if ($type instanceof NamedTypeInterface) {
329
            $typeName = $type->getName();
330
331
            if (isset($map[$typeName])) {
332
                if ($type !== $map[$typeName]) {
333
                    throw new InvariantException(\sprintf(
334
                        'Schema must contain unique named types but contains multiple types named "%s".',
335
                        $type->getName()
336
                    ));
337
                }
338
339
                return $map;
340
            }
341
342
            $map[$typeName] = $type;
343
344
            $reducedMap = $map;
345
346
            if ($type instanceof UnionType) {
347
                $reducedMap = \array_reduce($type->getTypes(), [$this, 'typeMapReducer'], $reducedMap);
348
            }
349
350
            if ($type instanceof ObjectType) {
351
                $reducedMap = \array_reduce($type->getInterfaces(), [$this, 'typeMapReducer'], $reducedMap);
352
            }
353
354
            if ($type instanceof ObjectType || $type instanceof InterfaceType) {
355
                foreach ($type->getFields() as $field) {
356
                    if ($field->hasArguments()) {
357
                        $fieldArgTypes = \array_map(function (Argument $argument) {
358
                            return $argument->getType();
359
                        }, $field->getArguments());
360
361
                        $reducedMap = \array_reduce($fieldArgTypes, [$this, 'typeMapReducer'], $reducedMap);
362
                    }
363
364
                    $reducedMap = $this->typeMapReducer($reducedMap, $field->getType());
365
                }
366
            }
367
368
            if ($type instanceof InputObjectType) {
369
                foreach ($type->getFields() as $field) {
370
                    $reducedMap = $this->typeMapReducer($reducedMap, $field->getType());
371
                }
372
            }
373
374
            return $reducedMap;
375
        }
376
377
        return $map;
378
    }
379
380
    /**
381
     * @param array     $map
382
     * @param Directive $directive
383
     * @return array
384
     * @throws InvariantException
385
     */
386
    protected function typeMapDirectiveReducer(array $map, Directive $directive): array
387
    {
388
        if (!$directive->hasArguments()) {
389
            return $map;
390
        }
391
392
        return \array_reduce($directive->getArguments(), function ($map, Argument $argument) {
393
            return $this->typeMapReducer($map, $argument->getType());
394
        }, $map);
395
    }
396
}
397