Passed
Pull Request — master (#351)
by Kirill
02:29
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 ObjectTypeInterface|null
60
     */
61
    protected $queryType;
62
63
    /**
64
     * @var ObjectTypeInterface|null
65
     */
66
    protected $mutationType;
67
68
    /**
69
     * @var ObjectTypeInterface|null
70
     */
71
    protected $subscriptionType;
72
73
    /**
74
     * @var array|TypeInterface[]
75
     */
76
    protected $types = [];
77
78
    /**
79
     * @var array|DirectiveInterface[]
80
     */
81
    protected $directives = [];
82
83
    /**
84
     * @var bool
85
     */
86
    protected $assumeValid = false;
87
88
    /**
89
     * @var array|NamedTypeInterface[]
90
     */
91
    protected $typeMap = [];
92
93
    /**
94
     * @var array|ObjectTypeInterface[][]
95
     */
96
    protected $implementations = [];
97
98
    /**
99
     * @var array|NamedTypeInterface[]
100
     */
101
    protected $possibleTypesMap = [];
102
103
    /**
104
     * Schema constructor.
105
     *
106
     * @param ObjectTypeInterface|null                               $queryType
107
     * @param ObjectTypeInterface|null                               $mutationType
108
     * @param ObjectTypeInterface|null                               $subscriptionType
109
     * @param TypeInterface[]                                        $types
110
     * @param DirectiveInterface[]                                   $directives
111
     * @param bool                                                   $assumeValid
112
     * @param SchemaDefinitionNode|null                              $astNode
113
     * @param ObjectTypeExtensionNode[]|InterfaceTypeExtensionNode[] $extensionASTNodes
114
     * @throws InvariantException
115
     */
116
    public function __construct(
117
        ?ObjectTypeInterface $queryType,
118
        ?ObjectTypeInterface $mutationType,
119
        ?ObjectTypeInterface $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 ObjectTypeInterface|null
143
     */
144
    public function getQueryType(): ?ObjectTypeInterface
145
    {
146
        return $this->queryType;
147
    }
148
149
    /**
150
     * @return ObjectTypeInterface|null
151
     */
152
    public function getMutationType(): ?ObjectTypeInterface
153
    {
154
        return $this->mutationType;
155
    }
156
157
    /**
158
     * @return ObjectTypeInterface|null
159
     */
160
    public function getSubscriptionType(): ?ObjectTypeInterface
161
    {
162
        return $this->subscriptionType;
163
    }
164
165
    /**
166
     * @param string $name
167
     * @return DirectiveInterface|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 DirectiveInterface[]
178
     */
179
    public function getDirectives(): array
180
    {
181
        return $this->directives;
182
    }
183
184
    /**
185
     * @return NamedTypeInterface[]
186
     */
187
    public function getTypeMap(): array
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
                static function (array $map, NamedTypeInterface $type) {
228
                    $map[$type->getName()] = true;
229
230
                    return $map;
231
                },
232
                []
233
            );
234
        }
235
236
        return isset($this->possibleTypesMap[$abstractTypeName][$possibleTypeName]);
237
    }
238
239
    /**
240
     * @param AbstractTypeContract $abstractType
241
     * @return array|ObjectTypeInterface[]
242
     * @throws InvariantException
243
     */
244
    public function getPossibleTypes(AbstractTypeContract $abstractType): array
245
    {
246
        assert($abstractType instanceof NamedTypeInterface);
247
248
        if ($abstractType instanceof UnionType) {
249
            return $abstractType->getTypes();
250
        }
251
252
        return $this->implementations[$abstractType->getName()] ?? [];
253
    }
254
255
    /**
256
     * @param string $name
257
     * @return NamedTypeInterface|null
258
     */
259
    public function getType(string $name): ?NamedTypeInterface
260
    {
261
        return $this->typeMap[$name] ?? null;
262
    }
263
264
    /**
265
     * @return void
266
     */
267
    protected function buildTypeMap(): void
