Failed Conditions
Push — master ( 975c9f...a4f39b )
by Vladimir
09:30
created

SchemaValidationContext   F

Complexity

Total Complexity 111

Size/Duplication

Total Lines 796
Duplicated Lines 0 %

Test Coverage

Coverage 90.93%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 111
eloc 348
c 2
b 0
f 0
dl 0
loc 796
rs 2
ccs 351
cts 386
cp 0.9093

29 Methods

Rating   Name   Duplication   Size   Complexity  
B validateFields() 0 74 9
A getImplementsInterfaceNode() 0 5 1
A getAllDirectiveArgNodes() 0 13 1
B validateRootTypes() 0 31 7
A reportError() 0 4 3
A getFieldArgTypeNode() 0 5 2
A validateName() 0 9 3
A getOperationTypeNode() 0 17 6
A validateObjectInterfaces() 0 24 4
A validateInputFields() 0 31 5
A getUnionMemberTypeNodes() 0 8 1
A getFieldTypeNode() 0 5 2
A validateUnionMembers() 0 33 5
A getAllFieldNodes() 0 8 1
A addError() 0 3 1
B validateDirectives() 0 48 7
A getFieldArgNode() 0 5 1
A getFieldNode() 0 5 1
A getErrors() 0 3 1
F validateObjectImplementsInterface() 0 138 15
A getAllFieldArgNodes() 0 15 5
A getAllImplementsInterfaceNodes() 0 8 1
A getDirectiveArgTypeNode() 0 5 2
A getAllSubNodes() 0 17 4
B validateEnumValues() 0 32 8
A getAllNodes() 0 7 4
A __construct() 0 4 1
B validateTypes() 0 36 9
A getEnumValueNodes() 0 8 1

How to fix   Complexity   

Complex Class

