Passed
Push — master ( fa923b...ffa870 )
by Christoffer
02:28
created

Schema::getDirectives()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
eloc 1
nc 1
nop 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 array                                 $map
316
     * @param TypeInterface|NameAwareInterface|null $type
317
     * @return array
318
     * @throws InvariantException
319
     */
320
    protected function typeMapReducer(array $map, ?TypeInterface $type): array
321
    {
322
        if (null === $type) {
323
            return $map;
324
        }
325
326
        if ($type instanceof WrappingTypeInterface) {
327
            return $this->typeMapReducer($map, $type->getOfType());
328
        }
329
330
        $typeName = $type->getName();
331
332
        if (isset($map[$typeName])) {
333
            invariant(
334
                $type === $map[$typeName],
335
                \sprintf(
336
                    'Schema must contain unique named types but contains multiple types named "%s".',
337
                    $type->getName()
338
                )
339
            );
340
341
            return $map;
342
        }
343
344
        $map[$typeName] = $type;
345
346
        $reducedMap = $map;
347
348
        if ($type instanceof UnionType) {
349
            $reducedMap = \array_reduce($type->getTypes(), [$this, 'typeMapReducer'], $reducedMap);
350
        }
351
352
        if ($type instanceof ObjectType) {
353
            $reducedMap = \array_reduce($type->getInterfaces(), [$this, 'typeMapReducer'], $reducedMap);
354
        }
355
356
        if ($type instanceof ObjectType || $type instanceof InterfaceType) {
357
            foreach ($type->getFields() as $field) {
358
                if ($field->hasArguments()) {
359
                    $fieldArgTypes = \array_map(function (Argument $argument) {
360
                        return $argument->getType();
361
                    }, $field->getArguments());
362
363
                    $reducedMap = \array_reduce($fieldArgTypes, [$this, 'typeMapReducer'], $reducedMap);
364
                }
365
366
                $reducedMap = $this->typeMapReducer($reducedMap, $field->getType());
367
            }
368
        }
369
370
        if ($type instanceof InputObjectType) {
371
            foreach ($type->getFields() as $field) {
372
                $reducedMap = $this->typeMapReducer($reducedMap, $field->getType());
373
            }
374
        }
375
376
        return $reducedMap;
377
    }
378
379
    /**
380
     * Note: We do not type-hint the `$directive`, because we want the `SchemaValidator` to catch these errors.
381
     *
382
     * @param array      $map
383
     * @param mixed|null $directive
384
     * @return array
385
     */
386
    protected function typeMapDirectiveReducer(array $map, $directive): array
387
    {
388
        if (!($directive instanceof Directive) ||
389
            ($directive instanceof Directive && !$directive->hasArguments())) {
390
            return $map;
391
        }
392
393
        return \array_reduce($directive->getArguments(), function ($map, Argument $argument) {
394
            return $this->typeMapReducer($map, $argument->getType());
395
        }, $map);
396
    }
397
}
398