Skip to content

Commit

Permalink
Properly apply Schema changes for interface extension support
Browse files Browse the repository at this point in the history
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
  • Loading branch information
Kingdutch committed Nov 30, 2020
1 parent d525145 commit a8f94b6
Show file tree
Hide file tree
Showing 21 changed files with 305 additions and 135 deletions.
4 changes: 2 additions & 2 deletions src/Executor/ReferenceExecutor.php
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,7 @@ private function doesFragmentConditionMatch(Node $fragment, ObjectType $type) :
return true;
}
if ($conditionalType instanceof AbstractType) {
return $this->exeContext->schema->isPossibleType($conditionalType, $type);
return $this->exeContext->schema->isSubType($conditionalType, $type);
}

return false;
Expand Down Expand Up @@ -1283,7 +1283,7 @@ private function ensureValidRuntimeType(
)
);
}
if (! $this->exeContext->schema->isPossibleType($returnType, $runtimeType)) {
if (! $this->exeContext->schema->isSubType($returnType, $runtimeType)) {
throw new InvariantViolation(
sprintf('Runtime Object type "%s" is not a possible type for "%s".', $runtimeType, $returnType)
);
Expand Down
4 changes: 2 additions & 2 deletions src/Experimental/Executor/Collector.php
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ private function doCollectFields(ObjectType $runtimeType, ?SelectionSetNode $sel
continue;
}
} elseif ($conditionType instanceof AbstractType) {
if (! $this->schema->isPossibleType($conditionType, $runtimeType)) {
if (! $this->schema->isSubType($conditionType, $runtimeType)) {
continue;
}
}
Expand All @@ -269,7 +269,7 @@ private function doCollectFields(ObjectType $runtimeType, ?SelectionSetNode $sel
continue;
}
} elseif ($conditionType instanceof AbstractType) {
if (! $this->schema->isPossibleType($conditionType, $runtimeType)) {
if (! $this->schema->isSubType($conditionType, $runtimeType)) {
continue;
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/Experimental/Executor/CoroutineExecutor.php
Original file line number Diff line number Diff line change
Expand Up @@ -745,7 +745,7 @@ private function completeValue(CoroutineContext $ctx, Type $type, $value, array

$returnValue = null;
goto CHECKED_RETURN;
} elseif (! $this->schema->isPossibleType($type, $objectType)) {
} elseif (! $this->schema->isSubType($type, $objectType)) {
$this->addError(Error::createLocatedError(
new InvariantViolation(sprintf(
'Runtime Object type "%s" is not a possible type for "%s".',
Expand Down
3 changes: 1 addition & 2 deletions src/Language/Parser.php
Original file line number Diff line number Diff line change
Expand Up @@ -1627,8 +1627,7 @@ private function parseInterfaceTypeExtension() : InterfaceTypeExtensionNode
$interfaces = $this->parseImplementsInterfaces();
$directives = $this->parseDirectives(true);
$fields = $this->parseFieldsDefinition();
if (
count($interfaces) === 0 &&
if (count($interfaces) === 0 &&
count($directives) === 0 &&
count($fields) === 0
) {
Expand Down
107 changes: 83 additions & 24 deletions src/Type/Schema.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\UnionType;
use GraphQL\Utils\InterfaceImplementations;
use GraphQL\Utils\TypeInfo;
use GraphQL\Utils\Utils;
use Traversable;
use function array_map;
use function array_values;
use function implode;
use function is_array;
Expand Down Expand Up @@ -63,7 +65,14 @@ class Schema
*
* @var array<string, array<string, ObjectType|UnionType>>
*/
private $possibleTypeMap;
private $subTypeMap;

/**
* Lazily initialised
*
* @var array<string, InterfaceImplementations>
*/
private $implementationsMap;

/**
* True when $resolvedTypes contain all possible schema types
Expand Down Expand Up @@ -417,55 +426,105 @@ public static function resolveType($type) : Type
*/
public function getPossibleTypes(Type $abstractType) : array
{
$possibleTypeMap = $this->getPossibleTypeMap();
return $abstractType instanceof UnionType
? $abstractType->getTypes()
: $this->getImplementations($abstractType)->objects();
}

return array_values($possibleTypeMap[$abstractType->name] ?? []);
/**
* Returns all types that implement a given interface type.
*
* This operations requires full schema scan. Do not use in production environment.
*
* @api
*/
public function getImplementations(InterfaceType $abstractType) : InterfaceImplementations
{
return $this->collectImplementations()[$abstractType->name];
}

/**
* @return array<string, array<string, ObjectType|UnionType>>
* @return array<string, InterfaceImplementations>
*/
private function getPossibleTypeMap() : array
private function collectImplementations() : array
{
if (! isset($this->possibleTypeMap)) {
$this->possibleTypeMap = [];
if (! isset($this->implementationsMap)) {
$foundImplementations = [];
foreach ($this->getTypeMap() as $type) {
if ($type instanceof ObjectType) {
foreach ($type->getInterfaces() as $interface) {
if (! ($interface instanceof InterfaceType)) {
continue;
}
if ($type instanceof InterfaceType) {
if (! isset($foundImplementations[$type->name])) {
$foundImplementations[$type->name] = ['objects' => [], 'interfaces' => []];
}

$this->possibleTypeMap[$interface->name][$type->name] = $type;
foreach ($type->getInterfaces() as $iface) {
if (! isset($foundImplementations[$iface->name])) {
$foundImplementations[$iface->name] = ['objects' => [], 'interfaces' => []];
}
$foundImplementations[$iface->name]['interfaces'][] = $type;
}
} elseif ($type instanceof UnionType) {
foreach ($type->getTypes() as $innerType) {
$this->possibleTypeMap[$type->name][$innerType->name] = $innerType;
} elseif ($type instanceof ObjectType) {
foreach ($type->getInterfaces() as $iface) {
if (! isset($foundImplementations[$iface->name])) {
$foundImplementations[$iface->name] = ['objects' => [], 'interfaces' => []];
}
$foundImplementations[$iface->name]['objects'][] = $type;
}
}
}
$this->implementationsMap = array_map(
static function (array $implementations) : InterfaceImplementations {
return new InterfaceImplementations($implementations['objects'], $implementations['interfaces']);
},
$foundImplementations
);
}

return $this->possibleTypeMap;
return $this->implementationsMap;
}

/**
* @deprecated as of 14.4.0 use isSubType instead, will be removed in 15.0.0.
*
* Returns true if object type is concrete type of given abstract type
* (implementation for interfaces and members of union type for unions)
*
* @api
* @codeCoverageIgnore
*/
public function isPossibleType(AbstractType $abstractType, ImplementingType $possibleType) : bool
public function isPossibleType(AbstractType $abstractType, ObjectType $possibleType) : bool
{
if ($abstractType instanceof InterfaceType) {
return $possibleType->implementsInterface($abstractType);
}
return $this->isSubType($abstractType, $possibleType);
}

if ($abstractType instanceof UnionType) {
return $abstractType->isPossibleType($possibleType);
/**
* Returns true if maybe sub type is a sub type of given abstract type.
*
* @param UnionType|InterfaceType $abstractType
* @param ObjectType|InterfaceType $maybeSubType
*
* @api
*/
public function isSubType(AbstractType $abstractType, ImplementingType $maybeSubType) : bool
{
if (! isset($this->subTypeMap[$abstractType->name])) {
$this->subTypeMap[$abstractType->name] = [];

if ($abstractType instanceof UnionType) {
foreach ($abstractType->getTypes() as $type) {
$this->subTypeMap[$abstractType->name][$type->name] = true;
}
} else {
$implementations = $this->getImplementations($abstractType);
foreach ($implementations->objects() as $type) {
$this->subTypeMap[$abstractType->name][$type->name] = true;
}
foreach ($implementations->interfaces() as $type) {
$this->subTypeMap[$abstractType->name][$type->name] = true;
}
}
}

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

/**
Expand Down
57 changes: 37 additions & 20 deletions src/Type/SchemaValidationContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
use function array_key_exists;
use function array_merge;
use function count;
use function in_array;
use function is_array;
use function is_object;
use function sprintf;
Expand Down Expand Up @@ -676,6 +677,18 @@ private function validateInterfaces(ImplementingType $type)
);
continue;
}

if ($type === $iface) {
$this->reportError(
sprintf(
'Type %s cannot implement itself because it would create a circular reference.',
$type->name
),
$this->getImplementsInterfaceNode($type, $iface)
);
continue;
}

if (isset($ifaceTypeNames[$iface->name])) {
$this->reportError(
sprintf('Type %s can only implement %s once.', $type->name, $iface->name),
Expand Down Expand Up @@ -883,29 +896,33 @@ private function validateTypeImplementsInterface($type, $iface)
* @param ObjectType|InterfaceType $type
* @param InterfaceType $iface
*/
private function validateTypeImplementsAncestors(ImplementingType $type, $iface) {
private function validateTypeImplementsAncestors(ImplementingType $type, $iface)
{
$typeInterfaces = $type->getInterfaces();
foreach ($iface->getInterfaces() as $transitive) {
if (!in_array($transitive, $typeInterfaces)) {
$this->reportError(
$transitive === $type ?
sprintf(
"Type %s cannot implement %s because it would create a circular reference.",
$type->name,
$iface->name
) :
sprintf(
"Type %s must implement %s because it is implemented by %s.",
$type->name,
$transitive->name,
$iface->name
),
array_merge(
$this->getAllImplementsInterfaceNodes($iface, $transitive),
$this->getAllImplementsInterfaceNodes($type, $iface)
)
);
if (in_array($transitive, $typeInterfaces, true)) {
continue;
}

$error = $transitive === $type ?
sprintf(
'Type %s cannot implement %s because it would create a circular reference.',
$type->name,
$iface->name
) :
sprintf(
'Type %s must implement %s because it is implemented by %s.',
$type->name,
$transitive->name,
$iface->name
);
$this->reportError(
$error,
array_merge(
$this->getAllImplementsInterfaceNodes($iface, $transitive),
$this->getAllImplementsInterfaceNodes($type, $iface)
)
);
}
}

Expand Down
36 changes: 22 additions & 14 deletions src/Utils/BuildClientSchema.php
Original file line number Diff line number Diff line change
Expand Up @@ -284,23 +284,35 @@ private function buildScalarDef(array $scalar) : ScalarType
}

/**
* @param array<string, mixed> $object
* @param array<string, mixed> $implementingIntrospection
*/
private function buildObjectDef(array $object) : ObjectType
private function buildImplementationsList(array $implementingIntrospection)
{
if (! array_key_exists('interfaces', $object)) {
throw new InvariantViolation('Introspection result missing interfaces: ' . json_encode($object) . '.');
// TODO: Temprorary workaround until GraphQL ecosystem will fully support
// 'interfaces' on interface types.
if (array_key_exists('interfaces', $implementingIntrospection) &&
$implementingIntrospection['interfaces'] === null &&
$implementingIntrospection['kind'] === TypeKind::INTERFACE) {
return [];
}

if (! array_key_exists('interfaces', $implementingIntrospection)) {
throw new InvariantViolation('Introspection result missing interfaces: ' . json_encode($implementingIntrospection) . '.');
}

return array_map([$this, 'getInterfaceType'], $implementingIntrospection['interfaces']);
}

/**
* @param array<string, mixed> $object
*/
private function buildObjectDef(array $object) : ObjectType
{
return new ObjectType([
'name' => $object['name'],
'description' => $object['description'],
'interfaces' => function () use ($object) : array {
return array_map(
[$this, 'getInterfaceType'],
// Legacy support for interfaces with null as interfaces field
$object['interfaces'] ?? []
);
return $this->buildImplementationsList($object);
},
'fields' => function () use ($object) {
return $this->buildFieldDefMap($object);
Expand All @@ -320,11 +332,7 @@ private function buildInterfaceDef(array $interface) : InterfaceType
return $this->buildFieldDefMap($interface);
},
'interfaces' => function () use ($interface) : array {
return array_map(
[$this, 'getInterfaceType'],
// Legacy support for interfaces with null as interfaces field
$interface['interfaces'] ?? []
);
return $this->buildImplementationsList($interface);
},
]);
}
Expand Down

0 comments on commit a8f94b6

Please sign in to comment.