Complex classes like SchemaValidationContext 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 SchemaValidationContext, 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 GraphQL\Error\Error;
8
use GraphQL\Language\AST\EnumValueDefinitionNode;
9
use GraphQL\Language\AST\FieldDefinitionNode;
10
use GraphQL\Language\AST\InputValueDefinitionNode;
11
use GraphQL\Language\AST\InterfaceTypeDefinitionNode;
12
use GraphQL\Language\AST\InterfaceTypeExtensionNode;
13
use GraphQL\Language\AST\ListTypeNode;
14
use GraphQL\Language\AST\NamedTypeNode;
15
use GraphQL\Language\AST\Node;
16
use GraphQL\Language\AST\NodeList;
17
use GraphQL\Language\AST\NonNullTypeNode;
18
use GraphQL\Language\AST\ObjectTypeDefinitionNode;
19
use GraphQL\Language\AST\ObjectTypeExtensionNode;
20
use GraphQL\Language\AST\SchemaDefinitionNode;
21
use GraphQL\Language\AST\TypeDefinitionNode;
22
use GraphQL\Language\AST\TypeNode;
23
use GraphQL\Type\Definition\Directive;
24
use GraphQL\Type\Definition\EnumType;
25
use GraphQL\Type\Definition\EnumValueDefinition;
26
use GraphQL\Type\Definition\FieldDefinition;
27
use GraphQL\Type\Definition\InputObjectField;
28
use GraphQL\Type\Definition\InputObjectType;
29
use GraphQL\Type\Definition\InterfaceType;
30
use GraphQL\Type\Definition\NamedType;
31
use GraphQL\Type\Definition\NonNull;
32
use GraphQL\Type\Definition\ObjectType;
33
use GraphQL\Type\Definition\Type;
34
use GraphQL\Type\Definition\UnionType;
35
use GraphQL\Type\Validation\InputObjectCircularRefs;
36
use GraphQL\Utils\TypeComparators;
37
use GraphQL\Utils\Utils;
38
use function array_filter;
39
use function array_key_exists;
40
use function array_merge;
41
use function count;
42
use function is_array;
43
use function is_object;
44
use function iterator_to_array;
45
use function sprintf;
46
47
class SchemaValidationContext
48
{
49
    /** @var Error[] */
50
    private $errors = [];
51
52
    /** @var Schema */
53
    private $schema;
54
55
    /** @var InputObjectCircularRefs */
56
    private $inputObjectCircularRefs;
57
58 91
    public function __construct(Schema $schema)
59
    {
60 91
        $this->schema                  = $schema;
61 91
        $this->inputObjectCircularRefs = new InputObjectCircularRefs($this);
62 91
    }
63
64
    /**
65
     * @return Error[]
66
     */
67 91
    public function getErrors()
68
    {
69 91
        return $this->errors;
70
    }
71
72 91
    public function validateRootTypes()
73
    {
74 91
        $queryType = $this->schema->getQueryType();
75 91
        if (! $queryType) {
0 ignored issues
show
introduced by
$queryType is of type GraphQL\Type\Definition\ObjectType, thus it always evaluated to true.
Loading history...
76 2
            $this->reportError(
77 2
                'Query root type must be provided.',
78 2
                $this->schema->getAstNode()
79
            );
80 89
        } elseif (! $queryType instanceof ObjectType) {
0 ignored issues
show
introduced by
$queryType is always a sub-type of GraphQL\Type\Definition\ObjectType.
Loading history...
81 2
            $this->reportError(
82 2
                'Query root type must be Object type, it cannot be ' . Utils::printSafe($queryType) . '.',
83 2
                $this->getOperationTypeNode($queryType, 'query')
84
            );
85
        }
86
87 91
        $mutationType = $this->schema->getMutationType();
88 91
        if ($mutationType && ! $mutationType instanceof ObjectType) {
0 ignored issues
show
introduced by
$mutationType is always a sub-type of GraphQL\Type\Definition\ObjectType.
Loading history...
89 2
            $this->reportError(
90 2
                'Mutation root type must be Object type if provided, it cannot be ' . Utils::printSafe($mutationType) . '.',
91 2
                $this->getOperationTypeNode($mutationType, 'mutation')
92
            );
93
        }
94
95 91
        $subscriptionType = $this->schema->getSubscriptionType();
96 91
        if (! $subscriptionType || $subscriptionType instanceof ObjectType) {
0 ignored issues
show
introduced by
$subscriptionType is always a sub-type of GraphQL\Type\Definition\ObjectType.
Loading history...
introduced by
$subscriptionType is of type GraphQL\Type\Definition\ObjectType, thus it always evaluated to true.
Loading history...
97 89
            return;
98
        }
99
100 2
        $this->reportError(
101 2
            'Subscription root type must be Object type if provided, it cannot be ' . Utils::printSafe($subscriptionType) . '.',
102 2
            $this->getOperationTypeNode($subscriptionType, 'subscription')
103
        );
104 2
    }
105
106
    /**
107
     * @param string                                       $message
108
     * @param Node[]|Node|TypeNode|TypeDefinitionNode|null $nodes
109
     */
110 55
    public function reportError($message, $nodes = null)
111
    {
112 55
        $nodes = array_filter($nodes && is_array($nodes) ? $nodes : [$nodes]);
113 55
        $this->addError(new Error($message, $nodes));
114 55
    }
115
116
    /**
117
     * @param Error $error
118
     */
119 60
    private function addError($error)
120
    {
121 60
        $this->errors[] = $error;
122 60
    }
123
124
    /**
125
     * @param Type   $type
126
     * @param string $operation
127
     *
128
     * @return NamedTypeNode|ListTypeNode|NonNullTypeNode|TypeDefinitionNode
129
     */
130 4
    private function getOperationTypeNode($type, $operation)
131
    {
132 4
        $astNode = $this->schema->getAstNode();
133
134 4
        $operationTypeNode = null;
135 4
        if ($astNode instanceof SchemaDefinitionNode) {
0 ignored issues
show
introduced by
$astNode is always a sub-type of GraphQL\Language\AST\SchemaDefinitionNode.
Loading history...
136 3
            $operationTypeNode = null;
137
138 3
            foreach ($astNode->operationTypes as $operationType) {
139 3
                if ($operationType->operation === $operation) {
140 3
                    $operationTypeNode = $operationType;
141 3
                    break;
142
                }
143
            }
144
        }
145
146 4
        return $operationTypeNode ? $operationTypeNode->type : ($type ? $type->astNode : null);
0 ignored issues
show
introduced by
$type is of type GraphQL\Type\Definition\Type, thus it always evaluated to true.
Loading history...
147
    }
148
149 91
    public function validateDirectives()
150
    {
151 91
        $directives = $this->schema->getDirectives();
152 91
        foreach ($directives as $directive) {
153
            // Ensure all directives are in fact GraphQL directives.
154 91
            if (! $directive instanceof Directive) {
155 1
                $this->reportError(
156 1
                    'Expected directive but got: ' . Utils::printSafe($directive) . '.',
157 1
                    is_object($directive) ? $directive->astNode : null
158
                );
159 1
                continue;
160
            }
161
162
            // Ensure they are named correctly.
163 90
            $this->validateName($directive);
164
165
            // TODO: Ensure proper locations.
166
167 90
            $argNames = [];
168 90
            foreach ($directive->args as $arg) {
169 90
                $argName = $arg->name;
170
171
                // Ensure they are named correctly.
172 90
                $this->validateName($directive);
173
174 90
                if (isset($argNames[$argName])) {
175
                    $this->reportError(
176
                        sprintf('Argument @%s(%s:) can only be defined once.', $directive->name, $argName),
177
                        $this->getAllDirectiveArgNodes($directive, $argName)
178
                    );
179
                    continue;
180
                }
181
182 90
                $argNames[$argName] = true;
183
184
                // Ensure the type is an input type.
185 90
                if (Type::isInputType($arg->getType())) {
186 90
                    continue;
187
                }
188
189
                $this->reportError(
190
                    sprintf(
191
                        'The type of @%s(%s:) must be Input Type but got: %s.',
192
                        $directive->name,
193
                        $argName,
194
                        Utils::printSafe($arg->getType())
195
                    ),
196 90
                    $this->getDirectiveArgTypeNode($directive, $argName)
197
                );
198
            }
199
        }
200 91
    }
201
202
    /**
203
     * @param Type|Directive|FieldDefinition|EnumValueDefinition|InputObjectField $node
204
     */
205 91
    private function validateName($node)
206
    {
207
        // Ensure names are valid, however introspection types opt out.
208 91
        $error = Utils::isValidNameError($node->name, $node->astNode);
209 91
        if (! $error || Introspection::isIntrospectionType($node)) {
0 ignored issues
show
Bug introduced by
It seems like $node can also be of type GraphQL\Type\Definition\Directive and GraphQL\Type\Definition\EnumValueDefinition and GraphQL\Type\Definition\FieldDefinition and GraphQL\Type\Definition\InputObjectField; however, parameter $type of GraphQL\Type\Introspection::isIntrospectionType() does only seem to accept GraphQL\Type\Definition\Type, 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

209
        if (! $error || Introspection::isIntrospectionType(/** @scrutinizer ignore-type */ $node)) {
Loading history...
210 91
            return;
211
        }
212
213 5
        $this->addError($error);
214 5
    }
215
216
    /**
217
     * @param string $argName
218
     *
219
     * @return InputValueDefinitionNode[]
220
     */
221
    private function getAllDirectiveArgNodes(Directive $directive, $argName)
222
    {
223
        $subNodes = $this->getAllSubNodes(
224
            $directive,
225
            static function ($directiveNode) {
226
                return $directiveNode->arguments;
227
            }
228
        );
229
230
        return Utils::filter(
231
            $subNodes,
232
            static function ($argNode) use ($argName) {
233
                return $argNode->name->value === $argName;
234
            }
235
        );
236
    }
237
238
    /**
239
     * @param string $argName
240
     *
241
     * @return NamedTypeNode|ListTypeNode|NonNullTypeNode|null
242
     */
243
    private function getDirectiveArgTypeNode(Directive $directive, $argName) : ?TypeNode
244
    {
245
        $argNode = $this->getAllDirectiveArgNodes($directive, $argName)[0];
246
247
        return $argNode ? $argNode->type : null;
0 ignored issues
show
introduced by
$argNode is of type GraphQL\Language\AST\InputValueDefinitionNode, thus it always evaluated to true.
Loading history...
248
    }
249
250 91
    public function validateTypes()
251
    {
252 91
        $typeMap = $this->schema->getTypeMap();
253 91
        foreach ($typeMap as $typeName => $type) {
254
            // Ensure all provided types are in fact GraphQL type.
255 91
            if (! $type instanceof NamedType) {
256 4
                $this->reportError(
257 4
                    'Expected GraphQL named type but got: ' . Utils::printSafe($type) . '.',
258 4
                    $type instanceof Type ? $type->astNode : null
259
                );
260 4
                continue;
261
            }
262
263 91
            $this->validateName($type);
264
265 91
            if ($type instanceof ObjectType) {
266
                // Ensure fields are valid
267 91
                $this->validateFields($type);
268
269
                // Ensure objects implement the interfaces they claim to.
270 91
                $this->validateObjectInterfaces($type);
271 91
            } elseif ($type instanceof InterfaceType) {
272
                // Ensure fields are valid.
273 44
                $this->validateFields($type);
274 91
            } elseif ($type instanceof UnionType) {
275
                // Ensure Unions include valid member types.
276 13
                $this->validateUnionMembers($type);
277 91
            } elseif ($type instanceof EnumType) {
278
                // Ensure Enums have valid values.
279 91
                $this->validateEnumValues($type);
280 91
            } elseif ($type instanceof InputObjectType) {
281
                // Ensure Input Object fields are valid.
282 25
                $this->validateInputFields($type);
283
284
                // Ensure Input Objects do not contain non-nullable circular references
285 91
                $this->inputObjectCircularRefs->validate($type);
286
            }
287
        }
288 91
    }
289
290
    /**
291
     * @param ObjectType|InterfaceType $type
292
     */
293 91
    private function validateFields($type)
294
    {
295 91
        $fieldMap = $type->getFields();
296
297
        // Objects and Interfaces both must define one or more fields.
298 91
        if (! $fieldMap) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $fieldMap of type GraphQL\Type\Definition\FieldDefinition[] 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...
299 1
            $this->reportError(
300 1
                sprintf('Type %s must define one or more fields.', $type->name),
301 1
                $this->getAllNodes($type)
302
            );
303
        }
304
305 91
        foreach ($fieldMap as $fieldName => $field) {
306
            // Ensure they are named correctly.
307 91
            $this->validateName($field);
308
309
            // Ensure they were defined at most once.
310 91
            $fieldNodes = $this->getAllFieldNodes($type, $fieldName);
311 91
            if ($fieldNodes && count($fieldNodes) > 1) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $fieldNodes of type GraphQL\Language\AST\FieldDefinitionNode[] 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...
312
                $this->reportError(
313
                    sprintf('Field %s.%s can only be defined once.', $type->name, $fieldName),
314
                    $fieldNodes
315
                );
316
                continue;
317
            }
318
319
            // Ensure the type is an output type
320 91
            if (! Type::isOutputType($field->getType())) {
321 8
                $this->reportError(
322 8
                    sprintf(
323 8
                        'The type of %s.%s must be Output Type but got: %s.',
324 8
                        $type->name,
325 8
                        $fieldName,
326 8
                        Utils::printSafe($field->getType())
327
                    ),
328 8
                    $this->getFieldTypeNode($type, $fieldName)
329
                );
330
            }
331
332
            // Ensure the arguments are valid
333 91
            $argNames = [];
334 91
            foreach ($field->args as $arg) {
335 91
                $argName = $arg->name;
336
337
                // Ensure they are named correctly.
338 91
                $this->validateName($arg);
339
340 91
                if (isset($argNames[$argName])) {
341
                    $this->reportError(
342
                        sprintf(
343
                            'Field argument %s.%s(%s:) can only be defined once.',
344
                            $type->name,
345
                            $fieldName,
346
                            $argName
347
                        ),
348
                        $this->getAllFieldArgNodes($type, $fieldName, $argName)
349
                    );
350
                }
351 91
                $argNames[$argName] = true;
352
353
                // Ensure the type is an input type
354 91
                if (Type::isInputType($arg->getType())) {
355 91
                    continue;
356
                }
357
358 4
                $this->reportError(
359 4
                    sprintf(
360 4
                        'The type of %s.%s(%s:) must be Input Type but got: %s.',
361 4
                        $type->name,
362 4
                        $fieldName,
363 4
                        $argName,
364 4
                        Utils::printSafe($arg->getType())
365
                    ),
366 91
                    $this->getFieldArgTypeNode($type, $fieldName, $argName)
367
                );
368
            }
369
        }
370 91
    }
