Passed
Pull Request — master (#351)
by Kirill
02:28
created

Schema::getDirective()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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