Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automatically reset static caches in Type, Introspection and Directive when overriding standard types #1632

Merged
merged 25 commits into from
Nov 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 19 additions & 10 deletions src/Executor/ReferenceExecutor.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down Expand Up @@ -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);
Expand Down
127 changes: 63 additions & 64 deletions src/Type/Definition/Directive.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ class Directive
/**
* Lazily initialized.
*
* @var array<string, Directive>
* @var array<string, Directive>|null
*/
protected static array $internalDirectives;
protected static ?array $internalDirectives = null;

public string $name;

Expand Down Expand Up @@ -75,91 +75,90 @@ public function __construct(array $config)
$this->config = $config;
}

/** @throws InvariantViolation */
public static function includeDirective(): Directive
{
$internal = self::getInternalDirectives();

return $internal['include'];
}

/**
* @throws InvariantViolation
*
* @return array<string, 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 */
public static function isSpecifiedDirective(Directive $directive): bool
{
return \array_key_exists($directive->name, self::getInternalDirectives());
}

public static function resetCachedInstances(): void
{
self::$internalDirectives = null;
}
}
16 changes: 11 additions & 5 deletions src/Type/Definition/Type.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,11 @@ abstract class Type implements \JsonSerializable
...Introspection::TYPE_NAMES,
];

/** @var array<string, ScalarType> */
protected static array $standardTypes;
/** @var array<string, ScalarType>|null */
protected static ?array $standardTypes;

/** @var array<string, Type&NamedType>|null */
protected static ?array $builtInTypes;

/**
* @api
Expand Down Expand Up @@ -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()
);
Expand Down Expand Up @@ -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) {
Expand Down
31 changes: 18 additions & 13 deletions src/Type/Introspection.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ class Introspection
self::DIRECTIVE_LOCATION_ENUM_NAME,
];

/** @var array<string, mixed> */
private static $map = [];
/** @var array<string, mixed>|null */
protected static ?array $cachedInstances;

/**
* @param IntrospectionOptions $options
Expand Down Expand Up @@ -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 '
Expand Down Expand Up @@ -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 '
Expand Down Expand Up @@ -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.',
Expand Down Expand Up @@ -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 '
Expand Down Expand Up @@ -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 '
Expand Down Expand Up @@ -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 '
Expand Down Expand Up @@ -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 '
Expand Down Expand Up @@ -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 '
Expand Down Expand Up @@ -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.',
Expand All @@ -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.',
Expand All @@ -787,12 +787,17 @@ 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.',
'args' => [],
'resolve' => static fn ($source, array $args, $context, ResolveInfo $info): string => $info->parentType->name,
]);
}

public static function resetCachedInstances(): void
{
self::$cachedInstances = null;
}
}
Loading
Loading