371
372
    /**
373
     * @param ObjectType|InterfaceType|UnionType|EnumType|InputObjectType|Directive $type
374
     *
375
     * @return ObjectTypeDefinitionNode[]|ObjectTypeExtensionNode[]|InterfaceTypeDefinitionNode[]|InterfaceTypeExtensionNode[]
376
     */
377 91
    private function getAllNodes($type)
378
    {
379 91
        return $type->astNode
380 49
            ? ($type->extensionASTNodes
0 ignored issues
show
Bug introduced by
The property extensionASTNodes does not seem to exist on GraphQL\Type\Definition\Directive.
Loading history...
381 8
                ? array_merge([$type->astNode], $type->extensionASTNodes)
382 49
                : [$type->astNode])
383 91
            : ($type->extensionASTNodes ?: []);
384
    }
385
386
    /**
387
     * @param ObjectType|InterfaceType|UnionType|EnumType|Directive $obj
388
     *
389
     * @return NodeList
390
     */
391 91
    private function getAllSubNodes($obj, callable $getter)
392
    {
393 91
        $result = new NodeList([]);
394 91
        foreach ($this->getAllNodes($obj) as $astNode) {
395 51
            if (! $astNode) {
396
                continue;
397
            }
398
399 51
            $subNodes = $getter($astNode);
400 51
            if (! $subNodes) {
401
                continue;
402
            }
403
404 51
            $result = $result->merge($subNodes);
405
        }
406
407 91
        return $result;
408
    }