268
    {
269
        $initialTypes = [
270
            $this->queryType,
271
            $this->mutationType,
272
            $this->subscriptionType,
273
            __Schema(), // Introspection schema
274
        ];
275
276
        if (!empty($this->types)) {
277
            $initialTypes = \array_merge($initialTypes, $this->types);
278
        }
279
280
        // Keep track of all types referenced within the schema.
281
        $typeMap = [];
282
283
        // First by deeply visiting all initial types.
284
        $typeMap = \array_reduce($initialTypes, [$this, 'typeMapReducer'], $typeMap);
285
286
        // Then by deeply visiting all directive types.
287
        $typeMap = \array_reduce($this->directives, [$this, 'typeMapDirectiveReducer'], $typeMap);
288
289
        // Storing the resulting map for reference by the schema.
290
        $this->typeMap = $typeMap;
291
    }
292
293
    /**
294
     * @throws InvariantException
295
     */
296
    protected function buildImplementations(): void
297
    {
298
        $implementations = [];
299
300
        // Keep track of all implementations by interface name.
301
        foreach ($this->typeMap as $typeName => $type) {
302
            if ($type instanceof ObjectType) {
303
                foreach ($type->getInterfaces() as $interface) {
304
                    if (!($interface instanceof InterfaceType)) {
305
                        continue;
306
                    }
307
308
                    $interfaceName = $interface->getName();
309
310
                    if (!isset($implementations[$interfaceName])) {
311
                        $implementations[$interfaceName] = [];
312
                    }
313
314
                    $implementations[$interfaceName][] = $type;
315
                }
316
            }
317
        }
318
319
        $this->implementations = $implementations;
320
    }
321
322
    /**
323
     * @param array              $map
324
     * @param TypeInterface|null $type
325
     * @return array
326
     * @throws InvariantException
327
     */
328
    protected function typeMapReducer(array $map, ?TypeInterface $type): array
329
    {
330
        if (null === $type) {
331
            return $map;
332
        }
333
334
        if ($type instanceof WrappingTypeInterface) {
335
            return $this->typeMapReducer($map, $type->getOfType());
336
        }
337
338
        if ($type instanceof NamedTypeInterface) {
339
            $typeName = $type->getName();
340
341
            if (isset($map[$typeName])) {
342
                if ($type !== $map[$typeName]) {
343
                    throw new InvariantException(\sprintf(
344
                        'Schema must contain unique named types but contains multiple types named "%s".',
345
                        $type->getName()
346
                    ));
347
                }
348
349
                return $map;
350
            }
351
352
            $map[$typeName] = $type;
353
354
            $reducedMap = $map;
355
356
            if ($type instanceof UnionType) {
357
                $reducedMap = \array_reduce($type->getTypes(), [$this, 'typeMapReducer'], $reducedMap);
358
            }
359
360
            if ($type instanceof ObjectType) {
361
                $reducedMap = \array_reduce($type->getInterfaces(), [$this, 'typeMapReducer'], $reducedMap);
362
            }
363
364
            if ($type instanceof ObjectType || $type instanceof InterfaceType) {
365
                foreach ($type->getFields() as $field) {
366
                    if ($field->hasArguments()) {
367
                        $fieldArgTypes = \array_map(function (Argument $argument) {
368
                            return $argument->getNullableType();
369
                        }, $field->getArguments());
370
371
                        $reducedMap = \array_reduce($fieldArgTypes, [$this, 'typeMapReducer'], $reducedMap);
372
                    }
373
374
                    $reducedMap = $this->typeMapReducer($reducedMap, $field->getNullableType());
375
                }
376
            }
377
378
            if ($type instanceof InputObjectType) {
379
                foreach ($type->getFields() as $field) {
380
                    $reducedMap = $this->typeMapReducer($reducedMap, $field->getNullableType());
381
                }
382
            }
383
384
            return $reducedMap;
385
        }
386
387
        return $map;
388
    }
389
390
    /**
391
     * @param array     $map
392
     * @param Directive $directive
393
     * @return array
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