Skip to content

Commit b0ce9de

Browse files
committed
Properly apply Schema changes for interface extension support
This redoes the work done for the Schema class since it was previously guessed at. It now more closely follows graphql/graphql-js/pull/2084
1 parent d525145 commit b0ce9de

17 files changed

+228
-73
lines changed

src/Executor/ReferenceExecutor.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -448,7 +448,7 @@ private function doesFragmentConditionMatch(Node $fragment, ObjectType $type) :
448448
return true;
449449
}
450450
if ($conditionalType instanceof AbstractType) {
451-
return $this->exeContext->schema->isPossibleType($conditionalType, $type);
451+
return $this->exeContext->schema->isSubType($conditionalType, $type);
452452
}
453453

454454
return false;
@@ -1283,7 +1283,7 @@ private function ensureValidRuntimeType(
12831283
)
12841284
);
12851285
}
1286-
if (! $this->exeContext->schema->isPossibleType($returnType, $runtimeType)) {
1286+
if (! $this->exeContext->schema->isSubType($returnType, $runtimeType)) {
12871287
throw new InvariantViolation(
12881288
sprintf('Runtime Object type "%s" is not a possible type for "%s".', $runtimeType, $returnType)
12891289
);

src/Experimental/Executor/Collector.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ private function doCollectFields(ObjectType $runtimeType, ?SelectionSetNode $sel
244244
continue;
245245
}
246246
} elseif ($conditionType instanceof AbstractType) {
247-
if (! $this->schema->isPossibleType($conditionType, $runtimeType)) {
247+
if (! $this->schema->isSubType($conditionType, $runtimeType)) {
248248
continue;
249249
}
250250
}
@@ -269,7 +269,7 @@ private function doCollectFields(ObjectType $runtimeType, ?SelectionSetNode $sel
269269
continue;
270270
}
271271
} elseif ($conditionType instanceof AbstractType) {
272-
if (! $this->schema->isPossibleType($conditionType, $runtimeType)) {
272+
if (! $this->schema->isSubType($conditionType, $runtimeType)) {
273273
continue;
274274
}
275275
}

src/Experimental/Executor/CoroutineExecutor.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -745,7 +745,7 @@ private function completeValue(CoroutineContext $ctx, Type $type, $value, array
745745

746746
$returnValue = null;
747747
goto CHECKED_RETURN;
748-
} elseif (! $this->schema->isPossibleType($type, $objectType)) {
748+
} elseif (! $this->schema->isSubType($type, $objectType)) {
749749
$this->addError(Error::createLocatedError(
750750
new InvariantViolation(sprintf(
751751
'Runtime Object type "%s" is not a possible type for "%s".',

src/Language/Parser.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1627,8 +1627,7 @@ private function parseInterfaceTypeExtension() : InterfaceTypeExtensionNode
16271627
$interfaces = $this->parseImplementsInterfaces();
16281628
$directives = $this->parseDirectives(true);
16291629
$fields = $this->parseFieldsDefinition();
1630-
if (
1631-
count($interfaces) === 0 &&
1630+
if (count($interfaces) === 0 &&
16321631
count($directives) === 0 &&
16331632
count($fields) === 0
16341633
) {

src/Type/Schema.php

Lines changed: 111 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,12 @@
1717
use GraphQL\Type\Definition\ObjectType;
1818
use GraphQL\Type\Definition\Type;
1919
use GraphQL\Type\Definition\UnionType;
20+
use GraphQL\Utils\InterfaceImplementations;
2021
use GraphQL\Utils\TypeInfo;
2122
use GraphQL\Utils\Utils;
2223
use Traversable;
2324
use function array_values;
25+
use function array_map;
2426
use function implode;
2527
use function is_array;
2628
use function is_callable;
@@ -30,8 +32,8 @@
3032
* Schema Definition (see [related docs](type-system/schema.md))
3133
*
3234
* A Schema is created by supplying the root types of each type of operation:
33-
* query, mutation (optional) and subscription (optional). A schema definition is
34-
* then supplied to the validator and executor. Usage Example:
35+
* query, mutation (optional) and subscription (optional). A schema definition
36+
* is then supplied to the validator and executor. Usage Example:
3537
*
3638
* $schema = new GraphQL\Type\Schema([
3739
* 'query' => $MyAppQueryRootType,
@@ -63,7 +65,14 @@ class Schema
6365
*
6466
* @var array<string, array<string, ObjectType|UnionType>>
6567
*/
66-
private $possibleTypeMap;
68+
private $subTypeMap;
69+
70+
/**
71+
* Lazily initialised
72+
*
73+
* @var array<string, InterfaceImplementations>
74+
*/
75+
private $implementationsMap;
6776

6877
/**
6978
* True when $resolvedTypes contain all possible schema types
@@ -190,10 +199,11 @@ private function resolveAdditionalTypes()
190199
}
191200

192201
/**
193-
* Returns array of all types in this schema. Keys of this array represent type names, values are instances
194-
* of corresponding type definitions
202+
* Returns array of all types in this schema. Keys of this array represent
203+
* type names, values are instances of corresponding type definitions
195204
*
196-
* This operation requires full schema scan. Do not use in production environment.
205+
* This operation requires full schema scan. Do not use in production
206+
* environment.
197207
*
198208
* @return Type[]
199209
*
@@ -407,7 +417,8 @@ public static function resolveType($type) : Type
407417
* Returns all possible concrete types for given abstract type
408418
* (implementations for interfaces and members of union type for unions)
409419
*
410-
* This operation requires full schema scan. Do not use in production environment.
420+
* This operation requires full schema scan. Do not use in production
421+
* environment.
411422
*
412423
* @param InterfaceType|UnionType $abstractType
413424
*
@@ -427,45 +438,122 @@ public function getPossibleTypes(Type $abstractType) : array
427438
*/
428439
private function getPossibleTypeMap() : array
429440
{
430-
if (! isset($this->possibleTypeMap)) {
431-
$this->possibleTypeMap = [];
441+
if (! isset($this->subTypeMap)) {
442+
$this->subTypeMap = [];
432443
foreach ($this->getTypeMap() as $type) {
433444
if ($type instanceof ObjectType) {
434445
foreach ($type->getInterfaces() as $interface) {
435446
if (! ($interface instanceof InterfaceType)) {
436447
continue;
437448
}
438449

439-
$this->possibleTypeMap[$interface->name][$type->name] = $type;
450+
$this->subTypeMap[$interface->name][$type->name] = $type;
440451
}
441452
} elseif ($type instanceof UnionType) {
442453
foreach ($type->getTypes() as $innerType) {
443-
$this->possibleTypeMap[$type->name][$innerType->name] = $innerType;
454+
$this->subTypeMap[$type->name][$innerType->name] = $innerType;
444455
}
445456
}
446457
}
447458
}
448459

449-
return $this->possibleTypeMap;
460+
return $this->subTypeMap;
461+
}
462+
463+
/**
464+
* Returns all types that implement a given interface type.
465+
*
466+
* This operations requires full schema scan. Do not use in production
467+
* environment.
468+
*
469+
* @api
470+
*/
471+
public function getImplementations(InterfaceType $abstractType) : InterfaceImplementations
472+
{
473+
return $this->collectImplementations()[$abstractType->name];
474+
}
475+
476+
/**
477+
* @return array<string, InterfaceImplementations>
478+
*/
479+
private function collectImplementations() : array
480+
{
481+
if (! isset($this->implementationsMap)) {
482+
$this->implementationsMap = [];
483+
$foundImplementations = [];
484+
foreach ($this->getTypeMap() as $type) {
485+
if ($type instanceof InterfaceType) {
486+
if (! isset($foundImplementations[$type->name])) {
487+
$foundImplementations[$type->name] = ['objects' => [], 'interfaces' => []];
488+
}
489+
490+
foreach ($type->getInterfaces() as $iface) {
491+
if (! isset($foundImplementations[$iface->name])) {
492+
$foundImplementations[$iface->name] = ['objects' => [], 'interfaces' => []];
493+
}
494+
$foundImplementations[$iface->name]['interfaces'][] = $type;
495+
}
496+
} elseif ($type instanceof ObjectType) {
497+
foreach ($type->getInterfaces() as $iface) {
498+
if (! isset($foundImplementations[$iface->name])) {
499+
$foundImplementations[$iface->name] = ['objects' => [], 'interfaces' => []];
500+
}
501+
$foundImplementations[$iface->name]['objects'][] = $type;
502+
}
503+
}
504+
}
505+
$this->implementationsMap = array_map(
506+
static function (array $implementations) : InterfaceImplementations {
507+
return new InterfaceImplementations($implementations['objects'], $implementations['interfaces']);
508+
},
509+
$foundImplementations
510+
);
511+
}
512+
513+
return $this->implementationsMap;
450514
}
451515

452516
/**
453517
* Returns true if object type is concrete type of given abstract type
454518
* (implementation for interfaces and members of union type for unions)
455519
*
456520
* @api
521+
* @deprecated use isSubType instead - will be removed in v16.
457522
*/
458-
public function isPossibleType(AbstractType $abstractType, ImplementingType $possibleType) : bool
523+
public function isPossibleType(AbstractType $abstractType, ObjectType $possibleType) : bool
459524
{
460-
if ($abstractType instanceof InterfaceType) {
461-
return $possibleType->implementsInterface($abstractType);
462-
}
525+
return $this->isSubType($abstractType, $possibleType);
526+
}
463527

464-
if ($abstractType instanceof UnionType) {
465-
return $abstractType->isPossibleType($possibleType);
528+
/**
529+
* Returns true if maybe sub type is a sub type of given abstract type.
530+
*
531+
* @param UnionType|ObjectType|InterfaceType $abstractType
532+
* @param ObjectType|InterfaceType $maybeSubType
533+
*
534+
* @api
535+
*/
536+
public function isSubType(AbstractType $abstractType, ImplementingType $maybeSubType) : bool
537+
{
538+
if (! isset($this->subTypeMap[$abstractType->name])) {
539+
$this->subTypeMap[$abstractType->name] = [];
540+
541+
if ($abstractType instanceof UnionType) {
542+
foreach ($abstractType->getTypes() as $type) {
543+
$this->subTypeMap[$abstractType->name][$type->name] = true;
544+
}
545+
} else {
546+
$implementations = $this->getImplementations($abstractType);
547+
foreach ($implementations->objects() as $type) {
548+
$this->subTypeMap[$abstractType->name][$type->name] = true;
549+
}
550+
foreach ($implementations->interfaces() as $type) {
551+
$this->subTypeMap[$abstractType->name][$type->name] = true;
552+
}
553+
}
466554
}
467555

468-
throw InvariantViolation::shouldNotHappen();
556+
return isset($this->subTypeMap[$abstractType->name][$maybeSubType->name]);
469557
}
470558

471559
/**
@@ -492,7 +580,8 @@ public function getAstNode() : ?SchemaDefinitionNode
492580
/**
493581
* Validates schema.
494582
*
495-
* This operation requires full schema scan. Do not use in production environment.
583+
* This operation requires full schema scan. Do not use in production
584+
* environment.
496585
*
497586
* @throws InvariantViolation
498587
*
@@ -532,7 +621,8 @@ public function assertValid()
532621
/**
533622
* Validates schema.
534623
*
535-
* This operation requires full schema scan. Do not use in production environment.
624+
* This operation requires full schema scan. Do not use in production
625+
* environment.
536626
*
537627
* @return InvariantViolation[]|Error[]
538628
*

src/Type/SchemaValidationContext.php

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
use function array_key_exists;
4545
use function array_merge;
4646
use function count;
47+
use function in_array;
4748
use function is_array;
4849
use function is_object;
4950
use function sprintf;
@@ -883,19 +884,20 @@ private function validateTypeImplementsInterface($type, $iface)
883884
* @param ObjectType|InterfaceType $type
884885
* @param InterfaceType $iface
885886
*/
886-
private function validateTypeImplementsAncestors(ImplementingType $type, $iface) {
887+
private function validateTypeImplementsAncestors(ImplementingType $type, $iface)
888+
{
887889
$typeInterfaces = $type->getInterfaces();
888890
foreach ($iface->getInterfaces() as $transitive) {
889-
if (!in_array($transitive, $typeInterfaces)) {
891+
if (! in_array($transitive, $typeInterfaces, TRUE)) {
890892
$this->reportError(
891893
$transitive === $type ?
892894
sprintf(
893-
"Type %s cannot implement %s because it would create a circular reference.",
895+
'Type %s cannot implement %s because it would create a circular reference.',
894896
$type->name,
895897
$iface->name
896898
) :
897899
sprintf(
898-
"Type %s must implement %s because it is implemented by %s.",
900+
'Type %s must implement %s because it is implemented by %s.',
899901
$type->name,
900902
$transitive->name,
901903
$iface->name
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace GraphQL\Utils;
6+
7+
use GraphQL\Type\Definition\InterfaceType;
8+
use GraphQL\Type\Definition\ObjectType;
9+
10+
/**
11+
* A way to track interface implementations.
12+
*
13+
* Distinguishes between implementations by ObjectTypes and InterfaceTypes.
14+
*/
15+
class InterfaceImplementations
16+
{
17+
18+
/** @var ObjectType[] */
19+
private $objects;
20+
21+
/** @var InterfaceType[] */
22+
private $interfaces;
23+
24+
/**
25+
* Create a new InterfaceImplementations instance.
26+
*
27+
* @param ObjectType[]
28+
* @param InterfaceType[]
29+
*/
30+
public function __construct(array $objects, array $interfaces)
31+
{
32+
$this->objects = $objects;
33+
$this->interfaces = $interfaces;
34+
}
35+
36+
/**
37+
* @return ObjectType[]
38+
*/
39+
public function objects() : array
40+
{
41+
return $this->objects;
42+
}
43+
44+
/**
45+
* @return InterfaceType[]
46+
*/
47+
public function interfaces() : array
48+
{
49+
return $this->interfaces;
50+
}
51+
}

src/Utils/TypeComparators.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ public static function isTypeSubTypeOf(Schema $schema, Type $maybeSubType, Type
8686
// possible object or interface type.
8787
return Type::isAbstractType($superType) &&
8888
$maybeSubType instanceof ImplementingType &&
89-
$schema->isPossibleType(
89+
$schema->isSubType(
9090
$superType,
9191
$maybeSubType
9292
);
@@ -115,7 +115,7 @@ public static function doTypesOverlap(Schema $schema, CompositeType $typeA, Comp
115115
// If both types are abstract, then determine if there is any intersection
116116
// between possible concrete types of each.
117117
foreach ($schema->getPossibleTypes($typeA) as $type) {
118-
if ($schema->isPossibleType($typeB, $type)) {
118+
if ($schema->isSubType($typeB, $type)) {
119119
return true;
120120
}
121121
}
@@ -124,12 +124,12 @@ public static function doTypesOverlap(Schema $schema, CompositeType $typeA, Comp
124124
}
125125

126126
// Determine if the latter type is a possible concrete type of the former.
127-
return $schema->isPossibleType($typeA, $typeB);
127+
return $schema->isSubType($typeA, $typeB);
128128
}
129129

130130
if ($typeB instanceof AbstractType) {
131131
// Determine if the former type is a possible concrete type of the latter.
132-
return $schema->isPossibleType($typeB, $typeA);
132+
return $schema->isSubType($typeB, $typeA);
133133
}
134134

135135
// Otherwise the types do not overlap.

0 commit comments

Comments
 (0)