Schema   F
last analyzed

Complexity

Total Complexity 70

Size/Duplication

Total Lines 475
Duplicated Lines 0 %

Test Coverage

Coverage 88.44%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 70
eloc 163
dl 0
loc 475
ccs 153
cts 173
cp 0.8844
rs 2.8
c 2
b 0
f 0

21 Methods

Rating   Name   Duplication   Size   Complexity  
A collectAllTypes() 0 21 6
A getDirectives() 0 3 2
C __construct() 0 73 13
A getTypeMap() 0 8 2
A validate() 0 17 2
A getDirective() 0 9 3
A assertValid() 0 26 5
A getAstNode() 0 3 1
A defaultTypeLoader() 0 6 1
B resolveAdditionalTypes() 0 24 7
B getPossibleTypeMap() 0 22 8
A getMutationType() 0 3 1
A getSubscriptionType() 0 3 1
A getConfig() 0 3 1
A getQueryType() 0 3 1
A getOperationType() 0 11 4
A isPossibleType() 0 11 3
A hasType() 0 3 1
A getType() 0 11 3
A loadType() 0 26 4
A getPossibleTypes() 0 5 1

How to fix   Complexity   

Complex Class

Complex classes like Schema often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Schema, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace GraphQL\Type;
6
7
use Generator;
8
use GraphQL\Error\Error;
9
use GraphQL\Error\InvariantViolation;
10
use GraphQL\GraphQL;
11
use GraphQL\Language\AST\SchemaDefinitionNode;
12
use GraphQL\Language\AST\SchemaTypeExtensionNode;
13
use GraphQL\Type\Definition\AbstractType;
14
use GraphQL\Type\Definition\Directive;
15
use GraphQL\Type\Definition\InterfaceType;
16
use GraphQL\Type\Definition\ObjectType;
17
use GraphQL\Type\Definition\Type;
18
use GraphQL\Type\Definition\UnionType;
19
use GraphQL\Utils\TypeInfo;
20
use GraphQL\Utils\Utils;
21
use Traversable;
22
use function array_values;
23
use function implode;
24
use function is_array;
25
use function is_callable;
26
use function sprintf;
27
28
/**
29
 * Schema Definition (see [related docs](type-system/schema.md))
30
 *
31
 * A Schema is created by supplying the root types of each type of operation:
32
 * query, mutation (optional) and subscription (optional). A schema definition is
33
 * then supplied to the validator and executor. Usage Example:
34
 *
35
 *     $schema = new GraphQL\Type\Schema([
36
 *       'query' => $MyAppQueryRootType,
37
 *       'mutation' => $MyAppMutationRootType,
38
 *     ]);
39
 *
40
 * Or using Schema Config instance:
41
 *
42
 *     $config = GraphQL\Type\SchemaConfig::create()
43
 *         ->setQuery($MyAppQueryRootType)
44
 *         ->setMutation($MyAppMutationRootType);
45
 *
46
 *     $schema = new GraphQL\Type\Schema($config);
47
 */
