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 41f5990
Show file tree
Hide file tree
Showing 12 changed files with 216 additions and 61 deletions.
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;
}
}
4 changes: 2 additions & 2 deletions tests/Executor/TestClasses/Cat.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ public function __construct(string $name, bool $meows)
{
$this->name = $name;
$this->meows = $meows;
$this->mother = NULL;
$this->father = NULL;
$this->mother = null;
$this->father = null;
$this->progeny = [];
}
}
4 changes: 2 additions & 2 deletions tests/Executor/TestClasses/Dog.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ public function __construct(string $name, bool $woofs)
{
$this->name = $name;
$this->woofs = $woofs;
$this->mother = NULL;
$this->father = NULL;
$this->mother = null;
$this->father = null;
$this->progeny = [];
}
}
Loading

0 comments on commit 41f5990

Please sign in to comment.