Failed Conditions
Push — master ( bf4e7d...c70528 )
by Vladimir
09:31
created

SchemaValidationContext::validateRootTypes()   B

Complexity

Conditions 7
Paths 12

Size

Total Lines 31
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 21
CRAP Score 7

Importance

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

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

823
                iterator_to_array(/** @scrutinizer ignore-type */ $enum->astNode->values),
Loading history...
824
                static function (EnumValueDefinitionNode $value) use ($valueName) {
825 1
                    return $value->name->value === $valueName;
826 1
                }
827
            );
828
        }
829
830 88
        return $enum->astNode
0 ignored issues
show
Bug Best Practice introduced by
The expression return $enum->astNode ? ...>astNode->values : null also could return the type GraphQL\Language\AST\NodeList which is incompatible with the documented return type GraphQL\Language\AST\EnumValueDefinitionNode[].
Loading history...
831
            ? $enum->astNode->values
832 88
            : null;
833
    }
834
835 25
    private function validateInputFields(InputObjectType $inputObj)
836
    {
837 25
        $fieldMap = $inputObj->getFields();
838
839 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...
840 1
            $this->reportError(
841 1
                sprintf('Input Object type %s must define one or more fields.', $inputObj->name),
842 1
                $inputObj->astNode
843
            );
844
        }
845
846
        // Ensure the arguments are valid
847 25
        foreach ($fieldMap as $fieldName => $field) {
848
            // Ensure they are named correctly.
849 24
            $this->validateName($field);
850
851
            // TODO: Ensure they are unique per field.
852
853
            // Ensure the type is an input type
854 24
            if (Type::isInputType($field->getType())) {
855 20
                continue;
856
            }
857
858 5
            $this->reportError(
859 5
                sprintf(
860 5
                    'The type of %s.%s must be Input Type but got: %s.',
861 5
                    $inputObj->name,
862 5
                    $fieldName,
863 5
                    Utils::printSafe($field->getType())
864
                ),
865 5
                $field->astNode ? $field->astNode->type : null
866
            );
867
        }
868 25
    }
869
}
870