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

Schema::__toString()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
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 NamedTypeInterface[]
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[]
241
     * @throws InvariantException
242
     */
243
    public function getPossibleTypes(AbstractTypeContract $abstractType): iterable
244
    {
245
        assert($abstractType instanceof NamedTypeInterface);
246
247
        if ($abstractType instanceof UnionType) {
248
            return $abstractType->getTypes();
249
        }
250
251
        return $this->implementations[$abstractType->getName()] ?? [];
252
    }
253
254
    /**
255
     * @param string $name
256
     * @return NamedTypeInterface|null
257
     */
258
    public function getType(string $name): ?NamedTypeInterface
259
    {
260
        return $this->typeMap[$name] ?? null;
261
    }
262
263
    /**
264
     *
265
     */
266
    protected function buildTypeMap(): void
267
    {
268
        $initialTypes = [
269
            $this->queryType,
270
            $this->mutationType,
271
            $this->subscriptionType,
272
            __Schema(), // Introspection schema
273
        ];
274
275
        if (!empty($this->types)) {
276
            $initialTypes = \array_merge($initialTypes, $this->types);
277
        }
278
279
        // Keep track of all types referenced within the schema.
280
        $typeMap = [];
281
282
        // First by deeply visiting all initial types.
283
        $typeMap = \array_reduce($initialTypes, [$this, 'typeMapReducer'], $typeMap);
284
285
        // Then by deeply visiting all directive types.
286
        $typeMap = \array_reduce($this->directives, [$this, 'typeMapDirectiveReducer'], $typeMap);
287
288
        // Storing the resulting map for reference by the schema.
289
        $this->typeMap = $typeMap;
290
    }
291
292
    /**
293
     * @throws InvariantException
294
     */
295
    protected function buildImplementations(): void
296
    {
297
        $implementations = [];
298
299
        // Keep track of all implementations by interface name.
300
        foreach ($this->typeMap as $typeName => $type) {
301
            if ($type instanceof ObjectType) {
302
                foreach ($type->getInterfaces() as $interface) {
303
                    if (!($interface instanceof InterfaceType)) {
304
                        continue;
305
                    }
306
307
                    $interfaceName = $interface->getName();
308
309
                    if (!isset($implementations[$interfaceName])) {
310
                        $implementations[$interfaceName] = [];
311
                    }
312
313
                    $implementations[$interfaceName][] = $type;
314
                }
315
            }
316
        }
317
318
        $this->implementations = $implementations;
319
    }
320
321
    /**
322
     * @param array              $map
323
     * @param TypeInterface|null $type
324
     * @return array
325
     * @throws InvariantException
326
     */
327
    protected function typeMapReducer(array $map, ?TypeInterface $type): array
328
    {
329
        if (null === $type) {
330
            return $map;
331
        }
332
333
        if ($type instanceof WrappingTypeInterface) {
334
            return $this->typeMapReducer($map, $type->getOfType());
335
        }
336
337
        if ($type instanceof NamedTypeInterface) {
338
            $typeName = $type->getName();
339
340
            if (isset($map[$typeName])) {
341
                if ($type !== $map[$typeName]) {
342
                    throw new InvariantException(\sprintf(
343
                        'Schema must contain unique named types but contains multiple types named "%s".',
344
                        $type->getName()
345
                    ));
346
                }
347
348
                return $map;
349
            }
350
351
            $map[$typeName] = $type;
352
353
            $reducedMap = $map;
354
355
            if ($type instanceof UnionType) {
356
                $reducedMap = \array_reduce($type->getTypes(), [$this, 'typeMapReducer'], $reducedMap);
357
            }
358
359
            if ($type instanceof ObjectType) {
360
                $reducedMap = \array_reduce($type->getInterfaces(), [$this, 'typeMapReducer'], $reducedMap);
361
            }
362
363
            if ($type instanceof ObjectType || $type instanceof InterfaceType) {
364
                foreach ($type->getFields() as $field) {
365
                    if ($field->hasArguments()) {
366
                        $fieldArgTypes = \array_map(function (Argument $argument) {
367
                            return $argument->getNullableType();
368
                        }, $field->getArguments());
369
370
                        $reducedMap = \array_reduce($fieldArgTypes, [$this, 'typeMapReducer'], $reducedMap);
371
                    }
372
373
                    $reducedMap = $this->typeMapReducer($reducedMap, $field->getNullableType());
374
                }
375
            }
376
377
            if ($type instanceof InputObjectType) {
378
                foreach ($type->getFields() as $field) {
379
                    $reducedMap = $this->typeMapReducer($reducedMap, $field->getNullableType());
380
                }
381
            }
382
383
            return $reducedMap;
384
        }
385
386
        return $map;
387
    }
388
389
    /**
390
     * @param array     $map
391
     * @param Directive $directive
392
     * @return array
393
     * @throws InvariantException
394
     */
395
    protected function typeMapDirectiveReducer(array $map, Directive $directive): array
396
    {
397
        if (!$directive->hasArguments()) {
398
            return $map;
399
        }
400
401
        return \array_reduce($directive->getArguments(), function ($map, Argument $argument) {
402
            return $this->typeMapReducer($map, $argument->getNullableType());
403
        }, $map);
404
    }
405
406
    /**
407
     * @return string
408
     */
409
    public function __toString(): string
410
    {
411
        return 'schema';
412
    }
413
}
414