409
410
    /**
411
     * @param ObjectType|InterfaceType $type
412
     * @param string                   $fieldName
413
     *
414
     * @return FieldDefinitionNode[]
415
     */
416 91
    private function getAllFieldNodes($type, $fieldName)
417
    {
418
        $subNodes = $this->getAllSubNodes($type, static function ($typeNode) {
419 50
            return $typeNode->fields;
420 91
        });
421
422
        return Utils::filter($subNodes, static function ($fieldNode) use ($fieldName) {
423 50
            return $fieldNode->name->value === $fieldName;
424 91
        });
425
    }
426
427
    /**
428
     * @param ObjectType|InterfaceType $type
429
     * @param string                   $fieldName
430
     *
431
     * @return NamedTypeNode|ListTypeNode|NonNullTypeNode|null
432
     */
433 15
    private function getFieldTypeNode($type, $fieldName) : ?TypeNode
434
    {
435 15
        $fieldNode = $this->getFieldNode($type, $fieldName);
436
437 15
        return $fieldNode ? $fieldNode->type : null;
438
    }
439
440
    /**
441
     * @param ObjectType|InterfaceType $type
442
     * @param string                   $fieldName
443
     *
444
     * @return FieldDefinitionNode|null
445
     */
446 26
    private function getFieldNode($type, $fieldName)
