Completed
Pull Request — master (#201)
by Christoffer
02:54
created

Schema::__construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 21
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 11
nc 2
nop 7
dl 0
loc 21
rs 9.3142
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\NameAwareInterface;
8
use Digia\GraphQL\Language\Node\SchemaDefinitionNode;
9
use Digia\GraphQL\Type\Definition\AbstractTypeInterface;
10
use Digia\GraphQL\Type\Definition\Argument;
11
use Digia\GraphQL\Type\Definition\Directive;
12
use Digia\GraphQL\Type\Definition\InputObjectType;
13
use Digia\GraphQL\Type\Definition\InterfaceType;
14
use Digia\GraphQL\Type\Definition\ObjectType;
15
use Digia\GraphQL\Type\Definition\TypeInterface;
16
use Digia\GraphQL\Type\Definition\UnionType;
17
use Digia\GraphQL\Type\Definition\WrappingTypeInterface;
18
use function Digia\GraphQL\Type\__Schema;
19
use function Digia\GraphQL\Util\find;
20
use function Digia\GraphQL\Util\invariant;
21
22
/**
23
 * Schema Definition
24
 *
25
 * A Schema is created by supplying the root types of each type of operation,
26
 * query and mutation (optional). A schema definition is then supplied to the
27
 * validator and executor.
28
 *
29
 * Example:
30
 *
31
 *     $MyAppSchema = GraphQLSchema([
32
 *       'query'    => $MyAppQueryRootType,
33
 *       'mutation' => $MyAppMutationRootType,
34
 *     ])
35
 *
36
 * Note: If an array of `directives` are provided to GraphQLSchema, that will be
37
 * the exact list of directives represented and allowed. If `directives` is not
38
 * provided then a default set of the specified directives (e.g. @include and
39
 * @skip) will be used. If you wish to provide *additional* directives to these
40
 * specified directives, you must explicitly declare them. Example:
41
 *
42
 *     $MyAppSchema = GraphQLSchema([
43
 *       ...
44
 *       'directives' => \array_merge(specifiedDirectives(), [$myCustomDirective]),
45
 *     ])
46
 */
47
48
/**
49
 * Class Schema
50
 *
51
 * @package Digia\GraphQL\Type
52
 * @property SchemaDefinitionNode $astNode
53
 */
54
class Schema implements SchemaInterface
55
{
56
    use ASTNodeTrait;
57
58
    /**
59
     * @var TypeInterface|null
60
     */
61
    protected $query;
62
63
    /**
64
     * @var TypeInterface|null
65
     */
66
    protected $mutation;
67
68
    /**
69
     * @var TypeInterface|null
70
     */
71
    protected $subscription;
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 array
90
     */
91
    protected $typeMap = [];
92
93
    /**
94
     * @var array
95
     */
96
    protected $implementations = [];
97
98
    /**
99
     * @var array
100
     */
101
    protected $possibleTypesMap = [];
102
103
    /**
104
     * Schema constructor.
105
     *
106
     * @param SchemaDefinitionNode   $astNode
107
     * @param TypeInterface|null     $query
108
     * @param TypeInterface|null     $mutation
109
     * @param TypeInterface|null     $subscription
110
     * @param TypeInterface[]        $types
111
     * @param Directive[]            $directives
112
     * @param bool                   $assumeValid
113
     * @param SchemaDefinitionNode[] $astNode
114
     * @throws InvariantException
115
     */
116
    public function __construct(
117
        ?TypeInterface $query,
118
        ?TypeInterface $mutation,
119
        ?TypeInterface $subscription,
120
        array $types,
121
        array $directives,
122
        bool $assumeValid,
123
        ?SchemaDefinitionNode $astNode
124
    ) {
125
        $this->query        = $query;
126
        $this->mutation     = $mutation;
127
        $this->subscription = $subscription;
128
        $this->types        = $types;
129
        $this->directives   = !empty($directives)
130
            ? $directives
131
            : specifiedDirectives();
132
        $this->assumeValid  = $assumeValid;
133
        $this->astNode      = $astNode;
134
135
        $this->buildTypeMap();
136
        $this->buildImplementations();
137
    }
138
139
    /**
140
     * @inheritdoc
141
     */
142
    public function getQueryType(): ?TypeInterface
143
    {
144
        return $this->query;
145
    }
146
147
    /**
148
     * @inheritdoc
149
     */
150
    public function getMutationType(): ?TypeInterface
151
    {
152
        return $this->mutation;
153
    }
154
155
    /**
156
     * @inheritdoc
157
     */
158
    public function getSubscriptionType(): ?TypeInterface
159
    {
160
        return $this->subscription;
161
    }
162
163
    /**
164
     * @inheritdoc
165
     */
166
    public function getDirective(string $name): ?Directive
167
    {
168
        return find($this->directives, function (Directive $directive) use ($name) {
169
            return $directive->getName() === $name;
170
        });
171
    }
172
173
    /**
174
     * @inheritdoc
175
     */
176
    public function getDirectives(): array
177
    {
178
        return $this->directives;
179
    }
180
181
    /**
182
     * @inheritdoc
183
     */
184
    public function getTypeMap(): array
185
    {
186
        return $this->typeMap;
187
    }
188
189
    /**
190
     * @inheritdoc
191
     */
192
    public function getAssumeValid(): bool
193
    {
194
        return $this->assumeValid;
195
    }
196
197
    /**
198
     * @inheritdoc
199
     * @throws InvariantException
200
     */
201
    public function isPossibleType(AbstractTypeInterface $abstractType, TypeInterface $possibleType): bool
202
    {
203
        /** @noinspection PhpUndefinedMethodInspection */
204
        $abstractTypeName = $abstractType->getName();
205
        /** @noinspection PhpUndefinedMethodInspection */
206
        $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\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

206
        /** @scrutinizer ignore-call */ 
207
        $possibleTypeName = $possibleType->getName();
Loading history...
207
208
        if (!isset($this->possibleTypesMap[$abstractTypeName])) {
209
            $possibleTypes = $this->getPossibleTypes($abstractType);
210
211
            invariant(
212
                \is_array($possibleTypes),
213
                \sprintf(
214
                    'Could not find possible implementing types for %s ' .
215
                    'in schema. Check that schema.types is defined and is an array of ' .
216
                    'all possible types in the schema.',
217
                    $abstractTypeName
218
                )
219
            );
220
221
            $this->possibleTypesMap[$abstractTypeName] = \array_reduce(
222
                $possibleTypes,
223
                function (array $map, TypeInterface $type) {
224
                    /** @var NameAwareInterface $type */
225
                    $map[$type->getName()] = true;
226
                    return $map;
227
                },
228
                []
229
            );
230
        }
231
232
        return isset($this->possibleTypesMap[$abstractTypeName][$possibleTypeName]);
233
    }
234
235
    /**
236
     * @inheritdoc
237
     * @throws InvariantException
238
     */
239
    public function getPossibleTypes(AbstractTypeInterface $abstractType): ?array
240
    {
241
        if ($abstractType instanceof UnionType) {
242
            return $abstractType->getTypes();
243
        }
244
245
        return $this->implementations[$abstractType->getName()] ?? null;
246
    }
247
248
    /**
249
     * @inheritdoc
250
     */
251
    public function getType(string $name): ?TypeInterface
252
    {
253
        return $this->typeMap[$name] ?? null;
254
    }
255
256
    /**
257
     *
258
     */
259
    protected function buildTypeMap(): void
260
    {
261
        $initialTypes = [
262
            $this->query,
263
            $this->mutation,
264
            $this->subscription,
265
            __Schema(), // Introspection schema
266
        ];
267
268
        if ($this->types) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->types of type Digia\GraphQL\Type\Definition\TypeInterface[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
269
            $initialTypes = \array_merge($initialTypes, $this->types);
270
        }
271
272
        // Keep track of all types referenced within the schema.
273
        $typeMap = [];
274
275
        // First by deeply visiting all initial types.
276
        $typeMap = \array_reduce($initialTypes, [$this, 'typeMapReducer'], $typeMap);
277
278
        // Then by deeply visiting all directive types.
279
        $typeMap = \array_reduce($this->directives, [$this, 'typeMapDirectiveReducer'], $typeMap);
280
281
        // Storing the resulting map for reference by the schema.
282
        $this->typeMap = $typeMap;
283
    }
284
285
    /**
286
     * @throws InvariantException
287
     */
288
    protected function buildImplementations()
289
    {
290
        $implementations = [];
291
292
        // Keep track of all implementations by interface name.
293
        foreach ($this->typeMap as $typeName => $type) {
294
            if ($type instanceof ObjectType) {
295
                foreach ($type->getInterfaces() as $interface) {
296
                    if (!($interface instanceof InterfaceType)) {
297
                        continue;
298
                    }
299
300
                    $interfaceName = $interface->getName();
301
302
                    if (!isset($implementations[$interfaceName])) {
303
                        $implementations[$interfaceName] = [];
304
                    }
305
306
                    $implementations[$interfaceName][] = $type;
307
                }
308
            }
309
        }
310
311
        $this->implementations = $implementations;
312
    }
313
314
    /**
315
     * @param TypeInterface|null $query
316
     * @return Schema
317
     */
318
    protected function setQuery(?TypeInterface $query): Schema
319
    {
320
        $this->query = $query;
321
        return $this;
322
    }
323
324
    /**
325
     * @param TypeInterface|null $mutation
326
     * @return Schema
327
     */
328
    protected function setMutation(?TypeInterface $mutation): Schema
329
    {
330
        $this->mutation = $mutation;
331
        return $this;
332
    }
333
334
    /**
335
     * @param TypeInterface|null $subscription
336
     * @return Schema
337
     */
338
    protected function setSubscription(?TypeInterface $subscription): Schema
339
    {
340
        $this->subscription = $subscription;
341
        return $this;
342
    }
343
344
    /**
345
     * @param array $types
346
     * @return Schema
347
     */
348
    protected function setTypes(array $types): Schema
349
    {
350
        $this->types = $types;
351
        return $this;
352
    }
353
354
    /**
355
     * @param Directive[] $directives
356
     * @return Schema
357
     */
358
    protected function setDirectives(array $directives): Schema
359
    {
360
        $this->directives = $directives;
361
        return $this;
362
    }
363
364
    /**
365
     * @param bool $assumeValid
366
     * @return Schema
367
     */
368
    protected function setAssumeValid(bool $assumeValid): Schema
369
    {
370
        $this->assumeValid = $assumeValid;
371
        return $this;
372
    }
373
374
    /**
375
     * @param array                                 $map
376
     * @param TypeInterface|NameAwareInterface|null $type
377
     * @return array
378
     * @throws InvariantException
379
     */
380
    protected function typeMapReducer(array $map, ?TypeInterface $type): array
381
    {
382
        if (null === $type) {
383
            return $map;
384
        }
385
386
        if ($type instanceof WrappingTypeInterface) {
387
            return $this->typeMapReducer($map, $type->getOfType());
388
        }
389
390
        $typeName = $type->getName();
391
392
        if (isset($map[$typeName])) {
393
            invariant(
394
                $type === $map[$typeName],
395
                \sprintf(
396
                    'Schema must contain unique named types but contains multiple types named "%s".',
397
                    $type->getName()
398
                )
399
            );
400
401
            return $map;
402
        }
403
404
        $map[$typeName] = $type;
405
406
        $reducedMap = $map;
407
408
        if ($type instanceof UnionType) {
409
            $reducedMap = \array_reduce($type->getTypes(), [$this, 'typeMapReducer'], $reducedMap);
410
        }
411
412
        if ($type instanceof ObjectType) {
413
            $reducedMap = \array_reduce($type->getInterfaces(), [$this, 'typeMapReducer'], $reducedMap);
414
        }
415
416
        if ($type instanceof ObjectType || $type instanceof InterfaceType) {
417
            foreach ($type->getFields() as $field) {
418
                if ($field->hasArguments()) {
419
                    $fieldArgTypes = \array_map(function (Argument $argument) {
420
                        return $argument->getType();
421
                    }, $field->getArguments());
422
423
                    $reducedMap = \array_reduce($fieldArgTypes, [$this, 'typeMapReducer'], $reducedMap);
424
                }
425
426
                $reducedMap = $this->typeMapReducer($reducedMap, $field->getType());
427
            }
428
        }
429
430
        if ($type instanceof InputObjectType) {
431
            foreach ($type->getFields() as $field) {
432
                $reducedMap = $this->typeMapReducer($reducedMap, $field->getType());
433
            }
434
        }
435
436
        return $reducedMap;
437
    }
438
439
    /**
440
     * Note: We do not type-hint the `$directive`, because we want the `SchemaValidator` to catch these errors.
441
     *
442
     * @param array      $map
443
     * @param mixed|null $directive
444
     * @return array
445
     */
446
    protected function typeMapDirectiveReducer(array $map, $directive): array
447
    {
448
        if (!($directive instanceof Directive) ||
449
            ($directive instanceof Directive && !$directive->hasArguments())) {
450
            return $map;
451
        }
452
453
        return \array_reduce($directive->getArguments(), function ($map, Argument $argument) {
454
            return $this->typeMapReducer($map, $argument->getType());
455
        }, $map);
456
    }
457
}
458