Skip to content

Commit a8f94b6

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 a8f94b6

21 files changed

+305
-135
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: 83 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@
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;
24+
use function array_map;
2325
use function array_values;
2426
use function implode;
2527
use function is_array;
@@ -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
@@ -417,55 +426,105 @@ public static function resolveType($type) : Type
417426
*/
418427
public function getPossibleTypes(Type $abstractType) : array
419428
{
420-
$possibleTypeMap = $this->getPossibleTypeMap();
429+
return $abstractType instanceof UnionType
430+
? $abstractType->getTypes()
431+
: $this->getImplementations($abstractType)->objects();
432+
}
421433

422-
return array_values($possibleTypeMap[$abstractType->name] ?? []);
434+
/**
435+
* Returns all types that implement a given interface type.
436+
*
437+
* This operations requires full schema scan. Do not use in production environment.
438+
*
439+
* @api
440+
*/
441+
public function getImplementations(InterfaceType $abstractType) : InterfaceImplementations
442+
{
443+
return $this->collectImplementations()[$abstractType->name];
423444
}
424445

425446
/**
426-
* @return array<string, array<string, ObjectType|UnionType>>
447+
* @return array<string, InterfaceImplementations>
427448
*/
428-
private function getPossibleTypeMap() : array
449+
private function collectImplementations() : array
429450
{
430-
if (! isset($this->possibleTypeMap)) {
431-
$this->possibleTypeMap = [];
451+
if (! isset($this->implementationsMap)) {
452+
$foundImplementations = [];
432453
foreach ($this->getTypeMap() as $type) {
433-
if ($type instanceof ObjectType) {
434-
foreach ($type->getInterfaces() as $interface) {
435-
if (! ($interface instanceof InterfaceType)) {
436-
continue;
437-
}
454+
if ($type instanceof InterfaceType) {
455+
if (! isset($foundImplementations[$type->name])) {
456+
$foundImplementations[$type->name] = ['objects' => [], 'interfaces' => []];
457+
}
438458

439-
$this->possibleTypeMap[$interface->name][$type->name] = $type;
459+
foreach ($type->getInterfaces() as $iface) {
460+
if (! isset($foundImplementations[$iface->name])) {
461+
$foundImplementations[$iface->name] = ['objects' => [], 'interfaces' => []];
462+
}
463+
$foundImplementations[$iface->name]['interfaces'][] = $type;
440464
}
441-
} elseif ($type instanceof UnionType) {
442-
foreach ($type->getTypes() as $innerType) {
443-
$this->possibleTypeMap[$type->name][$innerType->name] = $innerType;
465+
} elseif ($type instanceof ObjectType) {
466+
foreach ($type->getInterfaces() as $iface) {
467+
if (! isset($foundImplementations[$iface->name])) {
468+
$foundImplementations[$iface->name] = ['objects' => [], 'interfaces' => []];
469+
}
470+
$foundImplementations[$iface->name]['objects'][] = $type;
444471
}
445472
}
446473
}
474+
$this->implementationsMap = array_map(
475+
static function (array $implementations) : InterfaceImplementations {
476+
return new InterfaceImplementations($implementations['objects'], $implementations['interfaces']);
477+
},
478+
$foundImplementations
479+
);
447480
}
448481

449-
return $this->possibleTypeMap;
482+
return $this->implementationsMap;
450483
}
451484

452485
/**
486+
* @deprecated as of 14.4.0 use isSubType instead, will be removed in 15.0.0.
487+
*
453488
* Returns true if object type is concrete type of given abstract type
454489
* (implementation for interfaces and members of union type for unions)
455490
*
456491
* @api
492+
* @codeCoverageIgnore
457493
*/
458-
public function isPossibleType(AbstractType $abstractType, ImplementingType $possibleType) : bool
494+
public function isPossibleType(AbstractType $abstractType, ObjectType $possibleType) : bool
459495
{
460-
if ($abstractType instanceof InterfaceType) {
461-
return $possibleType->implementsInterface($abstractType);
462-
}
496+
return $this->isSubType($abstractType, $possibleType);
497+
}
463498

464-
if ($abstractType instanceof UnionType) {
465-
return $abstractType->isPossibleType($possibleType);
499+
/**
500+
* Returns true if maybe sub type is a sub type of given abstract type.
501+
*
502+
* @param UnionType|InterfaceType $abstractType
503+
* @param ObjectType|InterfaceType $maybeSubType
504+
*
505+
* @api
506+
*/
507+
public function isSubType(AbstractType $abstractType, ImplementingType $maybeSubType) : bool
508+
{
509+
if (! isset($this->subTypeMap[$abstractType->name])) {
510+
$this->subTypeMap[$abstractType->name] = [];
511+
512+
if ($abstractType instanceof UnionType) {
513+
foreach ($abstractType->getTypes() as $type) {
514+
$this->subTypeMap[$abstractType->name][$type->name] = true;
515+
}
516+
} else {
517+
$implementations = $this->getImplementations($abstractType);
518+
foreach ($implementations->objects() as $type) {
519+
$this->subTypeMap[$abstractType->name][$type->name] = true;
520+
}
521+
foreach ($implementations->interfaces() as $type) {
522+
$this->subTypeMap[$abstractType->name][$type->name] = true;
523+
}
524+
}
466525
}
467526

468-
throw InvariantViolation::shouldNotHappen();
527+
return isset($this->subTypeMap[$abstractType->name][$maybeSubType->name]);
469528
}
470529

471530
/**

src/Type/SchemaValidationContext.php

Lines changed: 37 additions & 20 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;
@@ -676,6 +677,18 @@ private function validateInterfaces(ImplementingType $type)
676677
);
677678
continue;
678679
}
680+
681+
if ($type === $iface) {
682+
$this->reportError(
683+
sprintf(
684+
'Type %s cannot implement itself because it would create a circular reference.',
685+
$type->name
686+
),
687+
$this->getImplementsInterfaceNode($type, $iface)
688+
);
689+
continue;
690+
}
691+
679692
if (isset($ifaceTypeNames[$iface->name])) {
680693
$this->reportError(
681694
sprintf('Type %s can only implement %s once.', $type->name, $iface->name),
@@ -883,29 +896,33 @@ private function validateTypeImplementsInterface($type, $iface)
883896
* @param ObjectType|InterfaceType $type
884897
* @param InterfaceType $iface
885898
*/
886-
private function validateTypeImplementsAncestors(ImplementingType $type, $iface) {
899+
private function validateTypeImplementsAncestors(ImplementingType $type, $iface)
900+
{
887901
$typeInterfaces = $type->getInterfaces();
888902
foreach ($iface->getInterfaces() as $transitive) {
889-
if (!in_array($transitive, $typeInterfaces)) {
890-
$this->reportError(
891-
$transitive === $type ?
892-
sprintf(
893-
"Type %s cannot implement %s because it would create a circular reference.",
894-
$type->name,
895-
$iface->name
896-
) :
897-
sprintf(
898-
"Type %s must implement %s because it is implemented by %s.",
899-
$type->name,
900-
$transitive->name,
901-
$iface->name
902-
),
903-
array_merge(
904-
$this->getAllImplementsInterfaceNodes($iface, $transitive),
905-
$this->getAllImplementsInterfaceNodes($type, $iface)
906-
)
907-
);
903+
if (in_array($transitive, $typeInterfaces, true)) {
904+
continue;
908905
}
906+
907+
$error = $transitive === $type ?
908+
sprintf(
909+
'Type %s cannot implement %s because it would create a circular reference.',
910+
$type->name,
911+
$iface->name
912+
) :
913+
sprintf(
914+
'Type %s must implement %s because it is implemented by %s.',
915+
$type->name,
916+
$transitive->name,
917+
$iface->name
918+
);
919+
$this->reportError(
920+
$error,
921+
array_merge(
922+
$this->getAllImplementsInterfaceNodes($iface, $transitive),
923+
$this->getAllImplementsInterfaceNodes($type, $iface)
924+
)
925+
);
909926
}
910927
}
911928

src/Utils/BuildClientSchema.php

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -284,23 +284,35 @@ private function buildScalarDef(array $scalar) : ScalarType
284284
}
285285

286286
/**
287-
* @param array<string, mixed> $object
287+
* @param array<string, mixed> $implementingIntrospection
288288
*/
289-
private function buildObjectDef(array $object) : ObjectType
289+
private function buildImplementationsList(array $implementingIntrospection)
290290
{
291-
if (! array_key_exists('interfaces', $object)) {
292-
throw new InvariantViolation('Introspection result missing interfaces: ' . json_encode($object) . '.');
291+
// TODO: Temprorary workaround until GraphQL ecosystem will fully support
292+
// 'interfaces' on interface types.
293+
if (array_key_exists('interfaces', $implementingIntrospection) &&
294+
$implementingIntrospection['interfaces'] === null &&
295+
$implementingIntrospection['kind'] === TypeKind::INTERFACE) {
296+
return [];
297+
}
298+
299+
if (! array_key_exists('interfaces', $implementingIntrospection)) {
300+
throw new InvariantViolation('Introspection result missing interfaces: ' . json_encode($implementingIntrospection) . '.');
293301
}
294302

303+
return array_map([$this, 'getInterfaceType'], $implementingIntrospection['interfaces']);
304+
}
305+
306+
/**
307+
* @param array<string, mixed> $object
308+
*/
309+
private function buildObjectDef(array $object) : ObjectType
310+
{
295311
return new ObjectType([
296312
'name' => $object['name'],
297313
'description' => $object['description'],
298314
'interfaces' => function () use ($object) : array {
299-
return array_map(
300-
[$this, 'getInterfaceType'],
301-
// Legacy support for interfaces with null as interfaces field
302-
$object['interfaces'] ?? []
303-
);
315+
return $this->buildImplementationsList($object);
304316
},
305317
'fields' => function () use ($object) {
306318
return $this->buildFieldDefMap($object);
@@ -320,11 +332,7 @@ private function buildInterfaceDef(array $interface) : InterfaceType
320332
return $this->buildFieldDefMap($interface);
321333
},
322334
'interfaces' => function () use ($interface) : array {
323-
return array_map(
324-
[$this, 'getInterfaceType'],
325-
// Legacy support for interfaces with null as interfaces field
326-
$interface['interfaces'] ?? []
327-
);
335+
return $this->buildImplementationsList($interface);
328336
},
329337
]);
330338
}

0 commit comments

Comments
 (0)