447
    {
448 26
        $nodes = $this->getAllFieldNodes($type, $fieldName);
449
450 26
        return $nodes[0] ?? null;
451
    }
452
453
    /**
454
     * @param ObjectType|InterfaceType $type
455
     * @param string                   $fieldName
456
     * @param string                   $argName
457
     *
458
     * @return InputValueDefinitionNode[]
459
     */
460 9
    private function getAllFieldArgNodes($type, $fieldName, $argName)
461
    {
462 9
        $argNodes  = [];
463 9
        $fieldNode = $this->getFieldNode($type, $fieldName);
464 9
        if ($fieldNode && $fieldNode->arguments) {
465 6
            foreach ($fieldNode->arguments as $node) {
466 6
                if ($node->name->value !== $argName) {
467 1
                    continue;
468
                }
469
470 6
                $argNodes[] = $node;
471
            }
472
        }
473
474 9
        return $argNodes;
475
    }
476
477
    /**
478
     * @param ObjectType|InterfaceType $type
479
     * @param string                   $fieldName
480
     * @param string                   $argName
481
     *
482
     * @return NamedTypeNode|ListTypeNode|NonNullTypeNode|null
483
     */
484 7
    private function getFieldArgTypeNode($type, $fieldName, $argName) : ?TypeNode
485
    {
486 7
        $fieldArgNode = $this->getFieldArgNode($type, $fieldName, $argName);
487
488 7
        return $fieldArgNode ? $fieldArgNode->type : null;
489
    }
490
491
    /**
492
     * @param ObjectType|InterfaceType $type
493
     * @param string                   $fieldName
494
     * @param string                   $argName
495
     *
496
     * @return InputValueDefinitionNode|null
497
     */
