diff --git a/src/Executor/ReferenceExecutor.php b/src/Executor/ReferenceExecutor.php index 303287080..1867cbd7b 100644 --- a/src/Executor/ReferenceExecutor.php +++ b/src/Executor/ReferenceExecutor.php @@ -69,6 +69,12 @@ class ReferenceExecutor implements ExecutorImplementation */ protected \SplObjectStorage $fieldArgsCache; + protected FieldDefinition $schemaMetaFieldDef; + + protected FieldDefinition $typeMetaFieldDef; + + protected FieldDefinition $typeNameMetaFieldDef; + protected function __construct(ExecutionContext $context) { if (! isset(static::$UNDEFINED)) { @@ -701,23 +707,26 @@ protected function resolveField( */ protected function getFieldDef(Schema $schema, ObjectType $parentType, string $fieldName): ?FieldDefinition { - static $schemaMetaFieldDef, $typeMetaFieldDef, $typeNameMetaFieldDef; - $schemaMetaFieldDef ??= Introspection::schemaMetaFieldDef(); - $typeMetaFieldDef ??= Introspection::typeMetaFieldDef(); - $typeNameMetaFieldDef ??= Introspection::typeNameMetaFieldDef(); + $this->schemaMetaFieldDef ??= Introspection::schemaMetaFieldDef(); + $this->typeMetaFieldDef ??= Introspection::typeMetaFieldDef(); + $this->typeNameMetaFieldDef ??= Introspection::typeNameMetaFieldDef(); $queryType = $schema->getQueryType(); - if ($fieldName === $schemaMetaFieldDef->name && $queryType === $parentType) { - return $schemaMetaFieldDef; + if ($fieldName === $this->schemaMetaFieldDef->name + && $queryType === $parentType + ) { + return $this->schemaMetaFieldDef; } - if ($fieldName === $typeMetaFieldDef->name && $queryType === $parentType) { - return $typeMetaFieldDef; + if ($fieldName === $this->typeMetaFieldDef->name + && $queryType === $parentType + ) { + return $this->typeMetaFieldDef; } - if ($fieldName === $typeNameMetaFieldDef->name) { - return $typeNameMetaFieldDef; + if ($fieldName === $this->typeNameMetaFieldDef->name) { + return $this->typeNameMetaFieldDef; } return $parentType->findField($fieldName); diff --git a/src/Type/Definition/Directive.php b/src/Type/Definition/Directive.php index 0a6db42e5..c955ca22e 100644 --- a/src/Type/Definition/Directive.php +++ b/src/Type/Definition/Directive.php @@ -31,9 +31,9 @@ class Directive /** * Lazily initialized. * - * @var array + * @var array|null */ - protected static array $internalDirectives; + protected static ?array $internalDirectives = null; public string $name; @@ -75,14 +75,6 @@ public function __construct(array $config) $this->config = $config; } - /** @throws InvariantViolation */ - public static function includeDirective(): Directive - { - $internal = self::getInternalDirectives(); - - return $internal['include']; - } - /** * @throws InvariantViolation * @@ -90,71 +82,73 @@ public static function includeDirective(): Directive */ public static function getInternalDirectives(): array { - return self::$internalDirectives ??= [ - 'include' => new self([ - 'name' => self::INCLUDE_NAME, - 'description' => 'Directs the executor to include this field or fragment only when the `if` argument is true.', - 'locations' => [ - DirectiveLocation::FIELD, - DirectiveLocation::FRAGMENT_SPREAD, - DirectiveLocation::INLINE_FRAGMENT, - ], - 'args' => [ - self::IF_ARGUMENT_NAME => [ - 'type' => Type::nonNull(Type::boolean()), - 'description' => 'Included when true.', - ], - ], - ]), - 'skip' => new self([ - 'name' => self::SKIP_NAME, - 'description' => 'Directs the executor to skip this field or fragment when the `if` argument is true.', - 'locations' => [ - DirectiveLocation::FIELD, - DirectiveLocation::FRAGMENT_SPREAD, - DirectiveLocation::INLINE_FRAGMENT, - ], - 'args' => [ - self::IF_ARGUMENT_NAME => [ - 'type' => Type::nonNull(Type::boolean()), - 'description' => 'Skipped when true.', - ], - ], - ]), - 'deprecated' => new self([ - 'name' => self::DEPRECATED_NAME, - 'description' => 'Marks an element of a GraphQL schema as no longer supported.', - 'locations' => [ - DirectiveLocation::FIELD_DEFINITION, - DirectiveLocation::ENUM_VALUE, - DirectiveLocation::ARGUMENT_DEFINITION, - DirectiveLocation::INPUT_FIELD_DEFINITION, - ], - 'args' => [ - self::REASON_ARGUMENT_NAME => [ - 'type' => Type::string(), - 'description' => 'Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted using the Markdown syntax, as specified by [CommonMark](https://commonmark.org/).', - 'defaultValue' => self::DEFAULT_DEPRECATION_REASON, - ], - ], - ]), + return [ + self::INCLUDE_NAME => self::includeDirective(), + self::SKIP_NAME => self::skipDirective(), + self::DEPRECATED_NAME => self::deprecatedDirective(), ]; } /** @throws InvariantViolation */ - public static function skipDirective(): Directive + public static function includeDirective(): Directive { - $internal = self::getInternalDirectives(); + return self::$internalDirectives[self::INCLUDE_NAME] ??= new self([ + 'name' => self::INCLUDE_NAME, + 'description' => 'Directs the executor to include this field or fragment only when the `if` argument is true.', + 'locations' => [ + DirectiveLocation::FIELD, + DirectiveLocation::FRAGMENT_SPREAD, + DirectiveLocation::INLINE_FRAGMENT, + ], + 'args' => [ + self::IF_ARGUMENT_NAME => [ + 'type' => Type::nonNull(Type::boolean()), + 'description' => 'Included when true.', + ], + ], + ]); + } - return $internal['skip']; + /** @throws InvariantViolation */ + public static function skipDirective(): Directive + { + return self::$internalDirectives[self::SKIP_NAME] ??= new self([ + 'name' => self::SKIP_NAME, + 'description' => 'Directs the executor to skip this field or fragment when the `if` argument is true.', + 'locations' => [ + DirectiveLocation::FIELD, + DirectiveLocation::FRAGMENT_SPREAD, + DirectiveLocation::INLINE_FRAGMENT, + ], + 'args' => [ + self::IF_ARGUMENT_NAME => [ + 'type' => Type::nonNull(Type::boolean()), + 'description' => 'Skipped when true.', + ], + ], + ]); } /** @throws InvariantViolation */ public static function deprecatedDirective(): Directive { - $internal = self::getInternalDirectives(); - - return $internal['deprecated']; + return self::$internalDirectives[self::DEPRECATED_NAME] ??= new self([ + 'name' => self::DEPRECATED_NAME, + 'description' => 'Marks an element of a GraphQL schema as no longer supported.', + 'locations' => [ + DirectiveLocation::FIELD_DEFINITION, + DirectiveLocation::ENUM_VALUE, + DirectiveLocation::ARGUMENT_DEFINITION, + DirectiveLocation::INPUT_FIELD_DEFINITION, + ], + 'args' => [ + self::REASON_ARGUMENT_NAME => [ + 'type' => Type::string(), + 'description' => 'Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted using the Markdown syntax, as specified by [CommonMark](https://commonmark.org/).', + 'defaultValue' => self::DEFAULT_DEPRECATION_REASON, + ], + ], + ]); } /** @throws InvariantViolation */ @@ -162,4 +156,9 @@ public static function isSpecifiedDirective(Directive $directive): bool { return \array_key_exists($directive->name, self::getInternalDirectives()); } + + public static function resetCachedInstances(): void + { + self::$internalDirectives = null; + } } diff --git a/src/Type/Definition/Type.php b/src/Type/Definition/Type.php index cd734b99c..35224f514 100644 --- a/src/Type/Definition/Type.php +++ b/src/Type/Definition/Type.php @@ -30,8 +30,11 @@ abstract class Type implements \JsonSerializable ...Introspection::TYPE_NAMES, ]; - /** @var array */ - protected static array $standardTypes; + /** @var array|null */ + protected static ?array $standardTypes; + + /** @var array|null */ + protected static ?array $builtInTypes; /** * @api @@ -116,9 +119,7 @@ public static function nonNull($type): NonNull */ public static function builtInTypes(): array { - static $builtInTypes; - - return $builtInTypes ??= \array_merge( + return self::$builtInTypes ??= \array_merge( Introspection::getTypes(), self::getStandardTypes() ); @@ -149,6 +150,11 @@ public static function getStandardTypes(): array */ public static function overrideStandardTypes(array $types): void { + // Reset caches that might contain instances of standard types + static::$builtInTypes = null; + Introspection::resetCachedInstances(); + Directive::resetCachedInstances(); + foreach ($types as $type) { // @phpstan-ignore-next-line generic type is not enforced by PHP if (! $type instanceof ScalarType) { diff --git a/src/Type/Introspection.php b/src/Type/Introspection.php index 9aaa5341a..27dd736f1 100644 --- a/src/Type/Introspection.php +++ b/src/Type/Introspection.php @@ -68,8 +68,8 @@ class Introspection self::DIRECTIVE_LOCATION_ENUM_NAME, ]; - /** @var array */ - private static $map = []; + /** @var array|null */ + protected static ?array $cachedInstances; /** * @param IntrospectionOptions $options @@ -253,7 +253,7 @@ public static function getTypes(): array /** @throws InvariantViolation */ public static function _schema(): ObjectType { - return self::$map[self::SCHEMA_OBJECT_NAME] ??= new ObjectType([ + return self::$cachedInstances[self::SCHEMA_OBJECT_NAME] ??= new ObjectType([ 'name' => self::SCHEMA_OBJECT_NAME, 'isIntrospection' => true, 'description' => 'A GraphQL Schema defines the capabilities of a GraphQL ' @@ -293,7 +293,7 @@ public static function _schema(): ObjectType /** @throws InvariantViolation */ public static function _type(): ObjectType { - return self::$map[self::TYPE_OBJECT_NAME] ??= new ObjectType([ + return self::$cachedInstances[self::TYPE_OBJECT_NAME] ??= new ObjectType([ 'name' => self::TYPE_OBJECT_NAME, 'isIntrospection' => true, 'description' => 'The fundamental unit of any GraphQL Schema is the type. There are ' @@ -444,7 +444,7 @@ public static function _type(): ObjectType /** @throws InvariantViolation */ public static function _typeKind(): EnumType { - return self::$map[self::TYPE_KIND_ENUM_NAME] ??= new EnumType([ + return self::$cachedInstances[self::TYPE_KIND_ENUM_NAME] ??= new EnumType([ 'name' => self::TYPE_KIND_ENUM_NAME, 'isIntrospection' => true, 'description' => 'An enum describing what kind of type a given `__Type` is.', @@ -488,7 +488,7 @@ public static function _typeKind(): EnumType /** @throws InvariantViolation */ public static function _field(): ObjectType { - return self::$map[self::FIELD_OBJECT_NAME] ??= new ObjectType([ + return self::$cachedInstances[self::FIELD_OBJECT_NAME] ??= new ObjectType([ 'name' => self::FIELD_OBJECT_NAME, 'isIntrospection' => true, 'description' => 'Object and Interface types are described by a list of Fields, each of ' @@ -542,7 +542,7 @@ public static function _field(): ObjectType /** @throws InvariantViolation */ public static function _inputValue(): ObjectType { - return self::$map[self::INPUT_VALUE_OBJECT_NAME] ??= new ObjectType([ + return self::$cachedInstances[self::INPUT_VALUE_OBJECT_NAME] ??= new ObjectType([ 'name' => self::INPUT_VALUE_OBJECT_NAME, 'isIntrospection' => true, 'description' => 'Arguments provided to Fields or Directives and the input fields of an ' @@ -600,7 +600,7 @@ public static function _inputValue(): ObjectType /** @throws InvariantViolation */ public static function _enumValue(): ObjectType { - return self::$map[self::ENUM_VALUE_OBJECT_NAME] ??= new ObjectType([ + return self::$cachedInstances[self::ENUM_VALUE_OBJECT_NAME] ??= new ObjectType([ 'name' => self::ENUM_VALUE_OBJECT_NAME, 'isIntrospection' => true, 'description' => 'One possible value for a given Enum. Enum values are unique values, not ' @@ -630,7 +630,7 @@ public static function _enumValue(): ObjectType /** @throws InvariantViolation */ public static function _directive(): ObjectType { - return self::$map[self::DIRECTIVE_OBJECT_NAME] ??= new ObjectType([ + return self::$cachedInstances[self::DIRECTIVE_OBJECT_NAME] ??= new ObjectType([ 'name' => self::DIRECTIVE_OBJECT_NAME, 'isIntrospection' => true, 'description' => 'A Directive provides a way to describe alternate runtime execution and ' @@ -669,7 +669,7 @@ public static function _directive(): ObjectType /** @throws InvariantViolation */ public static function _directiveLocation(): EnumType { - return self::$map[self::DIRECTIVE_LOCATION_ENUM_NAME] ??= new EnumType([ + return self::$cachedInstances[self::DIRECTIVE_LOCATION_ENUM_NAME] ??= new EnumType([ 'name' => self::DIRECTIVE_LOCATION_ENUM_NAME, 'isIntrospection' => true, 'description' => 'A Directive can be adjacent to many parts of the GraphQL language, a ' @@ -758,7 +758,7 @@ public static function _directiveLocation(): EnumType /** @throws InvariantViolation */ public static function schemaMetaFieldDef(): FieldDefinition { - return self::$map[self::SCHEMA_FIELD_NAME] ??= new FieldDefinition([ + return self::$cachedInstances[self::SCHEMA_FIELD_NAME] ??= new FieldDefinition([ 'name' => self::SCHEMA_FIELD_NAME, 'type' => Type::nonNull(self::_schema()), 'description' => 'Access the current type schema of this server.', @@ -770,7 +770,7 @@ public static function schemaMetaFieldDef(): FieldDefinition /** @throws InvariantViolation */ public static function typeMetaFieldDef(): FieldDefinition { - return self::$map[self::TYPE_FIELD_NAME] ??= new FieldDefinition([ + return self::$cachedInstances[self::TYPE_FIELD_NAME] ??= new FieldDefinition([ 'name' => self::TYPE_FIELD_NAME, 'type' => self::_type(), 'description' => 'Request the type information of a single type.', @@ -787,7 +787,7 @@ public static function typeMetaFieldDef(): FieldDefinition /** @throws InvariantViolation */ public static function typeNameMetaFieldDef(): FieldDefinition { - return self::$map[self::TYPE_NAME_FIELD_NAME] ??= new FieldDefinition([ + return self::$cachedInstances[self::TYPE_NAME_FIELD_NAME] ??= new FieldDefinition([ 'name' => self::TYPE_NAME_FIELD_NAME, 'type' => Type::nonNull(Type::string()), 'description' => 'The name of the current Object type at runtime.', @@ -795,4 +795,9 @@ public static function typeNameMetaFieldDef(): FieldDefinition 'resolve' => static fn ($source, array $args, $context, ResolveInfo $info): string => $info->parentType->name, ]); } + + public static function resetCachedInstances(): void + { + self::$cachedInstances = null; + } } diff --git a/tests/Type/StandardTypesTest.php b/tests/Type/StandardTypesTest.php index 500a30e67..6a569f45e 100644 --- a/tests/Type/StandardTypesTest.php +++ b/tests/Type/StandardTypesTest.php @@ -4,9 +4,11 @@ use GraphQL\Error\InvariantViolation; use GraphQL\Type\Definition\CustomScalarType; +use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\ScalarType; use GraphQL\Type\Definition\Type; +use GraphQL\Type\Introspection; use PHPUnit\Framework\TestCase; final class StandardTypesTest extends TestCase @@ -122,6 +124,29 @@ public function testStandardTypesOverrideDoesSanityChecks($notType, string $expe Type::overrideStandardTypes([$notType]); } + public function testCachesShouldResetWhenOverridingStandardTypes(): void + { + $string = Type::string(); + + $typeNameMetaFieldDef = Introspection::typeNameMetaFieldDef(); + self::assertSame($string, Type::getNullableType($typeNameMetaFieldDef->getType())); + + $deprecatedDirective = Directive::deprecatedDirective(); + self::assertSame($string, $deprecatedDirective->args[0]->getType()); + + $newString = self::createCustomScalarType(Type::STRING); + self::assertNotSame($string, $newString); + Type::overrideStandardTypes([$newString]); + + $newTypeNameMetaFieldDef = Introspection::typeNameMetaFieldDef(); + self::assertNotSame($typeNameMetaFieldDef, $newTypeNameMetaFieldDef); + self::assertSame($newString, Type::getNullableType($newTypeNameMetaFieldDef->getType())); + + $newDeprecatedDirective = Directive::deprecatedDirective(); + self::assertNotSame($deprecatedDirective, $newDeprecatedDirective); + self::assertSame($newString, $newDeprecatedDirective->args[0]->getType()); + } + /** @throws InvariantViolation */ private static function createCustomScalarType(string $name): CustomScalarType {