Skip to content

Commit 90f3576

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 90f3576

21 files changed

+333
-131
lines changed

src/Error/FormattedError.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,8 @@ private static function lpad($len, $str)
173173
*/
174174
public static function createFromException(Throwable $exception, int $debug = DebugFlag::NONE, $internalErrorMessage = null) : array
175175
{
176+
var_dump($exception->getPrevious()->getMessage());
177+
var_dump($exception->getPrevious()->getTraceAsString());
176178
$internalErrorMessage = $internalErrorMessage ?? self::$internalErrorMessage;
177179

178180
if ($exception instanceof ClientAware) {

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: 112 additions & 21 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;
@@ -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,123 @@ 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;
455+
}
456+
}
457+
}
458+
}
459+
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+
$foundImplementations = [];
483+
foreach ($this->getTypeMap() as $type) {
484+
if ($type instanceof InterfaceType) {
485+
if (! isset($foundImplementations[$type->name])) {
486+
$foundImplementations[$type->name] = ['objects' => [], 'interfaces' => []];
487+
}
488+
489+
foreach ($type->getInterfaces() as $iface) {
490+
if (! isset($foundImplementations[$iface->name])) {
491+
$foundImplementations[$iface->name] = ['objects' => [], 'interfaces' => []];
492+
}
493+
$foundImplementations[$iface->name]['interfaces'][] = $type;
494+
}
495+
} elseif ($type instanceof ObjectType) {
496+
foreach ($type->getInterfaces() as $iface) {
497+
if (! isset($foundImplementations[$iface->name])) {
498+
$foundImplementations[$iface->name] = ['objects' => [], 'interfaces' => []];
499+
}
500+
$foundImplementations[$iface->name]['objects'][] = $type;
444501
}
445502
}
446503
}
504+
$this->implementationsMap = array_map(
505+
static function (array $implementations) : InterfaceImplementations {
506+
return new InterfaceImplementations($implementations['objects'], $implementations['interfaces']);
507+
},
508+
$foundImplementations
509+
);
447510
}
448511

449-
return $this->possibleTypeMap;
512+
return $this->implementationsMap;
450513
}
451514

452515
/**
516+
* @deprecated as of 14.4.0 use isSubType instead, will be removed in 15.0.0.
517+
*
453518
* Returns true if object type is concrete type of given abstract type
454519
* (implementation for interfaces and members of union type for unions)
455520
*
456521
* @api
522+
* @codeCoverageIgnore
457523
*/
458-
public function isPossibleType(AbstractType $abstractType, ImplementingType $possibleType) : bool
524+
public function isPossibleType(AbstractType $abstractType, ObjectType $possibleType) : bool
459525
{
460-
if ($abstractType instanceof InterfaceType) {
461-
return $possibleType->implementsInterface($abstractType);
462-
}
526+
return $this->isSubType($abstractType, $possibleType);
527+
}
528+
529+
/**
530+
* Returns true if maybe sub type is a sub type of given abstract type.
531+
*
532+
* @param UnionType|InterfaceType $abstractType
533+
* @param ObjectType|InterfaceType $maybeSubType
534+
*
535+
* @api
536+
*/
537+
public function isSubType(AbstractType $abstractType, ImplementingType $maybeSubType) : bool
538+
{
539+
if (! isset($this->subTypeMap[$abstractType->name])) {
540+
$this->subTypeMap[$abstractType->name] = [];
463541

464-
if ($abstractType instanceof UnionType) {
465-
return $abstractType->isPossibleType($possibleType);
542+
if ($abstractType instanceof UnionType) {
543+
foreach ($abstractType->getTypes() as $type) {
544+
$this->subTypeMap[$abstractType->name][$type->name] = true;
545+
}
546+
} else {
547+
$implementations = $this->getImplementations($abstractType);
548+
foreach ($implementations->objects() as $type) {
549+
$this->subTypeMap[$abstractType->name][$type->name] = true;
550+
}
551+
foreach ($implementations->interfaces() as $type) {
552+
$this->subTypeMap[$abstractType->name][$type->name] = true;
553+
}
554+
}
466555
}
467556

468-
throw InvariantViolation::shouldNotHappen();
557+
return isset($this->subTypeMap[$abstractType->name][$maybeSubType->name]);
469558
}
470559

471560
/**
@@ -492,7 +581,8 @@ public function getAstNode() : ?SchemaDefinitionNode
492581
/**
493582
* Validates schema.
494583
*
495-
* This operation requires full schema scan. Do not use in production environment.
584+
* This operation requires full schema scan. Do not use in production
585+
* environment.
496586
*
497587
* @throws InvariantViolation
498588
*
@@ -532,7 +622,8 @@ public function assertValid()
532622
/**
533623
* Validates schema.
534624
*
535-
* This operation requires full schema scan. Do not use in production environment.
625+
* This operation requires full schema scan. Do not use in production
626+
* environment.
536627
*
537628
* @return InvariantViolation[]|Error[]
538629
*

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

0 commit comments

Comments
 (0)