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 b0ce9de
Show file tree
Hide file tree
Showing 17 changed files with 228 additions and 73 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
132 changes: 111 additions & 21 deletions src/Type/Schema.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@
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_values;
use function array_map;
use function implode;
use function is_array;
use function is_callable;
Expand All @@ -30,8 +32,8 @@
* Schema Definition (see [related docs](type-system/schema.md))
*
* A Schema is created by supplying the root types of each type of operation:
* query, mutation (optional) and subscription (optional). A schema definition is
* then supplied to the validator and executor. Usage Example:
* query, mutation (optional) and subscription (optional). A schema definition
* is then supplied to the validator and executor. Usage Example:
*
* $schema = new GraphQL\Type\Schema([
* 'query' => $MyAppQueryRootType,
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 @@ -190,10 +199,11 @@ private function resolveAdditionalTypes()
}

/**
* Returns array of all types in this schema. Keys of this array represent type names, values are instances
* of corresponding type definitions
* Returns array of all types in this schema. Keys of this array represent
* type names, values are instances of corresponding type definitions
*
* This operation requires full schema scan. Do not use in production environment.
* This operation requires full schema scan. Do not use in production
* environment.
*
* @return Type[]
*
Expand Down Expand Up @@ -407,7 +417,8 @@ public static function resolveType($type) : Type
* Returns all possible concrete types for given abstract type
* (implementations for interfaces and members of union type for unions)
*
* This operation requires full schema scan. Do not use in production environment.
* This operation requires full schema scan. Do not use in production
* environment.
*
* @param InterfaceType|UnionType $abstractType
*
Expand All @@ -427,45 +438,122 @@ public function getPossibleTypes(Type $abstractType) : array
*/
private function getPossibleTypeMap() : array
{
if (! isset($this->possibleTypeMap)) {
$this->possibleTypeMap = [];
if (! isset($this->subTypeMap)) {
$this->subTypeMap = [];
foreach ($this->getTypeMap() as $type) {
if ($type instanceof ObjectType) {
foreach ($type->getInterfaces() as $interface) {
if (! ($interface instanceof InterfaceType)) {
continue;
}

$this->possibleTypeMap[$interface->name][$type->name] = $type;
$this->subTypeMap[$interface->name][$type->name] = $type;
}
} elseif ($type instanceof UnionType) {
foreach ($type->getTypes() as $innerType) {
$this->possibleTypeMap[$type->name][$innerType->name] = $innerType;
$this->subTypeMap[$type->name][$innerType->name] = $innerType;
}
}
}
}

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

/**
* 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, InterfaceImplementations>
*/
private function collectImplementations() : array
{
if (! isset($this->implementationsMap)) {
$this->implementationsMap = [];
$foundImplementations = [];
foreach ($this->getTypeMap() as $type) {
if ($type instanceof InterfaceType) {
if (! isset($foundImplementations[$type->name])) {
$foundImplementations[$type->name] = ['objects' => [], 'interfaces' => []];
}

foreach ($type->getInterfaces() as $iface) {
if (! isset($foundImplementations[$iface->name])) {
$foundImplementations[$iface->name] = ['objects' => [], 'interfaces' => []];
}
$foundImplementations[$iface->name]['interfaces'][] = $type;
}
} 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->implementationsMap;
}

/**
* Returns true if object type is concrete type of given abstract type
* (implementation for interfaces and members of union type for unions)
*
* @api
* @deprecated use isSubType instead - will be removed in v16.
*/
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|ObjectType|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 All @@ -492,7 +580,8 @@ public function getAstNode() : ?SchemaDefinitionNode
/**
* Validates schema.
*
* This operation requires full schema scan. Do not use in production environment.
* This operation requires full schema scan. Do not use in production
* environment.
*
* @throws InvariantViolation
*
Expand Down Expand Up @@ -532,7 +621,8 @@ public function assertValid()
/**
* Validates schema.
*
* This operation requires full schema scan. Do not use in production environment.
* This operation requires full schema scan. Do not use in production
* environment.
*
* @return InvariantViolation[]|Error[]
*
Expand Down
10 changes: 6 additions & 4 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 @@ -883,19 +884,20 @@ 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)) {
if (! in_array($transitive, $typeInterfaces, TRUE)) {
$this->reportError(
$transitive === $type ?
sprintf(
"Type %s cannot implement %s because it would create a circular reference.",
'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 %s must implement %s because it is implemented by %s.',
$type->name,
$transitive->name,
$iface->name
Expand Down
51 changes: 51 additions & 0 deletions src/Utils/InterfaceImplementations.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

declare(strict_types=1);

namespace GraphQL\Utils;

use GraphQL\Type\Definition\InterfaceType;
use GraphQL\Type\Definition\ObjectType;

/**
* A way to track interface implementations.
*
* Distinguishes between implementations by ObjectTypes and InterfaceTypes.
*/
class InterfaceImplementations
{

/** @var ObjectType[] */
private $objects;

/** @var InterfaceType[] */
private $interfaces;

/**
* Create a new InterfaceImplementations instance.
*
* @param ObjectType[]
* @param InterfaceType[]
*/
public function __construct(array $objects, array $interfaces)
{
$this->objects = $objects;
$this->interfaces = $interfaces;
}

/**
* @return ObjectType[]
*/
public function objects() : array
{
return $this->objects;
}

/**
* @return InterfaceType[]
*/
public function interfaces() : array
{
return $this->interfaces;
}
}
8 changes: 4 additions & 4 deletions src/Utils/TypeComparators.php
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ public static function isTypeSubTypeOf(Schema $schema, Type $maybeSubType, Type
// possible object or interface type.
return Type::isAbstractType($superType) &&
$maybeSubType instanceof ImplementingType &&
$schema->isPossibleType(
$schema->isSubType(
$superType,
$maybeSubType
);
Expand Down Expand Up @@ -115,7 +115,7 @@ public static function doTypesOverlap(Schema $schema, CompositeType $typeA, Comp
// If both types are abstract, then determine if there is any intersection
// between possible concrete types of each.
foreach ($schema->getPossibleTypes($typeA) as $type) {
if ($schema->isPossibleType($typeB, $type)) {
if ($schema->isSubType($typeB, $type)) {
return true;
}
}
Expand All @@ -124,12 +124,12 @@ public static function doTypesOverlap(Schema $schema, CompositeType $typeA, Comp
}

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

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

// Otherwise the types do not overlap.
Expand Down
Loading

0 comments on commit b0ce9de

Please sign in to comment.