498 9
    private function getFieldArgNode($type, $fieldName, $argName)
499
    {
500 9
        $nodes = $this->getAllFieldArgNodes($type, $fieldName, $argName);
501
502 9
        return $nodes[0] ?? null;
503
    }
504
505 91
    private function validateObjectInterfaces(ObjectType $object)
506
    {
507 91
        $implementedTypeNames = [];
508 91
        foreach ($object->getInterfaces() as $iface) {
509 45
            if (! $iface instanceof InterfaceType) {
510 2
                $this->reportError(
511 2
                    sprintf(
512 2
                        'Type %s must only implement Interface types, it cannot implement %s.',
513 2
                        $object->name,
514 2
                        Utils::printSafe($iface)
515
                    ),
516 2
                    $this->getImplementsInterfaceNode($object, $iface)
517
                );
518 2
                continue;
519
            }
520 43
            if (isset($implementedTypeNames[$iface->name])) {
521 1
                $this->reportError(
522 1
                    sprintf('Type %s can only implement %s once.', $object->name, $iface->name),
523 1
                    $this->getAllImplementsInterfaceNodes($object, $iface)
524
                );
525 1
                continue;
526
            }
527 43
            $implementedTypeNames[$iface->name] = true;
528 43
            $this->validateObjectImplementsInterface($object, $iface);
529
        }
530 91
    }
531
532
    /**
533
     * @param InterfaceType $iface
534
     *
535
     * @return NamedTypeNode|null
536
     */
537 2
    private function getImplementsInterfaceNode(ObjectType $type, $iface)
538
    {
539 2
        $nodes = $this->getAllImplementsInterfaceNodes($type, $iface);
540
541 2
        return $nodes[0] ?? null;
542
    }
543
544
    /**
545
     * @param InterfaceType $iface
546
     *
547
     * @return NamedTypeNode[]
548
     */
549 3
    private function getAllImplementsInterfaceNodes(ObjectType $type, $iface)
550
    {
551
        $subNodes = $this->getAllSubNodes($type, static function ($typeNode) {
552 2
            return $typeNode->interfaces;
553 3
        });
554
555
        return Utils::filter($subNodes, static function ($ifaceNode) use ($iface) {
556 2
            return $ifaceNode->name->value === $iface->name;
557 3
        });
558
    }
559
560
    /**
561
     * @param InterfaceType $iface
562
     */
563 43
    private function validateObjectImplementsInterface(ObjectType $object, $iface)