48
class Schema
49
{
50
    /** @var SchemaConfig */
51
    private $config;
52
53
    /**
54
     * Contains currently resolved schema types
55
     *
56
     * @var Type[]
57
     */
58
    private $resolvedTypes = [];
59
60
    /** @var array<string, array<string, ObjectType>>|null */
61
    private $possibleTypeMap;
62
63
    /**
64
     * True when $resolvedTypes contain all possible schema types
65
     *
66
     * @var bool
67
     */
68
    private $fullyLoaded = false;
69
70
    /** @var Error[] */
71
    private $validationErrors;
72
73
    /** @var SchemaTypeExtensionNode[] */
74
    public $extensionASTNodes;
75
76
    /**
77
     * @param mixed[]|SchemaConfig $config
78
     *
79
     * @api
80
     */
81 891
    public function __construct($config)
82
    {
83 891
        if (is_array($config)) {
84 891
            $config = SchemaConfig::create($config);
85
        }
86
87
        // If this schema was built from a source known to be valid, then it may be
88
        // marked with assumeValid to avoid an additional type system validation.
89 890
        if ($config->getAssumeValid()) {
90
            $this->validationErrors = [];
91
        } else {
92
            // Otherwise check for common mistakes during construction to produce
93
            // clear and early error messages.
94 890
            Utils::invariant(
95 890
                $config instanceof SchemaConfig,
96 890
                'Schema constructor expects instance of GraphQL\Type\SchemaConfig or an array with keys: %s; but got: %s',
97 890
                implode(
98 890
                    ', ',
99
                    [
100 890
                        'query',
101
                        'mutation',
102
                        'subscription',
103
                        'types',
104
                        'directives',
105
                        'typeLoader',
106
                    ]
107
                ),
108 890
                Utils::getVariableType($config)
109
            );
110 890
            Utils::invariant(
111 890
                ! $config->types || is_array($config->types) || is_callable($config->types),
112 890
                '"types" must be array or callable if provided but got: ' . Utils::getVariableType($config->types)
113
            );
114 890
            Utils::invariant(
115 890
                ! $config->directives || is_array($config->directives),
0 ignored issues
show
Bug Best Practice introduced by
The expression $config->directives of type GraphQL\Type\Definition\Directive[] 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...
116 890
                '"directives" must be Array if provided but got: ' . Utils::getVariableType($config->directives)
117
            );
118
        }
119
120 890
        $this->config            = $config;
121 890
        $this->extensionASTNodes = $config->extensionASTNodes;
122
123 890
        if ($config->query) {
124 873
            $this->resolvedTypes[$config->query->name] = $config->query;
125
        }
126 890
        if ($config->mutation) {
127 83
            $this->resolvedTypes[$config->mutation->name] = $config->mutation;
128
        }
129 890
        if ($config->subscription) {
130 35
            $this->resolvedTypes[$config->subscription->name] = $config->subscription;
131
        }
132 890
        if (is_array($this->config->types)) {
133 157
            foreach ($this->resolveAdditionalTypes() as $type) {
134 157
                if (isset($this->resolvedTypes[$type->name])) {
135 57
                    Utils::invariant(
136 57
                        $type === $this->resolvedTypes[$type->name],
137 57
                        sprintf(
138 57
                            'Schema must contain unique named types but contains multiple types named "%s" (see http://webonyx.github.io/graphql-php/type-system/#type-registry).',
139 57
                            $type
140
                        )
141
                    );
142
                }
143 157
                $this->resolvedTypes[$type->name] = $type;
144
            }
145
        }
146 889
        $this->resolvedTypes += Type::getStandardTypes() + Introspection::getTypes();
147
148 889
        if ($this->config->typeLoader) {
149 161
            return;
150
        }
151
152
        // Perform full scan of the schema
153 777
        $this->getTypeMap();
154 773
    }
155
156
    /**
157
     * @return Generator
158
     */
159 254
    private function resolveAdditionalTypes()
160
    {
161 254
        $types = $this->config->types ?: [];
162
163 254
        if (is_callable($types)) {
164 112
            $types = $types();
165
        }
166
167 254
        if (! is_array($types) && ! $types instanceof Traversable) {
168
            throw new InvariantViolation(sprintf(
169
                'Schema types callable must return array or instance of Traversable but got: %s',
170
                Utils::getVariableType($types)
171
            ));
172
        }
173
174 254
        foreach ($types as $index => $type) {
175 254
            if (! $type instanceof Type) {
176
                throw new InvariantViolation(sprintf(
177
                    'Each entry of schema types must be instance of GraphQL\Type\Definition\Type but entry at %s is %s',
178
                    $index,
179
                    Utils::printSafe($type)
180
                ));
181
            }
182 254
            yield $type;
183
        }
184 253
    }
185
186
    /**
187
     * Returns array of all types in this schema. Keys of this array represent type names, values are instances
188
     * of corresponding type definitions
189
     *
190
     * This operation requires full schema scan. Do not use in production environment.
191
     *
192
     * @return Type[]
193
     *
194
     * @api
195
     */
196 854
    public function getTypeMap()
197
    {
198 854
        if (! $this->fullyLoaded) {
199 854
            $this->resolvedTypes = $this->collectAllTypes();
200 847
            $this->fullyLoaded   = true;
201
        }
202
203 847
        return $this->resolvedTypes;
204
    }
205
206
    /**
207
     * @return Type[]
208
     */
209 854
    private function collectAllTypes()
210
    {
211 854
        $typeMap = [];
212 854
        foreach ($this->resolvedTypes as $type) {
213 854
            $typeMap = TypeInfo::extractTypes($type, $typeMap);
214
        }
215 847
        foreach ($this->getDirectives() as $directive) {
216 847
            if (! ($directive instanceof Directive)) {
217 1
                continue;
218
            }
219
220 846
            $typeMap = TypeInfo::extractTypesFromDirectives($directive, $typeMap);
0 ignored issues
show
Bug introduced by
It seems like $typeMap can also be of type null; however, parameter $typeMap of GraphQL\Utils\TypeInfo::...ctTypesFromDirectives() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

220
            $typeMap = TypeInfo::extractTypesFromDirectives($directive, /** @scrutinizer ignore-type */ $typeMap);
Loading history...
221
        }
222
        // When types are set as array they are resolved in constructor
223 847
        if (is_callable($this->config->types)) {
224 112
            foreach ($this->resolveAdditionalTypes() as $type) {
225 112
                $typeMap = TypeInfo::extractTypes($type, $typeMap);
226
            }
227
        }
228
229 847
        return $typeMap;
230
    }
231
232
    /**
233
     * Returns a list of directives supported by this schema
234
     *
235
     * @return Directive[]
236
     *
237
     * @api
238
     */
239 868
    public function getDirectives()
240
    {
241 868
        return $this->config->directives ?: GraphQL::getStandardDirectives();
242
    }
243
244
    /**
245
     * @param string $operation
246
     *
247
     * @return ObjectType|null
248
     */
249
    public function getOperationType($operation)
250
    {
251
        switch ($operation) {
252
            case 'query':
253
                return $this->getQueryType();
254
            case 'mutation':
255
                return $this->getMutationType();
256
            case 'subscription':
257
                return $this->getSubscriptionType();
258
            default:
259
                return null;
260
        }
261
    }
262
263
    /**
264
     * Returns schema query type
265
     *
266
     * @return ObjectType
267
     *
268
     * @api
269
     */
270 771
    public function getQueryType()
271
    {
272 771
        return $this->config->query;
273
    }
274
275
    /**
276
     * Returns schema mutation type
277
     *
278
     * @return ObjectType|null
279
     *
280
     * @api
281
     */
282 215
    public function getMutationType()
283
    {
284 215
        return $this->config->mutation;
285
    }
286
287
    /**
288
     * Returns schema subscription
289
     *
290
     * @return ObjectType|null
291
     *
292
     * @api
293
     */
294 208
    public function getSubscriptionType()
295
    {
296 208
        return $this->config->subscription;
297
    }
298
299
    /**
300
     * @return SchemaConfig
301
     *
302
     * @api
303
     */
304 8
    public function getConfig()
305
    {
306 8
        return $this->config;
307
    }
308
309
    /**
310
     * Returns type by its name
311
     *
312
     * @api
313
     */
314 559
    public function getType(string $name) : ?Type
315
    {
316 559
        if (! isset($this->resolvedTypes[$name])) {
317 97
            $type = $this->loadType($name);
318 92
            if (! $type) {
319 70
                return null;
320
            }
321 22
            $this->resolvedTypes[$name] = $type;
322
        }
323
324 512
        return $this->resolvedTypes[$name];
325
    }
326
327 35
    public function hasType(string $name) : bool
328
    {
329 35
        return $this->getType($name) !== null;
330
    }
331
332 98
    private function loadType(string $typeName) : ?Type
333
    {
334 98
        $typeLoader = $this->config->typeLoader;
335
336 98
        if (! $typeLoader) {
337 70
            return $this->defaultTypeLoader($typeName);
338
        }
339
340 28
        $type = $typeLoader($typeName);
341
342 26
        if (! $type instanceof Type) {
343 2
            throw new InvariantViolation(
344 2
                sprintf(
345 2
                    'Type loader is expected to return valid type "%s", but it returned %s',
346 2
                    $typeName,
347 2
                    Utils::printSafe($type)
348
                )
349
            );
350
        }
351 24
        if ($type->name !== $typeName) {
352 1
            throw new InvariantViolation(
353 1
                sprintf('Type loader is expected to return type "%s", but it returned "%s"', $typeName, $type->name)
354
            );
355
        }
356
357 23
        return $type;
358
    }
359
360 70
    private function defaultTypeLoader(string $typeName) : ?Type
361
    {
362
        // Default type loader simply fallbacks to collecting all types
363 70
        $typeMap = $this->getTypeMap();
364
365 70
        return $typeMap[$typeName] ?? null;
366
    }
367
368
    /**
369
     * Returns all possible concrete types for given abstract type
370
     * (implementations for interfaces and members of union type for unions)
371
     *
372
     * This operation requires full schema scan. Do not use in production environment.
373
     *
374
     * @param InterfaceType|UnionType $abstractType
375
     *
376
     * @return array<Type&ObjectType>
377
     *
378
     * @api
379
     */
380 18
    public function getPossibleTypes(Type $abstractType) : array
381
    {
382 18
        $possibleTypeMap = $this->getPossibleTypeMap();
383
384 18
        return array_values($possibleTypeMap[$abstractType->name] ?? []);
385
    }
386
387
    /**
388
     * @return array<string, array<string, ObjectType>>
389
     */
390 18
    private function getPossibleTypeMap()
391
    {
392 18
        if ($this->possibleTypeMap === null) {
393 18
            $this->possibleTypeMap = [];
394 18
            foreach ($this->getTypeMap() as $type) {
395 18
                if ($type instanceof ObjectType) {
396 18
                    foreach ($type->getInterfaces() as $interface) {
397 16
                        if (! ($interface instanceof InterfaceType)) {
398
                            continue;
399
                        }
400
401 18
                        $this->possibleTypeMap[$interface->name][$type->name] = $type;
402
                    }
403 18
                } elseif ($type instanceof UnionType) {
404 12
                    foreach ($type->getTypes() as $innerType) {
405 18
                        $this->possibleTypeMap[$type->name][$innerType->name] = $innerType;
406
                    }
407
                }
408
            }
409
        }
410
411 18
        return $this->possibleTypeMap;
412
    }
413
414
    /**
415
     * Returns true if object type is concrete type of given abstract type
416
     * (implementation for interfaces and members of union type for unions)
417
     *
418
     * @api
419
     */
420 49
    public function isPossibleType(AbstractType $abstractType, ObjectType $possibleType) : bool
421
    {
422 49
        if ($abstractType instanceof InterfaceType) {
423 33
            return $possibleType->implementsInterface($abstractType);
424
        }
425
426 17
        if ($abstractType instanceof UnionType) {
427 17
            return $abstractType->isPossibleType($possibleType);
428
        }
429
430
        throw InvariantViolation::shouldNotHappen();
431
    }
432
433
    /**
434
     * Returns instance of directive by name
435
     *
436
     * @api
437
     */
438 65
    public function getDirective(string $name) : ?Directive
439
    {
440 65
        foreach ($this->getDirectives() as $directive) {
441 65
            if ($directive->name === $name) {
442 65
                return $directive;
443
            }
444
        }
445
446 28
        return null;
447
    }
448
449
    /**
450
     * @return SchemaDefinitionNode
451
     */
452 147
    public function getAstNode()
453
    {
454 147
        return $this->config->getAstNode();
455
    }
456
457
    /**
458
     * Validates schema.
459
     *
460
     * This operation requires full schema scan. Do not use in production environment.
461
     *
462
     * @throws InvariantViolation
463
     *
464
     * @api
465
     */
466 11
    public function assertValid()
467
    {
468 11
        $errors = $this->validate();
469
470 11
        if ($errors) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $errors of type GraphQL\Error\Error[] 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...
471
            throw new InvariantViolation(implode("\n\n", $this->validationErrors));
472
        }
473
474 11
        $internalTypes = Type::getStandardTypes() + Introspection::getTypes();
475 11
        foreach ($this->getTypeMap() as $name => $type) {
476 11
            if (isset($internalTypes[$name])) {
477 1
                continue;
478
            }
479
480 11
            $type->assertValid();
481
482
            // Make sure type loader returns the same instance as registered in other places of schema
483 11
            if (! $this->config->typeLoader) {
484 10
                continue;
485
            }
486
487 1
            Utils::invariant(
488 1
                $this->loadType($name) === $type,
489 1
                sprintf(
490 1
                    'Type loader returns different instance for %s than field/argument definitions. Make sure you always return the same instance for the same type name.',
491 1
                    $name
492
                )
493
            );
494
        }
495 1
    }
496
497
    /**
498
     * Validates schema.
499
     *
500
     * This operation requires full schema scan. Do not use in production environment.
501
     *
502
     * @return InvariantViolation[]|Error[]
503
     *
504
     * @api
505
     */
506 89
    public function validate()
507
    {
508
        // If this Schema has already been validated, return the previous results.
509 89
        if ($this->validationErrors !== null) {
510
            return $this->validationErrors;
511
        }
512
        // Validate the schema, producing a list of errors.
513 89
        $context = new SchemaValidationContext($this);
514 89
        $context->validateRootTypes();
515 89
        $context->validateDirectives();
516 89
        $context->validateTypes();
517
518
        // Persist the results of validation before returning to ensure validation
519
        // does not run multiple times for this schema.
520 89
        $this->validationErrors = $context->getErrors();
521
522 89
        return $this->validationErrors;
523
    }
524
}
525