diff --git a/src/Executor/ReferenceExecutor.php b/src/Executor/ReferenceExecutor.php index b5dea24b4..31b862968 100644 --- a/src/Executor/ReferenceExecutor.php +++ b/src/Executor/ReferenceExecutor.php @@ -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; @@ -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) ); diff --git a/src/Experimental/Executor/Collector.php b/src/Experimental/Executor/Collector.php index fe79eb9d0..dc1b49f73 100644 --- a/src/Experimental/Executor/Collector.php +++ b/src/Experimental/Executor/Collector.php @@ -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; } } @@ -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; } } diff --git a/src/Experimental/Executor/CoroutineExecutor.php b/src/Experimental/Executor/CoroutineExecutor.php index f35cbb882..749810052 100644 --- a/src/Experimental/Executor/CoroutineExecutor.php +++ b/src/Experimental/Executor/CoroutineExecutor.php @@ -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".', diff --git a/src/Language/Parser.php b/src/Language/Parser.php index a85f7cda5..e564253f0 100644 --- a/src/Language/Parser.php +++ b/src/Language/Parser.php @@ -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 ) { diff --git a/src/Type/Schema.php b/src/Type/Schema.php index dfb048e0c..d2fd13644 100644 --- a/src/Type/Schema.php +++ b/src/Type/Schema.php @@ -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; @@ -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, @@ -63,7 +65,14 @@ class Schema * * @var array> */ - private $possibleTypeMap; + private $subTypeMap; + + /** + * Lazily initialised + * + * @var array + */ + private $implementationsMap; /** * True when $resolvedTypes contain all possible schema types @@ -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[] * @@ -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 * @@ -427,8 +438,8 @@ 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) { @@ -436,17 +447,70 @@ private function getPossibleTypeMap() : array 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 + */ + 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; } /** @@ -454,18 +518,42 @@ private function getPossibleTypeMap() : array * (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]); } /** @@ -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 * @@ -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[] * diff --git a/src/Type/SchemaValidationContext.php b/src/Type/SchemaValidationContext.php index bcb65b8d8..cf7423886 100644 --- a/src/Type/SchemaValidationContext.php +++ b/src/Type/SchemaValidationContext.php @@ -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; @@ -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 diff --git a/src/Utils/InterfaceImplementations.php b/src/Utils/InterfaceImplementations.php new file mode 100644 index 000000000..68d4d4c82 --- /dev/null +++ b/src/Utils/InterfaceImplementations.php @@ -0,0 +1,51 @@ +objects = $objects; + $this->interfaces = $interfaces; + } + + /** + * @return ObjectType[] + */ + public function objects() : array + { + return $this->objects; + } + + /** + * @return InterfaceType[] + */ + public function interfaces() : array + { + return $this->interfaces; + } +} diff --git a/src/Utils/TypeComparators.php b/src/Utils/TypeComparators.php index 01210cc2b..7033eee70 100644 --- a/src/Utils/TypeComparators.php +++ b/src/Utils/TypeComparators.php @@ -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 ); @@ -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; } } @@ -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. diff --git a/src/Validator/Rules/PossibleFragmentSpreads.php b/src/Validator/Rules/PossibleFragmentSpreads.php index 184f58bec..24cf12741 100644 --- a/src/Validator/Rules/PossibleFragmentSpreads.php +++ b/src/Validator/Rules/PossibleFragmentSpreads.php @@ -68,12 +68,12 @@ private function doTypesOverlap(Schema $schema, CompositeType $fragType, Composi // Parent type is interface or union, fragment type is object type if ($parentType instanceof AbstractType && $fragType instanceof ObjectType) { - return $schema->isPossibleType($parentType, $fragType); + return $schema->isSubType($parentType, $fragType); } // Parent type is object type, fragment type is interface (or rather rare - union) if ($parentType instanceof ObjectType && $fragType instanceof AbstractType) { - return $schema->isPossibleType($fragType, $parentType); + return $schema->isSubType($fragType, $parentType); } // Both are object types: @@ -117,7 +117,7 @@ private function doTypesOverlap(Schema $schema, CompositeType $fragType, Composi if ($parentType instanceof UnionType && $fragType instanceof UnionType) { foreach ($fragType->getTypes() as $type) { - if ($parentType->isPossibleType($type)) { + if ($parentType->isSubType($type)) { return true; } } diff --git a/tests/Executor/TestClasses/Cat.php b/tests/Executor/TestClasses/Cat.php index b2e93884f..877386503 100644 --- a/tests/Executor/TestClasses/Cat.php +++ b/tests/Executor/TestClasses/Cat.php @@ -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 = []; } } diff --git a/tests/Executor/TestClasses/Dog.php b/tests/Executor/TestClasses/Dog.php index 02c68b451..f60d1f364 100644 --- a/tests/Executor/TestClasses/Dog.php +++ b/tests/Executor/TestClasses/Dog.php @@ -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 = []; } } diff --git a/tests/Executor/UnionInterfaceTest.php b/tests/Executor/UnionInterfaceTest.php index 1d58c5b1d..db9564742 100644 --- a/tests/Executor/UnionInterfaceTest.php +++ b/tests/Executor/UnionInterfaceTest.php @@ -53,7 +53,9 @@ public function setUp() : void $LifeType = new InterfaceType([ 'name' => 'Life', 'fields' => [ - 'progeny' => ['type' => new ListOfType(function () use (&$LifeType) { return $LifeType; })], + 'progeny' => ['type' => new ListOfType(static function () use (&$LifeType) : InterfaceType { + return $LifeType; + })], ], ]); @@ -61,7 +63,9 @@ public function setUp() : void 'name' => 'Mammal', 'interfaces' => [$LifeType], 'fields' => [ - 'progeny' => ['type' => new ListOfType(function () use (&$MammalType) { return $MammalType; })], + 'progeny' => ['type' => new ListOfType(static function () use (&$MammalType) : InterfaceType { + return $MammalType; + })], 'mother' => ['type' => &$MammalType], 'father' => ['type' => &$MammalType], ] @@ -73,7 +77,9 @@ public function setUp() : void 'fields' => [ 'name' => ['type' => Type::string()], 'woofs' => ['type' => Type::boolean()], - 'progeny' => ['type' => new ListOfType(function () use (&$DogType) {return $DogType; })], + 'progeny' => ['type' => new ListOfType(static function () use (&$DogType) : ObjectType { + return $DogType; + })], 'mother' => ['type' => &$DogType], 'father' => ['type' => &$DogType], ], @@ -88,7 +94,9 @@ public function setUp() : void 'fields' => [ 'name' => ['type' => Type::string()], 'meows' => ['type' => Type::boolean()], - 'progeny' => ['type' => new ListOfType(function () use (&$CatType) { return $CatType; })], + 'progeny' => ['type' => new ListOfType(static function () use (&$CatType) : ObjectType { + return $CatType; + })], 'mother' => ['type' => &$CatType], 'father' => ['type' => &$CatType], ], @@ -119,7 +127,9 @@ public function setUp() : void 'name' => ['type' => Type::string()], 'pets' => ['type' => Type::listOf($PetType)], 'friends' => ['type' => Type::listOf($NamedType)], - 'progeny' => ['type' => new ListOfType(function () use (&$PersonType) { return $PersonType; })], + 'progeny' => ['type' => new ListOfType(static function () use (&$PersonType) : ObjectType { + return $PersonType; + })], 'mother' => ['type' => &$PersonType], 'father' => ['type' => &$PersonType], ], @@ -141,8 +151,8 @@ public function setUp() : void $this->odie->mother = new Dog("Odie's Mom", true); $this->odie->mother->progeny = [$this->odie]; - $this->liz = new Person('Liz'); - $this->john = new Person('John', [$this->garfield, $this->odie], [$this->liz, $this->odie]); + $this->liz = new Person('Liz'); + $this->john = new Person('John', [$this->garfield, $this->odie], [$this->liz, $this->odie]); } // Execute: Union and intersection types @@ -210,7 +220,7 @@ enumValues { name } ['name' => 'father'], ], 'interfaces' => [ - ['name' => 'Life'] + ['name' => 'Life'], ], 'possibleTypes' => [ ['name' => 'Person'], @@ -263,12 +273,12 @@ public function testExecutesUsingUnionTypes() : void [ '__typename' => 'Cat', 'name' => 'Garfield', - 'meows' => false + 'meows' => false, ], [ '__typename' => 'Dog', 'name' => 'Odie', - 'woofs' => true + 'woofs' => true, ], ], ], @@ -308,12 +318,12 @@ public function testExecutesUnionTypesWithInlineFragments() : void [ '__typename' => 'Cat', 'name' => 'Garfield', - 'meows' => false + 'meows' => false, ], [ '__typename' => 'Dog', 'name' => 'Odie', - 'woofs' => true + 'woofs' => true, ], ], @@ -496,12 +506,12 @@ public function testAllowsFragmentConditionsToBeAbstractTypes() : void 'friends' => [ [ '__typename' => 'Person', - 'name' => 'Liz' + 'name' => 'Liz', ], [ '__typename' => 'Dog', 'name' => 'Odie', - 'woofs' => true + 'woofs' => true, ], ], ], diff --git a/tests/Language/SchemaParserTest.php b/tests/Language/SchemaParserTest.php index 48666da73..d03e3937a 100644 --- a/tests/Language/SchemaParserTest.php +++ b/tests/Language/SchemaParserTest.php @@ -446,7 +446,6 @@ public function testObjectExtensionDoNotIncludeDescriptions2() : void ); } - /** * @see it('Interface Extension do not include descriptions') */ diff --git a/tests/Type/DefinitionTest.php b/tests/Type/DefinitionTest.php index e4217443f..8e1513f90 100644 --- a/tests/Type/DefinitionTest.php +++ b/tests/Type/DefinitionTest.php @@ -1159,7 +1159,9 @@ public function testAcceptsAnInterfaceTypeWithInterfacesAsAFunctionReturningAnAr $interfaceType = new InterfaceType([ 'name' => 'AnotherInterface', 'fields' => [], - 'interfaces' => function () { return [$this->interfaceType]; }, + 'interfaces' => function () : array { + return [$this->interfaceType]; + }, ]); self::assertSame($this->interfaceType, $interfaceType->getInterfaces()[0]); } diff --git a/tests/Type/ValidationTest.php b/tests/Type/ValidationTest.php index 15680cf9a..74288d66e 100644 --- a/tests/Type/ValidationTest.php +++ b/tests/Type/ValidationTest.php @@ -2672,7 +2672,7 @@ interface ChildInterface implements ParentInterface { 'message' => 'Interface field argument ParentInterface.field(input:) expects type String ' . 'but ChildInterface.field(input:) is type Int.', 'locations' => [['line' => 7, 'column' => 22], ['line' => 11, 'column' => 22]], - ] + ], ] ); } @@ -2707,7 +2707,8 @@ interface ChildInterface implements ParentInterface { 'message' => 'Object field ChildInterface.field includes required argument requiredArg ' . 'that is missing from the Interface field ParentInterface.field.', 'locations' => [['line' => 13, 'column' => 11], ['line' => 7, 'column' => 9]], - ]] + ], + ] ); } @@ -2758,7 +2759,8 @@ interface ChildInterface implements ParentInterface { 'message' => 'Interface field ParentInterface.field expects type [String] ' . 'but ChildInterface.field is type String.', 'locations' => [['line' => 7, 'column' => 16], ['line' => 11, 'column' => 16]], - ]] + ], + ] ); } @@ -2787,7 +2789,8 @@ interface ChildInterface implements ParentInterface { 'message' => 'Interface field ParentInterface.field expects type String ' . 'but ChildInterface.field is type [String].', 'locations' => [['line' => 7, 'column' => 16], ['line' => 11, 'column' => 16]], - ]] + ], + ] ); } @@ -2838,7 +2841,8 @@ interface ChildInterface implements ParentInterface { 'message' => 'Interface field ParentInterface.field expects type String! ' . 'but ChildInterface.field is type String.', 'locations' => [['line' => 7, 'column' => 16], ['line' => 11, 'column' => 16]], - ]] + ], + ] ); } @@ -2871,7 +2875,8 @@ interface ChildInterface implements ParentInterface { 'message' => 'Type ChildInterface must implement SuperInterface ' . 'because it is implemented by ParentInterface.', 'locations' => [['line' => 10, 'column' => 44], ['line' => 14, 'column' => 43]], - ]] + ], + ] ); } @@ -2896,7 +2901,8 @@ interface FooInterface implements FooInterface { 'message' => 'Type FooInterface cannot implement itself ' . 'because it would create a circular reference.', 'locations' => [['line' => 10, 'column' => 44], ['line' => 14, 'column' => 43]], - ]] + ], + ] ); } diff --git a/tests/Utils/BreakingChangesFinderTest.php b/tests/Utils/BreakingChangesFinderTest.php index c681993b2..2d33baf45 100644 --- a/tests/Utils/BreakingChangesFinderTest.php +++ b/tests/Utils/BreakingChangesFinderTest.php @@ -1723,12 +1723,8 @@ public function testShouldDetectInterfacesAddedToTypes() : void */ public function testShouldDetectInterfacesAddedToInterfaces() : void { - $oldInterface = new InterfaceType([ - 'name' => 'OldInterface', - ]); - $newInterface = new InterfaceType([ - 'name' => 'NewInterface', - ]); + $oldInterface = new InterfaceType(['name' => 'OldInterface']); + $newInterface = new InterfaceType(['name' => 'NewInterface']); $oldInterface1 = new InterfaceType([ 'name' => 'Interface1', diff --git a/tests/Utils/SchemaExtenderTest.php b/tests/Utils/SchemaExtenderTest.php index 1c965f894..150f8eaba 100644 --- a/tests/Utils/SchemaExtenderTest.php +++ b/tests/Utils/SchemaExtenderTest.php @@ -83,7 +83,7 @@ public function setUp() : void 'name' => [ 'type' => Type::string()], 'some' => [ 'type' => $AnotherInterfaceType], ]; - } + }, ]); $FooType = new ObjectType([