564
    {
565 43
        $objectFieldMap = $object->getFields();
566 43
        $ifaceFieldMap  = $iface->getFields();
567
568
        // Assert each interface field is implemented.
569 43
        foreach ($ifaceFieldMap as $fieldName => $ifaceField) {
570 43
            $objectField = array_key_exists($fieldName, $objectFieldMap)
571 42
                ? $objectFieldMap[$fieldName]
572 43
                : null;
573
574
            // Assert interface field exists on object.
575 43
            if (! $objectField) {
576 3
                $this->reportError(
577 3
                    sprintf(
578 3
                        'Interface field %s.%s expected but %s does not provide it.',
579 3
                        $iface->name,
580 3
                        $fieldName,
581 3
                        $object->name
582
                    ),
583 3
                    array_merge(
584 3
                        [$this->getFieldNode($iface, $fieldName)],
585 3
                        $this->getAllNodes($object)
586
                    )
587
                );
588 3
                continue;
589
            }
590
591
            // Assert interface field type is satisfied by object field type, by being
592
            // a valid subtype. (covariant)
593 42
            if (! TypeComparators::isTypeSubTypeOf(
594 42
                $this->schema,
595 42
                $objectField->getType(),
596 42
                $ifaceField->getType()
597
            )
598
            ) {
599 7
                $this->reportError(
600 7
                    sprintf(
601 7
                        'Interface field %s.%s expects type %s but %s.%s is type %s.',
602 7
                        $iface->name,
603 7
                        $fieldName,
604 7
                        $ifaceField->getType(),
605 7
                        $object->name,
606 7
                        $fieldName,
607 7
                        Utils::printSafe($objectField->getType())
608
                    ),
609
                    [
610 7
                        $this->getFieldTypeNode($iface, $fieldName),
611 7
                        $this->getFieldTypeNode($object, $fieldName),
612
                    ]
613
                );
614
            }
615
616
            // Assert each interface field arg is implemented.
617 42
            foreach ($ifaceField->args as $ifaceArg) {
618 9
                $argName   = $ifaceArg->name;
619 9
                $objectArg = null;
620
621 9
                foreach ($objectField->args as $arg) {
622 7
                    if ($arg->name === $argName) {
623 7
                        $objectArg = $arg;
624 7
                        break;
625
                    }
626
                }
627
628
                // Assert interface field arg exists on object field.
629 9
                if (! $objectArg) {
630 2
                    $this->reportError(
631 2
                        sprintf(
632 2
                            'Interface field argument %s.%s(%s:) expected but %s.%s does not provide it.',
633 2
                            $iface->name,
634 2
                            $fieldName,
635 2
                            $argName,
636 2
                            $object->name,
637 2
                            $fieldName
638
                        ),
639
                        [
640 2
                            $this->getFieldArgNode($iface, $fieldName, $argName),
641 2
                            $this->getFieldNode($object, $fieldName),
642
                        ]
643
                    );
644 2
                    continue;
645
                }
646
647
                // Assert interface field arg type matches object field arg type.
648
                // (invariant)
649
                // TODO: change to contravariant?
650 7
                if (! TypeComparators::isEqualType($ifaceArg->getType(), $objectArg->getType())) {
651 2
                    $this->reportError(
652 2
                        sprintf(
653 2
                            'Interface field argument %s.%s(%s:) expects type %s but %s.%s(%s:) is type %s.',
654 2
                            $iface->name,
655 2
                            $fieldName,
656 2
                            $argName,
657 2
                            Utils::printSafe($ifaceArg->getType()),
658 2
                            $object->name,
659 2
                            $fieldName,
660 2
                            $argName,
661 2
                            Utils::printSafe($objectArg->getType())
662
                        ),
663
                        [
664 2
                            $this->getFieldArgTypeNode($iface, $fieldName, $argName),
665 7
                            $this->getFieldArgTypeNode($object, $fieldName, $argName),
666
                        ]
667
                    );
668
                }
669
                // TODO: validate default values?
670
            }
671
672
            // Assert additional arguments must not be required.
673 42
            foreach ($objectField->args as $objectArg) {
674 7
                $argName  = $objectArg->name;
675 7
                $ifaceArg = null;
676
677 7
                foreach ($ifaceField->args as $arg) {
678 7
                    if ($arg->name === $argName) {
679 7
                        $ifaceArg = $arg;
680 7
                        break;
681
                    }
682
                }
683
684 7
                if ($ifaceArg || ! ($objectArg->getType() instanceof NonNull)) {
685 7
                    continue;
686
                }
687
688 1
                $this->reportError(
689 1
                    sprintf(
690 1
                        'Object field argument %s.%s(%s:) is of required type %s but is not also provided by the Interface field %s.%s.',
691 1
                        $object->name,
692 1
                        $fieldName,
693 1
                        $argName,
694 1
                        Utils::printSafe($objectArg->getType()),
695 1
                        $iface->name,
696 1
                        $fieldName
697
                    ),
698
                    [
699 1
                        $this->getFieldArgTypeNode($object, $fieldName, $argName),
700 42
                        $this->getFieldNode($iface, $fieldName),
701
                    ]
702
                );
703
            }
704
        }
705 43
    }
706
707 13
    private function validateUnionMembers(UnionType $union)
708
    {
709 13
        $memberTypes = $union->getTypes();
710
711 13
        if (! $memberTypes) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $memberTypes of type GraphQL\Type\Definition\ObjectType[] 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...
712 1
            $this->reportError(
713 1
                sprintf('Union type %s must define one or more member types.', $union->name),
714 1
                $this->getAllNodes($union)
715
            );
716
        }
717
718 13
        $includedTypeNames = [];
719
720 13
        foreach ($memberTypes as $memberType) {
721 12
            if (isset($includedTypeNames[$memberType->name])) {
722 1
                $this->reportError(
723 1
                    sprintf('Union type %s can only include type %s once.', $union->name, $memberType->name),
724 1
                    $this->getUnionMemberTypeNodes($union, $memberType->name)
725
                );
726 1
                continue;
727
            }
728 12
            $includedTypeNames[$memberType->name] = true;
729 12
            if ($memberType instanceof ObjectType) {
730 12
                continue;
731
            }
732
733 2
            $this->reportError(
734 2
                sprintf(
735 2
                    'Union type %s can only include Object types, it cannot include %s.',
736 2
                    $union->name,
737 2
                    Utils::printSafe($memberType)
738
                ),
739 2
                $this->getUnionMemberTypeNodes($union, Utils::printSafe($memberType))
740
            );
741
        }
742 13
    }
743
744
    /**
745
     * @param string $typeName
746
     *
747
     * @return NamedTypeNode[]
748
     */
749 3
    private function getUnionMemberTypeNodes(UnionType $union, $typeName)
750
    {
751
        $subNodes = $this->getAllSubNodes($union, static function ($unionNode) {
752 3
            return $unionNode->types;
753 3
        });
754
755
        return Utils::filter($subNodes, static function ($typeNode) use ($typeName) {
756 3
            return $typeNode->name->value === $typeName;
757 3
        });
758
    }
759
760 91
    private function validateEnumValues(EnumType $enumType)
761
    {
762 91
        $enumValues = $enumType->getValues();
763
764 91
        if (! $enumValues) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $enumValues of type GraphQL\Type\Definition\EnumValueDefinition[] 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...
introduced by
$enumValues is a non-empty array, thus ! $enumValues is always false.
Loading history...
765 1
            $this->reportError(
766 1
                sprintf('Enum type %s must define one or more values.', $enumType->name),
767 1
                $this->getAllNodes($enumType)
768
            );
769
        }
770
771 91
        foreach ($enumValues as $enumValue) {
772 91
            $valueName = $enumValue->name;
773
774
            // Ensure no duplicates
775 91
            $allNodes = $this->getEnumValueNodes($enumType, $valueName);
776 91
            if ($allNodes && count($allNodes) > 1) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $allNodes of type GraphQL\Language\AST\EnumValueDefinitionNode[] 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...
777 1
                $this->reportError(
778 1
                    sprintf('Enum type %s can include value %s only once.', $enumType->name, $valueName),
779 1
                    $allNodes
780
                );
781
            }
782
783
            // Ensure valid name.
784 91
            $this->validateName($enumValue);
785 91
            if ($valueName !== 'true' && $valueName !== 'false' && $valueName !== 'null') {
786 91
                continue;
787
            }
788
789 3
            $this->reportError(
790 3
                sprintf('Enum type %s cannot include value: %s.', $enumType->name, $valueName),
791 3
                $enumValue->astNode
792
            );
793
        }
794 91
    }
795
796
    /**
797
     * @param string $valueName
798
     *
799
     * @return EnumValueDefinitionNode[]
800
     */
801 91
    private function getEnumValueNodes(EnumType $enum, $valueName)
802
    {
803
        $subNodes = $this->getAllSubNodes($enum, static function ($enumNode) {
804 1
            return $enumNode->values;
805 91
        });
806
807
        return Utils::filter($subNodes, static function ($valueNode) use ($valueName) {
808 1
            return $valueNode->name->value === $valueName;
809 91
        });
810
    }
811
812 25
    private function validateInputFields(InputObjectType $inputObj)
813
    {
814 25
        $fieldMap = $inputObj->getFields();
815
816 25
        if (! $fieldMap) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $fieldMap of type GraphQL\Type\Definition\InputObjectField[] 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...
817 1
            $this->reportError(
818 1
                sprintf('Input Object type %s must define one or more fields.', $inputObj->name),
819 1
                $this->getAllNodes($inputObj)
820
            );
821
        }
822
823
        // Ensure the arguments are valid
824 25
        foreach ($fieldMap as $fieldName => $field) {
825
            // Ensure they are named correctly.
826 24
            $this->validateName($field);
827
828
            // TODO: Ensure they are unique per field.
829
830
            // Ensure the type is an input type
831 24
            if (Type::isInputType($field->getType())) {
832 20
                continue;
833
            }
834
835 5
            $this->reportError(
836 5
                sprintf(
837 5
                    'The type of %s.%s must be Input Type but got: %s.',
838 5
                    $inputObj->name,
839 5
                    $fieldName,
840 5
                    Utils::printSafe($field->getType())
841
                ),
842 5
                $field->astNode ? $field->astNode->type : null
843
            );
844
        }
845 25
    }
846
}
847