From 2dfe82856721950ecb973787b8aa63bc6b3eb10a Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Sat, 7 Mar 2026 23:32:15 +0100 Subject: [PATCH 1/8] Introduce BuiltInTypes for per-schema built-in type isolation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Built-in types (scalars, introspection types, directives) were cached as static state shared across all schemas in a process, causing conflicts when multiple schemas need different standard types — e.g. in tests or long-running processes. Add a BuiltInTypes class that holds per-instance lazy caches of all built-in scalars, introspection types, meta field definitions, and directives. Schema owns a BuiltInTypes instance (defaulting to a shared singleton). Existing static methods delegate to the singleton so there is no @api breakage. https://github.com/webonyx/graphql-php/issues/1424 Co-Authored-By: Claude Opus 4.6 --- docs/class-reference.md | 1 + src/Executor/ReferenceExecutor.php | 15 +- src/Type/BuiltInTypes.php | 780 ++++++++++++++++++++++ src/Type/Definition/Directive.php | 77 +-- src/Type/Definition/Type.php | 14 +- src/Type/Introspection.php | 560 +--------------- src/Type/Schema.php | 26 +- src/Type/SchemaConfig.php | 19 + src/Utils/BuildClientSchema.php | 6 +- src/Utils/BuildSchema.php | 12 +- src/Utils/TypeInfo.php | 9 +- src/Validator/Rules/QuerySecurityRule.php | 8 +- tests/Type/BuiltInTypesTest.php | 145 ++++ 13 files changed, 1007 insertions(+), 665 deletions(-) create mode 100644 src/Type/BuiltInTypes.php create mode 100644 tests/Type/BuiltInTypesTest.php diff --git a/docs/class-reference.md b/docs/class-reference.md index 0102d10df..8c2116af5 100644 --- a/docs/class-reference.md +++ b/docs/class-reference.md @@ -724,6 +724,7 @@ Usage example: types?: Types|null, directives?: array|null, typeLoader?: TypeLoader|null, + builtInTypes?: BuiltInTypes|null, assumeValid?: bool|null, astNode?: SchemaDefinitionNode|null, extensionASTNodes?: array|null, diff --git a/src/Executor/ReferenceExecutor.php b/src/Executor/ReferenceExecutor.php index 53c52dd8a..d6b5067bc 100644 --- a/src/Executor/ReferenceExecutor.php +++ b/src/Executor/ReferenceExecutor.php @@ -17,7 +17,6 @@ use GraphQL\Language\AST\SelectionNode; use GraphQL\Language\AST\SelectionSetNode; use GraphQL\Type\Definition\AbstractType; -use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\FieldDefinition; use GraphQL\Type\Definition\InterfaceType; use GraphQL\Type\Definition\LeafType; @@ -28,7 +27,6 @@ use GraphQL\Type\Definition\OutputType; use GraphQL\Type\Definition\ResolveInfo; use GraphQL\Type\Definition\Type; -use GraphQL\Type\Introspection; use GraphQL\Type\Schema; use GraphQL\Type\SchemaValidationContext; use GraphQL\Utils\AST; @@ -466,8 +464,10 @@ protected function shouldIncludeNode(SelectionNode $node): bool { $variableValues = $this->exeContext->variableValues; + $builtIn = $this->exeContext->schema->getBuiltInTypes(); + $skip = Values::getDirectiveValues( - Directive::skipDirective(), + $builtIn->skipDirective(), $node, $variableValues ); @@ -476,7 +476,7 @@ protected function shouldIncludeNode(SelectionNode $node): bool } $include = Values::getDirectiveValues( - Directive::includeDirective(), + $builtIn->includeDirective(), $node, $variableValues ); @@ -684,9 +684,10 @@ protected function resolveField( */ protected function getFieldDef(Schema $schema, ObjectType $parentType, string $fieldName): ?FieldDefinition { - $this->schemaMetaFieldDef ??= Introspection::schemaMetaFieldDef(); - $this->typeMetaFieldDef ??= Introspection::typeMetaFieldDef(); - $this->typeNameMetaFieldDef ??= Introspection::typeNameMetaFieldDef(); + $builtIn = $schema->getBuiltInTypes(); + $this->schemaMetaFieldDef ??= $builtIn->schemaMetaFieldDef(); + $this->typeMetaFieldDef ??= $builtIn->typeMetaFieldDef(); + $this->typeNameMetaFieldDef ??= $builtIn->typeNameMetaFieldDef(); $queryType = $schema->getQueryType(); diff --git a/src/Type/BuiltInTypes.php b/src/Type/BuiltInTypes.php new file mode 100644 index 000000000..553174d58 --- /dev/null +++ b/src/Type/BuiltInTypes.php @@ -0,0 +1,780 @@ + */ + private array $scalars = []; + + private ?ObjectType $schemaTypeInstance = null; + + private ?ObjectType $typeTypeInstance = null; + + private ?EnumType $typeKindTypeInstance = null; + + private ?ObjectType $fieldTypeInstance = null; + + private ?ObjectType $inputValueTypeInstance = null; + + private ?ObjectType $enumValueTypeInstance = null; + + private ?ObjectType $directiveTypeInstance = null; + + private ?EnumType $directiveLocationTypeInstance = null; + + /** @var array */ + private array $metaFieldDefs = []; + + /** @var array */ + private array $directives = []; + + /** @var array */ + private array $standardTypeOverrides; + + /** @param array $standardTypeOverrides */ + public function __construct(array $standardTypeOverrides = []) + { + $this->standardTypeOverrides = $standardTypeOverrides; + } + + public static function standard(): self + { + return self::$standard ??= new self(); + } + + public static function resetStandard(): void + { + self::$standard = null; + } + + public function int(): ScalarType + { + return $this->scalars[Type::INT] ??= $this->standardTypeOverrides[Type::INT] ?? Type::int(); + } + + public function float(): ScalarType + { + return $this->scalars[Type::FLOAT] ??= $this->standardTypeOverrides[Type::FLOAT] ?? Type::float(); + } + + public function string(): ScalarType + { + return $this->scalars[Type::STRING] ??= $this->standardTypeOverrides[Type::STRING] ?? Type::string(); + } + + public function boolean(): ScalarType + { + return $this->scalars[Type::BOOLEAN] ??= $this->standardTypeOverrides[Type::BOOLEAN] ?? Type::boolean(); + } + + public function id(): ScalarType + { + return $this->scalars[Type::ID] ??= $this->standardTypeOverrides[Type::ID] ?? Type::id(); + } + + /** @return array */ + public function standardTypes(): array + { + return [ + Type::INT => $this->int(), + Type::FLOAT => $this->float(), + Type::STRING => $this->string(), + Type::BOOLEAN => $this->boolean(), + Type::ID => $this->id(), + ]; + } + + public function schemaType(): ObjectType + { + return $this->schemaTypeInstance ??= new ObjectType([ // @phpstan-ignore missingType.checkedException (static configuration is known to be correct) + 'name' => Introspection::SCHEMA_OBJECT_NAME, + 'isIntrospection' => true, + 'description' => 'A GraphQL Schema defines the capabilities of a GraphQL ' + . 'server. It exposes all available types and directives on ' + . 'the server, as well as the entry points for query, mutation, and ' + . 'subscription operations.', + 'fields' => [ + 'description' => [ + 'type' => $this->string(), + 'resolve' => static fn (Schema $schema): ?string => $schema->description, + ], + 'types' => [ + 'description' => 'A list of all types supported by this server.', + 'type' => new NonNull(new ListOfType(new NonNull($this->typeType()))), + 'resolve' => static fn (Schema $schema): array => $schema->getTypeMap(), + ], + 'queryType' => [ + 'description' => 'The type that query operations will be rooted at.', + 'type' => new NonNull($this->typeType()), + 'resolve' => static fn (Schema $schema): ?ObjectType => $schema->getQueryType(), + ], + 'mutationType' => [ + 'description' => 'If this server supports mutation, the type that mutation operations will be rooted at.', + 'type' => $this->typeType(), + 'resolve' => static fn (Schema $schema): ?ObjectType => $schema->getMutationType(), + ], + 'subscriptionType' => [ + 'description' => 'If this server support subscription, the type that subscription operations will be rooted at.', + 'type' => $this->typeType(), + 'resolve' => static fn (Schema $schema): ?ObjectType => $schema->getSubscriptionType(), + ], + 'directives' => [ + 'description' => 'A list of all directives supported by this server.', + 'type' => new NonNull(new ListOfType(new NonNull($this->directiveType()))), + 'resolve' => static fn (Schema $schema): array => $schema->getDirectives(), + ], + ], + ]); + } + + public function typeType(): ObjectType + { + return $this->typeTypeInstance ??= new ObjectType([ // @phpstan-ignore missingType.checkedException (static configuration is known to be correct) + 'name' => Introspection::TYPE_OBJECT_NAME, + 'isIntrospection' => true, + 'description' => 'The fundamental unit of any GraphQL Schema is the type. There are ' + . 'many kinds of types in GraphQL as represented by the `__TypeKind` enum.' + . "\n\n" + . 'Depending on the kind of a type, certain fields describe ' + . 'information about that type. Scalar types provide no information ' + . 'beyond a name and description, while Enum types provide their values. ' + . 'Object and Interface types provide the fields they describe. Abstract ' + . 'types, Union and Interface, provide the Object types possible ' + . 'at runtime. List and NonNull types compose other types.', + 'fields' => fn (): array => [ + 'kind' => [ + 'type' => new NonNull($this->typeKindType()), + 'resolve' => static function (Type $type): string { + switch (true) { + case $type instanceof ListOfType: + return TypeKind::LIST; + case $type instanceof NonNull: + return TypeKind::NON_NULL; + case $type instanceof ScalarType: + return TypeKind::SCALAR; + case $type instanceof ObjectType: + return TypeKind::OBJECT; + case $type instanceof EnumType: + return TypeKind::ENUM; + case $type instanceof InputObjectType: + return TypeKind::INPUT_OBJECT; + case $type instanceof InterfaceType: + return TypeKind::INTERFACE; + case $type instanceof UnionType: + return TypeKind::UNION; + default: + $safeType = Utils::printSafe($type); + throw new \Exception("Unknown kind of type: {$safeType}"); + } + }, + ], + 'name' => [ + 'type' => $this->string(), + 'resolve' => static fn (Type $type): ?string => $type instanceof NamedType + ? $type->name + : null, + ], + 'description' => [ + 'type' => $this->string(), + 'resolve' => static fn (Type $type): ?string => $type instanceof NamedType + ? $type->description + : null, + ], + 'fields' => [ + 'type' => new ListOfType(new NonNull($this->fieldType())), + 'args' => [ + 'includeDeprecated' => [ + 'type' => new NonNull($this->boolean()), + 'defaultValue' => false, + ], + ], + 'resolve' => static function (Type $type, $args): ?array { + if ($type instanceof ObjectType || $type instanceof InterfaceType) { + $fields = $type->getVisibleFields(); + + if (! $args['includeDeprecated']) { + return array_filter( + $fields, + static fn (FieldDefinition $field): bool => ! $field->isDeprecated() + ); + } + + return $fields; + } + + return null; + }, + ], + 'interfaces' => [ + 'type' => new ListOfType(new NonNull($this->typeType())), + 'resolve' => static fn ($type): ?array => $type instanceof ObjectType || $type instanceof InterfaceType + ? $type->getInterfaces() + : null, + ], + 'possibleTypes' => [ + 'type' => new ListOfType(new NonNull($this->typeType())), + 'resolve' => static fn ($type, $args, $context, ResolveInfo $info): ?array => $type instanceof InterfaceType || $type instanceof UnionType + ? $info->schema->getPossibleTypes($type) + : null, + ], + 'enumValues' => [ + 'type' => new ListOfType(new NonNull($this->enumValueType())), + 'args' => [ + 'includeDeprecated' => [ + 'type' => new NonNull($this->boolean()), + 'defaultValue' => false, + ], + ], + 'resolve' => static function ($type, $args): ?array { + if ($type instanceof EnumType) { + $values = $type->getValues(); + + if (! $args['includeDeprecated']) { + return array_filter( + $values, + static fn (EnumValueDefinition $value): bool => ! $value->isDeprecated() + ); + } + + return $values; + } + + return null; + }, + ], + 'inputFields' => [ + 'type' => new ListOfType(new NonNull($this->inputValueType())), + 'args' => [ + 'includeDeprecated' => [ + 'type' => new NonNull($this->boolean()), + 'defaultValue' => false, + ], + ], + 'resolve' => static function ($type, $args): ?array { + if ($type instanceof InputObjectType) { + $fields = $type->getFields(); + + if (! $args['includeDeprecated']) { + return array_filter( + $fields, + static fn (InputObjectField $field): bool => ! $field->isDeprecated(), + ); + } + + return $fields; + } + + return null; + }, + ], + 'ofType' => [ + 'type' => $this->typeType(), + 'resolve' => static fn ($type): ?Type => $type instanceof WrappingType + ? $type->getWrappedType() + : null, + ], + 'isOneOf' => [ + 'type' => $this->boolean(), + 'resolve' => static fn ($type): ?bool => $type instanceof InputObjectType + ? $type->isOneOf() + : null, + ], + ], + ]); + } + + public function typeKindType(): EnumType + { + return $this->typeKindTypeInstance ??= new EnumType([ // @phpstan-ignore missingType.checkedException (static configuration is known to be correct) + 'name' => Introspection::TYPE_KIND_ENUM_NAME, + 'isIntrospection' => true, + 'description' => 'An enum describing what kind of type a given `__Type` is.', + 'values' => [ + 'SCALAR' => [ + 'value' => TypeKind::SCALAR, + 'description' => 'Indicates this type is a scalar.', + ], + 'OBJECT' => [ + 'value' => TypeKind::OBJECT, + 'description' => 'Indicates this type is an object. `fields` and `interfaces` are valid fields.', + ], + 'INTERFACE' => [ + 'value' => TypeKind::INTERFACE, + 'description' => 'Indicates this type is an interface. `fields`, `interfaces`, and `possibleTypes` are valid fields.', + ], + 'UNION' => [ + 'value' => TypeKind::UNION, + 'description' => 'Indicates this type is a union. `possibleTypes` is a valid field.', + ], + 'ENUM' => [ + 'value' => TypeKind::ENUM, + 'description' => 'Indicates this type is an enum. `enumValues` is a valid field.', + ], + 'INPUT_OBJECT' => [ + 'value' => TypeKind::INPUT_OBJECT, + 'description' => 'Indicates this type is an input object. `inputFields` is a valid field.', + ], + 'LIST' => [ + 'value' => TypeKind::LIST, + 'description' => 'Indicates this type is a list. `ofType` is a valid field.', + ], + 'NON_NULL' => [ + 'value' => TypeKind::NON_NULL, + 'description' => 'Indicates this type is a non-null. `ofType` is a valid field.', + ], + ], + ]); + } + + public function fieldType(): ObjectType + { + return $this->fieldTypeInstance ??= new ObjectType([ // @phpstan-ignore missingType.checkedException (static configuration is known to be correct) + 'name' => Introspection::FIELD_OBJECT_NAME, + 'isIntrospection' => true, + 'description' => 'Object and Interface types are described by a list of Fields, each of ' + . 'which has a name, potentially a list of arguments, and a return type.', + 'fields' => fn (): array => [ + 'name' => [ + 'type' => new NonNull($this->string()), + 'resolve' => static fn (FieldDefinition $field): string => $field->name, + ], + 'description' => [ + 'type' => $this->string(), + 'resolve' => static fn (FieldDefinition $field): ?string => $field->description, + ], + 'args' => [ + 'type' => new NonNull(new ListOfType(new NonNull($this->inputValueType()))), + 'args' => [ + 'includeDeprecated' => [ + 'type' => new NonNull($this->boolean()), + 'defaultValue' => false, + ], + ], + 'resolve' => static function (FieldDefinition $field, $args): array { + $values = $field->args; + + if (! $args['includeDeprecated']) { + return array_filter( + $values, + static fn (Argument $value): bool => ! $value->isDeprecated(), + ); + } + + return $values; + }, + ], + 'type' => [ + 'type' => new NonNull($this->typeType()), + 'resolve' => static fn (FieldDefinition $field): Type => $field->getType(), + ], + 'isDeprecated' => [ + 'type' => new NonNull($this->boolean()), + 'resolve' => static fn (FieldDefinition $field): bool => $field->isDeprecated(), + ], + 'deprecationReason' => [ + 'type' => $this->string(), + 'resolve' => static fn (FieldDefinition $field): ?string => $field->deprecationReason, + ], + ], + ]); + } + + public function inputValueType(): ObjectType + { + return $this->inputValueTypeInstance ??= new ObjectType([ // @phpstan-ignore missingType.checkedException (static configuration is known to be correct) + 'name' => Introspection::INPUT_VALUE_OBJECT_NAME, + 'isIntrospection' => true, + 'description' => 'Arguments provided to Fields or Directives and the input fields of an ' + . 'InputObject are represented as Input Values which describe their type ' + . 'and optionally a default value.', + 'fields' => fn (): array => [ + 'name' => [ + 'type' => new NonNull($this->string()), + /** @param Argument|InputObjectField $inputValue */ + 'resolve' => static fn ($inputValue): string => $inputValue->name, + ], + 'description' => [ + 'type' => $this->string(), + /** @param Argument|InputObjectField $inputValue */ + 'resolve' => static fn ($inputValue): ?string => $inputValue->description, + ], + 'type' => [ + 'type' => new NonNull($this->typeType()), + /** @param Argument|InputObjectField $inputValue */ + 'resolve' => static fn ($inputValue): Type => $inputValue->getType(), + ], + 'defaultValue' => [ + 'type' => $this->string(), + 'description' => 'A GraphQL-formatted string representing the default value for this input value.', + /** @param Argument|InputObjectField $inputValue */ + 'resolve' => static function ($inputValue): ?string { + if ($inputValue->defaultValueExists()) { + $defaultValueAST = AST::astFromValue($inputValue->defaultValue, $inputValue->getType()); + + if ($defaultValueAST === null) { + $inconvertibleDefaultValue = Utils::printSafe($inputValue->defaultValue); + throw new InvariantViolation("Unable to convert defaultValue of argument {$inputValue->name} into AST: {$inconvertibleDefaultValue}."); + } + + return Printer::doPrint($defaultValueAST); + } + + return null; + }, + ], + 'isDeprecated' => [ + 'type' => new NonNull($this->boolean()), + /** @param Argument|InputObjectField $inputValue */ + 'resolve' => static fn ($inputValue): bool => $inputValue->isDeprecated(), + ], + 'deprecationReason' => [ + 'type' => $this->string(), + /** @param Argument|InputObjectField $inputValue */ + 'resolve' => static fn ($inputValue): ?string => $inputValue->deprecationReason, + ], + ], + ]); + } + + public function enumValueType(): ObjectType + { + return $this->enumValueTypeInstance ??= new ObjectType([ // @phpstan-ignore missingType.checkedException (static configuration is known to be correct) + 'name' => Introspection::ENUM_VALUE_OBJECT_NAME, + 'isIntrospection' => true, + 'description' => 'One possible value for a given Enum. Enum values are unique values, not ' + . 'a placeholder for a string or numeric value. However an Enum value is ' + . 'returned in a JSON response as a string.', + 'fields' => [ + 'name' => [ + 'type' => new NonNull($this->string()), + 'resolve' => static fn (EnumValueDefinition $enumValue): string => $enumValue->name, + ], + 'description' => [ + 'type' => $this->string(), + 'resolve' => static fn (EnumValueDefinition $enumValue): ?string => $enumValue->description, + ], + 'isDeprecated' => [ + 'type' => new NonNull($this->boolean()), + 'resolve' => static fn (EnumValueDefinition $enumValue): bool => $enumValue->isDeprecated(), + ], + 'deprecationReason' => [ + 'type' => $this->string(), + 'resolve' => static fn (EnumValueDefinition $enumValue): ?string => $enumValue->deprecationReason, + ], + ], + ]); + } + + public function directiveType(): ObjectType + { + return $this->directiveTypeInstance ??= new ObjectType([ // @phpstan-ignore missingType.checkedException (static configuration is known to be correct) + 'name' => Introspection::DIRECTIVE_OBJECT_NAME, + 'isIntrospection' => true, + 'description' => 'A Directive provides a way to describe alternate runtime execution and ' + . 'type validation behavior in a GraphQL document.' + . "\n\nIn some cases, you need to provide options to alter GraphQL's " + . 'execution behavior in ways field arguments will not suffice, such as ' + . 'conditionally including or skipping a field. Directives provide this by ' + . 'describing additional information to the executor.', + 'fields' => [ + 'name' => [ + 'type' => new NonNull($this->string()), + 'resolve' => static fn (Directive $directive): string => $directive->name, + ], + 'description' => [ + 'type' => $this->string(), + 'resolve' => static fn (Directive $directive): ?string => $directive->description, + ], + 'isRepeatable' => [ + 'type' => new NonNull($this->boolean()), + 'resolve' => static fn (Directive $directive): bool => $directive->isRepeatable, + ], + 'locations' => [ + 'type' => new NonNull(new ListOfType(new NonNull( + $this->directiveLocationType() + ))), + 'resolve' => static fn (Directive $directive): array => $directive->locations, + ], + 'args' => [ + 'type' => new NonNull(new ListOfType(new NonNull($this->inputValueType()))), + 'args' => [ + 'includeDeprecated' => [ + 'type' => new NonNull($this->boolean()), + 'defaultValue' => false, + ], + ], + 'resolve' => static function (Directive $directive, $args): array { + $values = $directive->args; + + if (! $args['includeDeprecated']) { + return array_filter( + $values, + static fn (Argument $value): bool => ! $value->isDeprecated(), + ); + } + + return $values; + }, + ], + ], + ]); + } + + public function directiveLocationType(): EnumType + { + return $this->directiveLocationTypeInstance ??= new EnumType([ // @phpstan-ignore missingType.checkedException (static configuration is known to be correct) + 'name' => Introspection::DIRECTIVE_LOCATION_ENUM_NAME, + 'isIntrospection' => true, + 'description' => 'A Directive can be adjacent to many parts of the GraphQL language, a ' + . '__DirectiveLocation describes one such possible adjacencies.', + 'values' => [ + 'QUERY' => [ + 'value' => DirectiveLocation::QUERY, + 'description' => 'Location adjacent to a query operation.', + ], + 'MUTATION' => [ + 'value' => DirectiveLocation::MUTATION, + 'description' => 'Location adjacent to a mutation operation.', + ], + 'SUBSCRIPTION' => [ + 'value' => DirectiveLocation::SUBSCRIPTION, + 'description' => 'Location adjacent to a subscription operation.', + ], + 'FIELD' => [ + 'value' => DirectiveLocation::FIELD, + 'description' => 'Location adjacent to a field.', + ], + 'FRAGMENT_DEFINITION' => [ + 'value' => DirectiveLocation::FRAGMENT_DEFINITION, + 'description' => 'Location adjacent to a fragment definition.', + ], + 'FRAGMENT_SPREAD' => [ + 'value' => DirectiveLocation::FRAGMENT_SPREAD, + 'description' => 'Location adjacent to a fragment spread.', + ], + 'INLINE_FRAGMENT' => [ + 'value' => DirectiveLocation::INLINE_FRAGMENT, + 'description' => 'Location adjacent to an inline fragment.', + ], + 'VARIABLE_DEFINITION' => [ + 'value' => DirectiveLocation::VARIABLE_DEFINITION, + 'description' => 'Location adjacent to a variable definition.', + ], + 'SCHEMA' => [ + 'value' => DirectiveLocation::SCHEMA, + 'description' => 'Location adjacent to a schema definition.', + ], + 'SCALAR' => [ + 'value' => DirectiveLocation::SCALAR, + 'description' => 'Location adjacent to a scalar definition.', + ], + 'OBJECT' => [ + 'value' => DirectiveLocation::OBJECT, + 'description' => 'Location adjacent to an object type definition.', + ], + 'FIELD_DEFINITION' => [ + 'value' => DirectiveLocation::FIELD_DEFINITION, + 'description' => 'Location adjacent to a field definition.', + ], + 'ARGUMENT_DEFINITION' => [ + 'value' => DirectiveLocation::ARGUMENT_DEFINITION, + 'description' => 'Location adjacent to an argument definition.', + ], + 'INTERFACE' => [ + 'value' => DirectiveLocation::IFACE, + 'description' => 'Location adjacent to an interface definition.', + ], + 'UNION' => [ + 'value' => DirectiveLocation::UNION, + 'description' => 'Location adjacent to a union definition.', + ], + 'ENUM' => [ + 'value' => DirectiveLocation::ENUM, + 'description' => 'Location adjacent to an enum definition.', + ], + 'ENUM_VALUE' => [ + 'value' => DirectiveLocation::ENUM_VALUE, + 'description' => 'Location adjacent to an enum value definition.', + ], + 'INPUT_OBJECT' => [ + 'value' => DirectiveLocation::INPUT_OBJECT, + 'description' => 'Location adjacent to an input object type definition.', + ], + 'INPUT_FIELD_DEFINITION' => [ + 'value' => DirectiveLocation::INPUT_FIELD_DEFINITION, + 'description' => 'Location adjacent to an input object field definition.', + ], + ], + ]); + } + + /** @return array */ + public function introspectionTypes(): array + { + return [ + Introspection::SCHEMA_OBJECT_NAME => $this->schemaType(), + Introspection::TYPE_OBJECT_NAME => $this->typeType(), + Introspection::DIRECTIVE_OBJECT_NAME => $this->directiveType(), + Introspection::FIELD_OBJECT_NAME => $this->fieldType(), + Introspection::INPUT_VALUE_OBJECT_NAME => $this->inputValueType(), + Introspection::ENUM_VALUE_OBJECT_NAME => $this->enumValueType(), + Introspection::TYPE_KIND_ENUM_NAME => $this->typeKindType(), + Introspection::DIRECTIVE_LOCATION_ENUM_NAME => $this->directiveLocationType(), + ]; + } + + public function schemaMetaFieldDef(): FieldDefinition + { + return $this->metaFieldDefs[Introspection::SCHEMA_FIELD_NAME] ??= new FieldDefinition([ + 'name' => Introspection::SCHEMA_FIELD_NAME, + 'type' => new NonNull($this->schemaType()), + 'description' => 'Access the current type schema of this server.', + 'args' => [], + 'resolve' => static fn ($source, array $args, $context, ResolveInfo $info): Schema => $info->schema, + ]); + } + + public function typeMetaFieldDef(): FieldDefinition + { + return $this->metaFieldDefs[Introspection::TYPE_FIELD_NAME] ??= new FieldDefinition([ + 'name' => Introspection::TYPE_FIELD_NAME, + 'type' => $this->typeType(), + 'description' => 'Request the type information of a single type.', + 'args' => [ + [ + 'name' => 'name', + 'type' => new NonNull($this->string()), + ], + ], + 'resolve' => static fn ($source, array $args, $context, ResolveInfo $info): ?Type => $info->schema->getType($args['name']), + ]); + } + + public function typeNameMetaFieldDef(): FieldDefinition + { + return $this->metaFieldDefs[Introspection::TYPE_NAME_FIELD_NAME] ??= new FieldDefinition([ + 'name' => Introspection::TYPE_NAME_FIELD_NAME, + 'type' => new NonNull($this->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 function includeDirective(): Directive + { + return $this->directives[Directive::INCLUDE_NAME] ??= new Directive([ + 'name' => Directive::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' => [ + Directive::IF_ARGUMENT_NAME => [ + 'type' => new NonNull($this->boolean()), + 'description' => 'Included when true.', + ], + ], + ]); + } + + public function skipDirective(): Directive + { + return $this->directives[Directive::SKIP_NAME] ??= new Directive([ + 'name' => Directive::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' => [ + Directive::IF_ARGUMENT_NAME => [ + 'type' => new NonNull($this->boolean()), + 'description' => 'Skipped when true.', + ], + ], + ]); + } + + public function deprecatedDirective(): Directive + { + return $this->directives[Directive::DEPRECATED_NAME] ??= new Directive([ + 'name' => Directive::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' => [ + Directive::REASON_ARGUMENT_NAME => [ + 'type' => $this->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' => Directive::DEFAULT_DEPRECATION_REASON, + ], + ], + ]); + } + + public function oneOfDirective(): Directive + { + return $this->directives[Directive::ONE_OF_NAME] ??= new Directive([ + 'name' => Directive::ONE_OF_NAME, + 'description' => 'Indicates that an Input Object is a OneOf Input Object (and thus requires exactly one of its fields be provided).', + 'locations' => [ + DirectiveLocation::INPUT_OBJECT, + ], + 'args' => [], + ]); + } + + /** @return array */ + public function directives(): array + { + return [ + Directive::INCLUDE_NAME => $this->includeDirective(), + Directive::SKIP_NAME => $this->skipDirective(), + Directive::DEPRECATED_NAME => $this->deprecatedDirective(), + Directive::ONE_OF_NAME => $this->oneOfDirective(), + ]; + } + + /** @return array */ + public function allTypes(): array + { + return array_merge( + $this->introspectionTypes(), + $this->standardTypes() + ); + } +} diff --git a/src/Type/Definition/Directive.php b/src/Type/Definition/Directive.php index e613b85d3..2b5303d0e 100644 --- a/src/Type/Definition/Directive.php +++ b/src/Type/Definition/Directive.php @@ -3,7 +3,7 @@ namespace GraphQL\Type\Definition; use GraphQL\Language\AST\DirectiveDefinitionNode; -use GraphQL\Language\DirectiveLocation; +use GraphQL\Type\BuiltInTypes; /** * @phpstan-import-type ArgumentListConfig from Argument @@ -28,13 +28,6 @@ class Directive public const REASON_ARGUMENT_NAME = 'reason'; public const ONE_OF_NAME = 'oneOf'; - /** - * Lazily initialized. - * - * @var array|null - */ - protected static ?array $internalDirectives = null; - public string $name; public ?string $description; @@ -78,83 +71,27 @@ public function __construct(array $config) /** @return array */ public static function getInternalDirectives(): array { - return [ - self::INCLUDE_NAME => self::includeDirective(), - self::SKIP_NAME => self::skipDirective(), - self::DEPRECATED_NAME => self::deprecatedDirective(), - self::ONE_OF_NAME => self::oneOfDirective(), - ]; + return BuiltInTypes::standard()->directives(); } public static function includeDirective(): Directive { - 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 BuiltInTypes::standard()->includeDirective(); } 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.', - ], - ], - ]); + return BuiltInTypes::standard()->skipDirective(); } public static function deprecatedDirective(): Directive { - 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, - ], - ], - ]); + return BuiltInTypes::standard()->deprecatedDirective(); } public static function oneOfDirective(): Directive { - return self::$internalDirectives[self::ONE_OF_NAME] ??= new self([ - 'name' => self::ONE_OF_NAME, - 'description' => 'Indicates that an Input Object is a OneOf Input Object (and thus requires exactly one of its fields be provided).', - 'locations' => [ - DirectiveLocation::INPUT_OBJECT, - ], - 'args' => [], - ]); + return BuiltInTypes::standard()->oneOfDirective(); } public static function isSpecifiedDirective(Directive $directive): bool @@ -164,6 +101,6 @@ public static function isSpecifiedDirective(Directive $directive): bool public static function resetCachedInstances(): void { - self::$internalDirectives = null; + BuiltInTypes::resetStandard(); } } diff --git a/src/Type/Definition/Type.php b/src/Type/Definition/Type.php index 8dee72b8d..dfee70dbf 100644 --- a/src/Type/Definition/Type.php +++ b/src/Type/Definition/Type.php @@ -3,6 +3,7 @@ namespace GraphQL\Type\Definition; use GraphQL\Error\InvariantViolation; +use GraphQL\Type\BuiltInTypes; use GraphQL\Type\Introspection; use GraphQL\Utils\Utils; @@ -33,9 +34,6 @@ abstract class Type implements \JsonSerializable /** @var array|null */ protected static ?array $standardTypes; - /** @var array|null */ - protected static ?array $builtInTypes; - /** * Returns the registered or default standard Int type. * @@ -125,10 +123,7 @@ public static function nonNull($type): NonNull */ public static function builtInTypes(): array { - return self::$builtInTypes ??= array_merge( - Introspection::getTypes(), - self::getStandardTypes() - ); + return BuiltInTypes::standard()->allTypes(); } /** @@ -156,10 +151,7 @@ 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(); + BuiltInTypes::resetStandard(); foreach ($types as $type) { // @phpstan-ignore-next-line generic type is not enforced by PHP diff --git a/src/Type/Introspection.php b/src/Type/Introspection.php index 806658611..0f1fd476a 100644 --- a/src/Type/Introspection.php +++ b/src/Type/Introspection.php @@ -4,26 +4,11 @@ use GraphQL\Error\InvariantViolation; use GraphQL\GraphQL; -use GraphQL\Language\DirectiveLocation; -use GraphQL\Language\Printer; -use GraphQL\Type\Definition\Argument; -use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\EnumType; -use GraphQL\Type\Definition\EnumValueDefinition; use GraphQL\Type\Definition\FieldDefinition; -use GraphQL\Type\Definition\InputObjectField; -use GraphQL\Type\Definition\InputObjectType; -use GraphQL\Type\Definition\InterfaceType; -use GraphQL\Type\Definition\ListOfType; use GraphQL\Type\Definition\NamedType; -use GraphQL\Type\Definition\NonNull; use GraphQL\Type\Definition\ObjectType; -use GraphQL\Type\Definition\ResolveInfo; -use GraphQL\Type\Definition\ScalarType; use GraphQL\Type\Definition\Type; -use GraphQL\Type\Definition\UnionType; -use GraphQL\Type\Definition\WrappingType; -use GraphQL\Utils\AST; use GraphQL\Utils\Utils; /** @@ -73,9 +58,6 @@ class Introspection self::DIRECTIVE_LOCATION_ENUM_NAME, ]; - /** @var array|null */ - protected static ?array $cachedInstances; - /** * @param IntrospectionOptions $options * @@ -253,582 +235,66 @@ public static function isIntrospectionType(NamedType $type): bool /** @return array */ public static function getTypes(): array { - return [ - self::SCHEMA_OBJECT_NAME => self::_schema(), - self::TYPE_OBJECT_NAME => self::_type(), - self::DIRECTIVE_OBJECT_NAME => self::_directive(), - self::FIELD_OBJECT_NAME => self::_field(), - self::INPUT_VALUE_OBJECT_NAME => self::_inputValue(), - self::ENUM_VALUE_OBJECT_NAME => self::_enumValue(), - self::TYPE_KIND_ENUM_NAME => self::_typeKind(), - self::DIRECTIVE_LOCATION_ENUM_NAME => self::_directiveLocation(), - ]; + return BuiltInTypes::standard()->introspectionTypes(); } public static function _schema(): ObjectType { - return self::$cachedInstances[self::SCHEMA_OBJECT_NAME] ??= new ObjectType([ // @phpstan-ignore missingType.checkedException (static configuration is known to be correct) - 'name' => self::SCHEMA_OBJECT_NAME, - 'isIntrospection' => true, - 'description' => 'A GraphQL Schema defines the capabilities of a GraphQL ' - . 'server. It exposes all available types and directives on ' - . 'the server, as well as the entry points for query, mutation, and ' - . 'subscription operations.', - 'fields' => [ - 'description' => [ - 'type' => Type::string(), - 'resolve' => static fn (Schema $schema): ?string => $schema->description, - ], - 'types' => [ - 'description' => 'A list of all types supported by this server.', - 'type' => new NonNull(new ListOfType(new NonNull(self::_type()))), - 'resolve' => static fn (Schema $schema): array => $schema->getTypeMap(), - ], - 'queryType' => [ - 'description' => 'The type that query operations will be rooted at.', - 'type' => new NonNull(self::_type()), - 'resolve' => static fn (Schema $schema): ?ObjectType => $schema->getQueryType(), - ], - 'mutationType' => [ - 'description' => 'If this server supports mutation, the type that mutation operations will be rooted at.', - 'type' => self::_type(), - 'resolve' => static fn (Schema $schema): ?ObjectType => $schema->getMutationType(), - ], - 'subscriptionType' => [ - 'description' => 'If this server support subscription, the type that subscription operations will be rooted at.', - 'type' => self::_type(), - 'resolve' => static fn (Schema $schema): ?ObjectType => $schema->getSubscriptionType(), - ], - 'directives' => [ - 'description' => 'A list of all directives supported by this server.', - 'type' => Type::nonNull(Type::listOf(Type::nonNull(self::_directive()))), - 'resolve' => static fn (Schema $schema): array => $schema->getDirectives(), - ], - ], - ]); + return BuiltInTypes::standard()->schemaType(); } public static function _type(): ObjectType { - return self::$cachedInstances[self::TYPE_OBJECT_NAME] ??= new ObjectType([ // @phpstan-ignore missingType.checkedException (static configuration is known to be correct) - 'name' => self::TYPE_OBJECT_NAME, - 'isIntrospection' => true, - 'description' => 'The fundamental unit of any GraphQL Schema is the type. There are ' - . 'many kinds of types in GraphQL as represented by the `__TypeKind` enum.' - . "\n\n" - . 'Depending on the kind of a type, certain fields describe ' - . 'information about that type. Scalar types provide no information ' - . 'beyond a name and description, while Enum types provide their values. ' - . 'Object and Interface types provide the fields they describe. Abstract ' - . 'types, Union and Interface, provide the Object types possible ' - . 'at runtime. List and NonNull types compose other types.', - 'fields' => static fn (): array => [ - 'kind' => [ - 'type' => Type::nonNull(self::_typeKind()), - 'resolve' => static function (Type $type): string { - switch (true) { - case $type instanceof ListOfType: - return TypeKind::LIST; - case $type instanceof NonNull: - return TypeKind::NON_NULL; - case $type instanceof ScalarType: - return TypeKind::SCALAR; - case $type instanceof ObjectType: - return TypeKind::OBJECT; - case $type instanceof EnumType: - return TypeKind::ENUM; - case $type instanceof InputObjectType: - return TypeKind::INPUT_OBJECT; - case $type instanceof InterfaceType: - return TypeKind::INTERFACE; - case $type instanceof UnionType: - return TypeKind::UNION; - default: - $safeType = Utils::printSafe($type); - throw new \Exception("Unknown kind of type: {$safeType}"); - } - }, - ], - 'name' => [ - 'type' => Type::string(), - 'resolve' => static fn (Type $type): ?string => $type instanceof NamedType - ? $type->name - : null, - ], - 'description' => [ - 'type' => Type::string(), - 'resolve' => static fn (Type $type): ?string => $type instanceof NamedType - ? $type->description - : null, - ], - 'fields' => [ - 'type' => Type::listOf(Type::nonNull(self::_field())), - 'args' => [ - 'includeDeprecated' => [ - 'type' => Type::nonNull(Type::boolean()), - 'defaultValue' => false, - ], - ], - 'resolve' => static function (Type $type, $args): ?array { - if ($type instanceof ObjectType || $type instanceof InterfaceType) { - $fields = $type->getVisibleFields(); - - if (! $args['includeDeprecated']) { - return array_filter( - $fields, - static fn (FieldDefinition $field): bool => ! $field->isDeprecated() - ); - } - - return $fields; - } - - return null; - }, - ], - 'interfaces' => [ - 'type' => Type::listOf(Type::nonNull(self::_type())), - 'resolve' => static fn ($type): ?array => $type instanceof ObjectType || $type instanceof InterfaceType - ? $type->getInterfaces() - : null, - ], - 'possibleTypes' => [ - 'type' => Type::listOf(Type::nonNull(self::_type())), - 'resolve' => static fn ($type, $args, $context, ResolveInfo $info): ?array => $type instanceof InterfaceType || $type instanceof UnionType - ? $info->schema->getPossibleTypes($type) - : null, - ], - 'enumValues' => [ - 'type' => Type::listOf(Type::nonNull(self::_enumValue())), - 'args' => [ - 'includeDeprecated' => [ - 'type' => Type::nonNull(Type::boolean()), - 'defaultValue' => false, - ], - ], - 'resolve' => static function ($type, $args): ?array { - if ($type instanceof EnumType) { - $values = $type->getValues(); - - if (! $args['includeDeprecated']) { - return array_filter( - $values, - static fn (EnumValueDefinition $value): bool => ! $value->isDeprecated() - ); - } - - return $values; - } - - return null; - }, - ], - 'inputFields' => [ - 'type' => Type::listOf(Type::nonNull(self::_inputValue())), - 'args' => [ - 'includeDeprecated' => [ - 'type' => Type::nonNull(Type::boolean()), - 'defaultValue' => false, - ], - ], - 'resolve' => static function ($type, $args): ?array { - if ($type instanceof InputObjectType) { - $fields = $type->getFields(); - - if (! $args['includeDeprecated']) { - return array_filter( - $fields, - static fn (InputObjectField $field): bool => ! $field->isDeprecated(), - ); - } - - return $fields; - } - - return null; - }, - ], - 'ofType' => [ - 'type' => self::_type(), - 'resolve' => static fn ($type): ?Type => $type instanceof WrappingType - ? $type->getWrappedType() - : null, - ], - 'isOneOf' => [ - 'type' => Type::boolean(), - 'resolve' => static fn ($type): ?bool => $type instanceof InputObjectType - ? $type->isOneOf() - : null, - ], - ], - ]); + return BuiltInTypes::standard()->typeType(); } public static function _typeKind(): EnumType { - return self::$cachedInstances[self::TYPE_KIND_ENUM_NAME] ??= new EnumType([ // @phpstan-ignore missingType.checkedException (static configuration is known to be correct) - 'name' => self::TYPE_KIND_ENUM_NAME, - 'isIntrospection' => true, - 'description' => 'An enum describing what kind of type a given `__Type` is.', - 'values' => [ - 'SCALAR' => [ - 'value' => TypeKind::SCALAR, - 'description' => 'Indicates this type is a scalar.', - ], - 'OBJECT' => [ - 'value' => TypeKind::OBJECT, - 'description' => 'Indicates this type is an object. `fields` and `interfaces` are valid fields.', - ], - 'INTERFACE' => [ - 'value' => TypeKind::INTERFACE, - 'description' => 'Indicates this type is an interface. `fields`, `interfaces`, and `possibleTypes` are valid fields.', - ], - 'UNION' => [ - 'value' => TypeKind::UNION, - 'description' => 'Indicates this type is a union. `possibleTypes` is a valid field.', - ], - 'ENUM' => [ - 'value' => TypeKind::ENUM, - 'description' => 'Indicates this type is an enum. `enumValues` is a valid field.', - ], - 'INPUT_OBJECT' => [ - 'value' => TypeKind::INPUT_OBJECT, - 'description' => 'Indicates this type is an input object. `inputFields` is a valid field.', - ], - 'LIST' => [ - 'value' => TypeKind::LIST, - 'description' => 'Indicates this type is a list. `ofType` is a valid field.', - ], - 'NON_NULL' => [ - 'value' => TypeKind::NON_NULL, - 'description' => 'Indicates this type is a non-null. `ofType` is a valid field.', - ], - ], - ]); + return BuiltInTypes::standard()->typeKindType(); } public static function _field(): ObjectType { - return self::$cachedInstances[self::FIELD_OBJECT_NAME] ??= new ObjectType([ // @phpstan-ignore missingType.checkedException (static configuration is known to be correct) - 'name' => self::FIELD_OBJECT_NAME, - 'isIntrospection' => true, - 'description' => 'Object and Interface types are described by a list of Fields, each of ' - . 'which has a name, potentially a list of arguments, and a return type.', - 'fields' => static fn (): array => [ - 'name' => [ - 'type' => Type::nonNull(Type::string()), - 'resolve' => static fn (FieldDefinition $field): string => $field->name, - ], - 'description' => [ - 'type' => Type::string(), - 'resolve' => static fn (FieldDefinition $field): ?string => $field->description, - ], - 'args' => [ - 'type' => Type::nonNull(Type::listOf(Type::nonNull(self::_inputValue()))), - 'args' => [ - 'includeDeprecated' => [ - 'type' => Type::nonNull(Type::boolean()), - 'defaultValue' => false, - ], - ], - 'resolve' => static function (FieldDefinition $field, $args): array { - $values = $field->args; - - if (! $args['includeDeprecated']) { - return array_filter( - $values, - static fn (Argument $value): bool => ! $value->isDeprecated(), - ); - } - - return $values; - }, - ], - 'type' => [ - 'type' => Type::nonNull(self::_type()), - 'resolve' => static fn (FieldDefinition $field): Type => $field->getType(), - ], - 'isDeprecated' => [ - 'type' => Type::nonNull(Type::boolean()), - 'resolve' => static fn (FieldDefinition $field): bool => $field->isDeprecated(), - ], - 'deprecationReason' => [ - 'type' => Type::string(), - 'resolve' => static fn (FieldDefinition $field): ?string => $field->deprecationReason, - ], - ], - ]); + return BuiltInTypes::standard()->fieldType(); } public static function _inputValue(): ObjectType { - return self::$cachedInstances[self::INPUT_VALUE_OBJECT_NAME] ??= new ObjectType([ // @phpstan-ignore missingType.checkedException (static configuration is known to be correct) - 'name' => self::INPUT_VALUE_OBJECT_NAME, - 'isIntrospection' => true, - 'description' => 'Arguments provided to Fields or Directives and the input fields of an ' - . 'InputObject are represented as Input Values which describe their type ' - . 'and optionally a default value.', - 'fields' => static fn (): array => [ - 'name' => [ - 'type' => Type::nonNull(Type::string()), - /** @param Argument|InputObjectField $inputValue */ - 'resolve' => static fn ($inputValue): string => $inputValue->name, - ], - 'description' => [ - 'type' => Type::string(), - /** @param Argument|InputObjectField $inputValue */ - 'resolve' => static fn ($inputValue): ?string => $inputValue->description, - ], - 'type' => [ - 'type' => Type::nonNull(self::_type()), - /** @param Argument|InputObjectField $inputValue */ - 'resolve' => static fn ($inputValue): Type => $inputValue->getType(), - ], - 'defaultValue' => [ - 'type' => Type::string(), - 'description' => 'A GraphQL-formatted string representing the default value for this input value.', - /** @param Argument|InputObjectField $inputValue */ - 'resolve' => static function ($inputValue): ?string { - if ($inputValue->defaultValueExists()) { - $defaultValueAST = AST::astFromValue($inputValue->defaultValue, $inputValue->getType()); - - if ($defaultValueAST === null) { - $inconvertibleDefaultValue = Utils::printSafe($inputValue->defaultValue); - throw new InvariantViolation("Unable to convert defaultValue of argument {$inputValue->name} into AST: {$inconvertibleDefaultValue}."); - } - - return Printer::doPrint($defaultValueAST); - } - - return null; - }, - ], - 'isDeprecated' => [ - 'type' => Type::nonNull(Type::boolean()), - /** @param Argument|InputObjectField $inputValue */ - 'resolve' => static fn ($inputValue): bool => $inputValue->isDeprecated(), - ], - 'deprecationReason' => [ - 'type' => Type::string(), - /** @param Argument|InputObjectField $inputValue */ - 'resolve' => static fn ($inputValue): ?string => $inputValue->deprecationReason, - ], - ], - ]); + return BuiltInTypes::standard()->inputValueType(); } public static function _enumValue(): ObjectType { - return self::$cachedInstances[self::ENUM_VALUE_OBJECT_NAME] ??= new ObjectType([ // @phpstan-ignore missingType.checkedException (static configuration is known to be correct) - 'name' => self::ENUM_VALUE_OBJECT_NAME, - 'isIntrospection' => true, - 'description' => 'One possible value for a given Enum. Enum values are unique values, not ' - . 'a placeholder for a string or numeric value. However an Enum value is ' - . 'returned in a JSON response as a string.', - 'fields' => [ - 'name' => [ - 'type' => Type::nonNull(Type::string()), - 'resolve' => static fn (EnumValueDefinition $enumValue): string => $enumValue->name, - ], - 'description' => [ - 'type' => Type::string(), - 'resolve' => static fn (EnumValueDefinition $enumValue): ?string => $enumValue->description, - ], - 'isDeprecated' => [ - 'type' => Type::nonNull(Type::boolean()), - 'resolve' => static fn (EnumValueDefinition $enumValue): bool => $enumValue->isDeprecated(), - ], - 'deprecationReason' => [ - 'type' => Type::string(), - 'resolve' => static fn (EnumValueDefinition $enumValue): ?string => $enumValue->deprecationReason, - ], - ], - ]); + return BuiltInTypes::standard()->enumValueType(); } public static function _directive(): ObjectType { - return self::$cachedInstances[self::DIRECTIVE_OBJECT_NAME] ??= new ObjectType([ // @phpstan-ignore missingType.checkedException (static configuration is known to be correct) - 'name' => self::DIRECTIVE_OBJECT_NAME, - 'isIntrospection' => true, - 'description' => 'A Directive provides a way to describe alternate runtime execution and ' - . 'type validation behavior in a GraphQL document.' - . "\n\nIn some cases, you need to provide options to alter GraphQL's " - . 'execution behavior in ways field arguments will not suffice, such as ' - . 'conditionally including or skipping a field. Directives provide this by ' - . 'describing additional information to the executor.', - 'fields' => [ - 'name' => [ - 'type' => Type::nonNull(Type::string()), - 'resolve' => static fn (Directive $directive): string => $directive->name, - ], - 'description' => [ - 'type' => Type::string(), - 'resolve' => static fn (Directive $directive): ?string => $directive->description, - ], - 'isRepeatable' => [ - 'type' => Type::nonNull(Type::boolean()), - 'resolve' => static fn (Directive $directive): bool => $directive->isRepeatable, - ], - 'locations' => [ - 'type' => Type::nonNull(Type::listOf(Type::nonNull( - self::_directiveLocation() - ))), - 'resolve' => static fn (Directive $directive): array => $directive->locations, - ], - 'args' => [ - 'type' => Type::nonNull(Type::listOf(Type::nonNull(self::_inputValue()))), - 'args' => [ - 'includeDeprecated' => [ - 'type' => Type::nonNull(Type::boolean()), - 'defaultValue' => false, - ], - ], - 'resolve' => static function (Directive $directive, $args): array { - $values = $directive->args; - - if (! $args['includeDeprecated']) { - return array_filter( - $values, - static fn (Argument $value): bool => ! $value->isDeprecated(), - ); - } - - return $values; - }, - ], - ], - ]); + return BuiltInTypes::standard()->directiveType(); } public static function _directiveLocation(): EnumType { - return self::$cachedInstances[self::DIRECTIVE_LOCATION_ENUM_NAME] ??= new EnumType([ // @phpstan-ignore missingType.checkedException (static configuration is known to be correct) - 'name' => self::DIRECTIVE_LOCATION_ENUM_NAME, - 'isIntrospection' => true, - 'description' => 'A Directive can be adjacent to many parts of the GraphQL language, a ' - . '__DirectiveLocation describes one such possible adjacencies.', - 'values' => [ - 'QUERY' => [ - 'value' => DirectiveLocation::QUERY, - 'description' => 'Location adjacent to a query operation.', - ], - 'MUTATION' => [ - 'value' => DirectiveLocation::MUTATION, - 'description' => 'Location adjacent to a mutation operation.', - ], - 'SUBSCRIPTION' => [ - 'value' => DirectiveLocation::SUBSCRIPTION, - 'description' => 'Location adjacent to a subscription operation.', - ], - 'FIELD' => [ - 'value' => DirectiveLocation::FIELD, - 'description' => 'Location adjacent to a field.', - ], - 'FRAGMENT_DEFINITION' => [ - 'value' => DirectiveLocation::FRAGMENT_DEFINITION, - 'description' => 'Location adjacent to a fragment definition.', - ], - 'FRAGMENT_SPREAD' => [ - 'value' => DirectiveLocation::FRAGMENT_SPREAD, - 'description' => 'Location adjacent to a fragment spread.', - ], - 'INLINE_FRAGMENT' => [ - 'value' => DirectiveLocation::INLINE_FRAGMENT, - 'description' => 'Location adjacent to an inline fragment.', - ], - 'VARIABLE_DEFINITION' => [ - 'value' => DirectiveLocation::VARIABLE_DEFINITION, - 'description' => 'Location adjacent to a variable definition.', - ], - 'SCHEMA' => [ - 'value' => DirectiveLocation::SCHEMA, - 'description' => 'Location adjacent to a schema definition.', - ], - 'SCALAR' => [ - 'value' => DirectiveLocation::SCALAR, - 'description' => 'Location adjacent to a scalar definition.', - ], - 'OBJECT' => [ - 'value' => DirectiveLocation::OBJECT, - 'description' => 'Location adjacent to an object type definition.', - ], - 'FIELD_DEFINITION' => [ - 'value' => DirectiveLocation::FIELD_DEFINITION, - 'description' => 'Location adjacent to a field definition.', - ], - 'ARGUMENT_DEFINITION' => [ - 'value' => DirectiveLocation::ARGUMENT_DEFINITION, - 'description' => 'Location adjacent to an argument definition.', - ], - 'INTERFACE' => [ - 'value' => DirectiveLocation::IFACE, - 'description' => 'Location adjacent to an interface definition.', - ], - 'UNION' => [ - 'value' => DirectiveLocation::UNION, - 'description' => 'Location adjacent to a union definition.', - ], - 'ENUM' => [ - 'value' => DirectiveLocation::ENUM, - 'description' => 'Location adjacent to an enum definition.', - ], - 'ENUM_VALUE' => [ - 'value' => DirectiveLocation::ENUM_VALUE, - 'description' => 'Location adjacent to an enum value definition.', - ], - 'INPUT_OBJECT' => [ - 'value' => DirectiveLocation::INPUT_OBJECT, - 'description' => 'Location adjacent to an input object type definition.', - ], - 'INPUT_FIELD_DEFINITION' => [ - 'value' => DirectiveLocation::INPUT_FIELD_DEFINITION, - 'description' => 'Location adjacent to an input object field definition.', - ], - ], - ]); + return BuiltInTypes::standard()->directiveLocationType(); } public static function schemaMetaFieldDef(): 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.', - 'args' => [], - 'resolve' => static fn ($source, array $args, $context, ResolveInfo $info): Schema => $info->schema, - ]); + return BuiltInTypes::standard()->schemaMetaFieldDef(); } public static function typeMetaFieldDef(): 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.', - 'args' => [ - [ - 'name' => 'name', - 'type' => Type::nonNull(Type::string()), - ], - ], - 'resolve' => static fn ($source, array $args, $context, ResolveInfo $info): ?Type => $info->schema->getType($args['name']), - ]); + return BuiltInTypes::standard()->typeMetaFieldDef(); } public static function typeNameMetaFieldDef(): 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, - ]); + return BuiltInTypes::standard()->typeNameMetaFieldDef(); } public static function resetCachedInstances(): void { - self::$cachedInstances = null; + BuiltInTypes::resetStandard(); } } diff --git a/src/Type/Schema.php b/src/Type/Schema.php index 03fc0034a..a81545b48 100644 --- a/src/Type/Schema.php +++ b/src/Type/Schema.php @@ -4,7 +4,6 @@ use GraphQL\Error\Error; use GraphQL\Error\InvariantViolation; -use GraphQL\GraphQL; use GraphQL\Language\AST\OperationDefinitionNode; use GraphQL\Language\AST\SchemaDefinitionNode; use GraphQL\Language\AST\SchemaExtensionNode; @@ -63,6 +62,8 @@ class Schema */ private array $implementationsMap; + private BuiltInTypes $builtInTypes; + /** True when $resolvedTypes contains all possible schema types. */ private bool $fullyLoaded = false; @@ -100,6 +101,7 @@ public function __construct($config) $this->description = $config->description; $this->astNode = $config->astNode; $this->extensionASTNodes = $config->extensionASTNodes; + $this->builtInTypes = $config->builtInTypes ?? BuiltInTypes::standard(); $this->config = $config; } @@ -164,7 +166,7 @@ public function getTypeMap(): array TypeInfo::extractTypesFromDirectives($directive, $allReferencedTypes); } } - TypeInfo::extractTypes(Introspection::_schema(), $allReferencedTypes); + TypeInfo::extractTypes($this->builtInTypes->schemaType(), $allReferencedTypes); $this->resolvedTypes = $allReferencedTypes; $this->fullyLoaded = true; @@ -184,7 +186,7 @@ public function getTypeMap(): array */ public function getDirectives(): array { - return $this->config->directives ?? GraphQL::getStandardDirectives(); + return $this->config->directives ?? array_values($this->builtInTypes->directives()); } /** @param mixed $typeLoaderReturn could be anything */ @@ -272,6 +274,11 @@ public function getSubscriptionType(): ?ObjectType return $subscription; } + public function getBuiltInTypes(): BuiltInTypes + { + return $this->builtInTypes; + } + /** @api */ public function getConfig(): SchemaConfig { @@ -293,14 +300,9 @@ public function getType(string $name): ?Type return $this->resolvedTypes[$name]; } - $introspectionTypes = Introspection::getTypes(); - if (isset($introspectionTypes[$name])) { - return $introspectionTypes[$name]; - } - - $standardTypes = Type::getStandardTypes(); - if (isset($standardTypes[$name])) { - return $standardTypes[$name]; + $builtIn = $this->builtInTypes->allTypes(); + if (isset($builtIn[$name])) { + return $builtIn[$name]; } $type = $this->loadType($name); @@ -511,7 +513,7 @@ public function assertValid(): void throw new InvariantViolation(implode("\n\n", $this->validationErrors)); } - $internalTypes = Type::getStandardTypes() + Introspection::getTypes(); + $internalTypes = $this->builtInTypes->allTypes(); foreach ($this->getTypeMap() as $name => $type) { if (isset($internalTypes[$name])) { continue; diff --git a/src/Type/SchemaConfig.php b/src/Type/SchemaConfig.php index abf82dd4f..dbd5b7f3e 100644 --- a/src/Type/SchemaConfig.php +++ b/src/Type/SchemaConfig.php @@ -37,6 +37,7 @@ * types?: Types|null, * directives?: array|null, * typeLoader?: TypeLoader|null, + * builtInTypes?: BuiltInTypes|null, * assumeValid?: bool|null, * astNode?: SchemaDefinitionNode|null, * extensionASTNodes?: array|null, @@ -65,6 +66,8 @@ class SchemaConfig /** @var array|null */ public ?array $directives = null; + public ?BuiltInTypes $builtInTypes = null; + /** * @var callable|null * @@ -117,6 +120,10 @@ public static function create(array $options = []): self $config->setDirectives($options['directives']); } + if (isset($options['builtInTypes'])) { + $config->setBuiltInTypes($options['builtInTypes']); + } + if (isset($options['typeLoader'])) { $config->setTypeLoader($options['typeLoader']); } @@ -274,6 +281,18 @@ public function setDirectives(?array $directives): self return $this; } + public function getBuiltInTypes(): ?BuiltInTypes + { + return $this->builtInTypes; + } + + public function setBuiltInTypes(?BuiltInTypes $builtInTypes): self + { + $this->builtInTypes = $builtInTypes; + + return $this; + } + /** * @return callable|null $typeLoader * diff --git a/src/Utils/BuildClientSchema.php b/src/Utils/BuildClientSchema.php index 7cd55e78b..1136ca4ff 100644 --- a/src/Utils/BuildClientSchema.php +++ b/src/Utils/BuildClientSchema.php @@ -21,7 +21,6 @@ use GraphQL\Type\Definition\ScalarType; use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\UnionType; -use GraphQL\Type\Introspection; use GraphQL\Type\Schema; use GraphQL\Type\SchemaConfig; use GraphQL\Type\TypeKind; @@ -110,10 +109,7 @@ public function buildSchema(): Schema $schemaIntrospection = $this->introspection['__schema']; - $builtInTypes = array_merge( - Type::getStandardTypes(), - Introspection::getTypes() - ); + $builtInTypes = Type::builtInTypes(); foreach ($schemaIntrospection['types'] as $typeIntrospection) { if (! isset($typeIntrospection['name'])) { diff --git a/src/Utils/BuildSchema.php b/src/Utils/BuildSchema.php index 4fdade26e..669c34c47 100644 --- a/src/Utils/BuildSchema.php +++ b/src/Utils/BuildSchema.php @@ -13,7 +13,7 @@ use GraphQL\Language\AST\TypeExtensionNode; use GraphQL\Language\Parser; use GraphQL\Language\Source; -use GraphQL\Type\Definition\Directive; +use GraphQL\Type\BuiltInTypes; use GraphQL\Type\Definition\Type; use GraphQL\Type\Schema; use GraphQL\Type\SchemaConfig; @@ -222,18 +222,20 @@ static function (string $typeName): Type { $directivesByName[$directive->name][] = $directive; } + $builtIn = BuiltInTypes::standard(); + // If specified directives were not explicitly declared, add them. if (! isset($directivesByName['include'])) { - $directives[] = Directive::includeDirective(); + $directives[] = $builtIn->includeDirective(); } if (! isset($directivesByName['skip'])) { - $directives[] = Directive::skipDirective(); + $directives[] = $builtIn->skipDirective(); } if (! isset($directivesByName['deprecated'])) { - $directives[] = Directive::deprecatedDirective(); + $directives[] = $builtIn->deprecatedDirective(); } if (! isset($directivesByName['oneOf'])) { - $directives[] = Directive::oneOfDirective(); + $directives[] = $builtIn->oneOfDirective(); } // Note: While this could make early assertions to get the correctly diff --git a/src/Utils/TypeInfo.php b/src/Utils/TypeInfo.php index ac60efaf2..e25407590 100644 --- a/src/Utils/TypeInfo.php +++ b/src/Utils/TypeInfo.php @@ -33,7 +33,6 @@ use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\UnionType; use GraphQL\Type\Definition\WrappingType; -use GraphQL\Type\Introspection; use GraphQL\Type\Schema; class TypeInfo @@ -338,17 +337,19 @@ public function getParentType(): ?CompositeType private static function getFieldDefinition(Schema $schema, Type $parentType, FieldNode $fieldNode): ?FieldDefinition { $name = $fieldNode->name->value; - $schemaMeta = Introspection::schemaMetaFieldDef(); + $builtIn = $schema->getBuiltInTypes(); + + $schemaMeta = $builtIn->schemaMetaFieldDef(); if ($name === $schemaMeta->name && $schema->getQueryType() === $parentType) { return $schemaMeta; } - $typeMeta = Introspection::typeMetaFieldDef(); + $typeMeta = $builtIn->typeMetaFieldDef(); if ($name === $typeMeta->name && $schema->getQueryType() === $parentType) { return $typeMeta; } - $typeNameMeta = Introspection::typeNameMetaFieldDef(); + $typeNameMeta = $builtIn->typeNameMetaFieldDef(); if ($name === $typeNameMeta->name && $parentType instanceof CompositeType) { return $typeNameMeta; } diff --git a/src/Validator/Rules/QuerySecurityRule.php b/src/Validator/Rules/QuerySecurityRule.php index b61932135..0d118e4b7 100644 --- a/src/Validator/Rules/QuerySecurityRule.php +++ b/src/Validator/Rules/QuerySecurityRule.php @@ -12,7 +12,6 @@ use GraphQL\Type\Definition\FieldDefinition; use GraphQL\Type\Definition\HasFieldsType; use GraphQL\Type\Definition\Type; -use GraphQL\Type\Introspection; use GraphQL\Utils\AST; use GraphQL\Validator\QueryValidationContext; @@ -115,9 +114,10 @@ protected function collectFieldASTsAndDefs( $fieldDef = null; if ($parentType instanceof HasFieldsType) { - $schemaMetaFieldDef = Introspection::schemaMetaFieldDef(); - $typeMetaFieldDef = Introspection::typeMetaFieldDef(); - $typeNameMetaFieldDef = Introspection::typeNameMetaFieldDef(); + $builtIn = $context->getSchema()->getBuiltInTypes(); + $schemaMetaFieldDef = $builtIn->schemaMetaFieldDef(); + $typeMetaFieldDef = $builtIn->typeMetaFieldDef(); + $typeNameMetaFieldDef = $builtIn->typeNameMetaFieldDef(); $queryType = $context->getSchema()->getQueryType(); diff --git a/tests/Type/BuiltInTypesTest.php b/tests/Type/BuiltInTypesTest.php new file mode 100644 index 000000000..2e55c047a --- /dev/null +++ b/tests/Type/BuiltInTypesTest.php @@ -0,0 +1,145 @@ + */ + private static array $originalStandardTypes; + + public static function setUpBeforeClass(): void + { + self::$originalStandardTypes = Type::getStandardTypes(); + } + + public function tearDown(): void + { + parent::tearDown(); + Type::overrideStandardTypes(self::$originalStandardTypes); + } + + public function testStandardReturnsSingleton(): void + { + $a = BuiltInTypes::standard(); + $b = BuiltInTypes::standard(); + self::assertSame($a, $b); + } + + public function testResetStandardClearsSingleton(): void + { + $before = BuiltInTypes::standard(); + BuiltInTypes::resetStandard(); + $after = BuiltInTypes::standard(); + self::assertNotSame($before, $after); + } + + public function testDefaultInstanceMatchesStaticTypes(): void + { + $builtIn = BuiltInTypes::standard(); + + self::assertSame(Type::int(), $builtIn->int()); + self::assertSame(Type::float(), $builtIn->float()); + self::assertSame(Type::string(), $builtIn->string()); + self::assertSame(Type::boolean(), $builtIn->boolean()); + self::assertSame(Type::id(), $builtIn->id()); + } + + public function testTwoInstancesProduceDistinctTypes(): void + { + $a = new BuiltInTypes(); + $b = new BuiltInTypes(); + + self::assertNotSame($a->schemaType(), $b->schemaType()); + self::assertNotSame($a->typeType(), $b->typeType()); + self::assertNotSame($a->schemaMetaFieldDef(), $b->schemaMetaFieldDef()); + self::assertNotSame($a->includeDirective(), $b->includeDirective()); + } + + public function testCustomScalarOverridePropagates(): void + { + $customString = new CustomScalarType([ + 'name' => Type::STRING, + 'serialize' => static fn ($value) => $value, + ]); + + $builtIn = new BuiltInTypes([Type::STRING => $customString]); + + self::assertSame($customString, $builtIn->string()); + self::assertSame($customString, $builtIn->standardTypes()[Type::STRING]); + self::assertSame($customString, $builtIn->allTypes()[Type::STRING]); + + $typeNameMeta = $builtIn->typeNameMetaFieldDef(); + $innerType = Type::getNullableType($typeNameMeta->getType()); + self::assertSame($customString, $innerType); + + $deprecatedDirective = $builtIn->deprecatedDirective(); + self::assertSame($customString, $deprecatedDirective->args[0]->getType()); + } + + public function testTwoSchemasWithDifferentBuiltInTypes(): void + { + $queryType = new ObjectType([ + 'name' => 'Query', + 'fields' => ['hello' => Type::string()], + ]); + + $builtInA = new BuiltInTypes(); + $builtInB = new BuiltInTypes(); + + $schemaA = new Schema( + (new SchemaConfig()) + ->setQuery($queryType) + ->setBuiltInTypes($builtInA) + ->setAssumeValid(true) + ); + + $schemaB = new Schema( + (new SchemaConfig()) + ->setQuery($queryType) + ->setBuiltInTypes($builtInB) + ->setAssumeValid(true) + ); + + self::assertSame($builtInA, $schemaA->getBuiltInTypes()); + self::assertSame($builtInB, $schemaB->getBuiltInTypes()); + self::assertNotSame($schemaA->getBuiltInTypes(), $schemaB->getBuiltInTypes()); + + self::assertNotSame( + $schemaA->getBuiltInTypes()->schemaType(), + $schemaB->getBuiltInTypes()->schemaType() + ); + } + + public function testAllTypesContainsScalarsAndIntrospection(): void + { + $builtIn = new BuiltInTypes(); + $allTypes = $builtIn->allTypes(); + + self::assertArrayHasKey('Int', $allTypes); + self::assertArrayHasKey('String', $allTypes); + self::assertArrayHasKey('__Schema', $allTypes); + self::assertArrayHasKey('__Type', $allTypes); + self::assertArrayHasKey('__TypeKind', $allTypes); + self::assertCount(13, $allTypes); + } + + public function testDirectivesReturnsAllStandard(): void + { + $builtIn = new BuiltInTypes(); + $directives = $builtIn->directives(); + + self::assertArrayHasKey('include', $directives); + self::assertArrayHasKey('skip', $directives); + self::assertArrayHasKey('deprecated', $directives); + self::assertArrayHasKey('oneOf', $directives); + self::assertCount(4, $directives); + } +} From c3438f92e40615f8e45b2b47606b999712e81076 Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Sun, 8 Mar 2026 11:38:38 +0100 Subject: [PATCH 2/8] Optimize ReferenceExecutor to avoid redundant BuiltInTypes lookups - Cache skip/include directives in the constructor instead of looking them up on every shouldIncludeNode call - Add allTypesCache to BuiltInTypes to avoid re-merging on every call - Fix getFieldDef guard to use double-underscore prefix check per spec, so single-underscore fields like _id take the fast path (https://spec.graphql.org/October2021/#sec-Reserved-Names) - Add regression test for single-underscore field resolution Co-Authored-By: Claude Sonnet 4.6 --- src/Executor/ReferenceExecutor.php | 30 ++++++++++++++++++------------ src/Type/BuiltInTypes.php | 5 ++++- tests/Executor/ExecutorTest.php | 18 ++++++++++++++++++ 3 files changed, 40 insertions(+), 13 deletions(-) diff --git a/src/Executor/ReferenceExecutor.php b/src/Executor/ReferenceExecutor.php index d6b5067bc..08288d311 100644 --- a/src/Executor/ReferenceExecutor.php +++ b/src/Executor/ReferenceExecutor.php @@ -17,6 +17,7 @@ use GraphQL\Language\AST\SelectionNode; use GraphQL\Language\AST\SelectionSetNode; use GraphQL\Type\Definition\AbstractType; +use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\FieldDefinition; use GraphQL\Type\Definition\InterfaceType; use GraphQL\Type\Definition\LeafType; @@ -73,6 +74,10 @@ class ReferenceExecutor implements ExecutorImplementation protected FieldDefinition $typeNameMetaFieldDef; + protected Directive $skipDirective; + + protected Directive $includeDirective; + protected function __construct(ExecutionContext $context) { if (! isset(static::$UNDEFINED)) { @@ -82,6 +87,10 @@ protected function __construct(ExecutionContext $context) $this->exeContext = $context; $this->subFieldCache = new \SplObjectStorage(); $this->fieldArgsCache = new \SplObjectStorage(); + + $builtIn = $context->schema->getBuiltInTypes(); + $this->skipDirective = $builtIn->skipDirective(); + $this->includeDirective = $builtIn->includeDirective(); } /** @@ -464,22 +473,12 @@ protected function shouldIncludeNode(SelectionNode $node): bool { $variableValues = $this->exeContext->variableValues; - $builtIn = $this->exeContext->schema->getBuiltInTypes(); - - $skip = Values::getDirectiveValues( - $builtIn->skipDirective(), - $node, - $variableValues - ); + $skip = Values::getDirectiveValues($this->skipDirective, $node, $variableValues); if (isset($skip['if']) && $skip['if'] === true) { return false; } - $include = Values::getDirectiveValues( - $builtIn->includeDirective(), - $node, - $variableValues - ); + $include = Values::getDirectiveValues($this->includeDirective, $node, $variableValues); return ! isset($include['if']) || $include['if'] !== false; } @@ -684,6 +683,13 @@ protected function resolveField( */ protected function getFieldDef(Schema $schema, ObjectType $parentType, string $fieldName): ?FieldDefinition { + // Only `__`-prefixed names are reserved for introspection meta-fields (__schema, __type, __typename). + // https://spec.graphql.org/October2021/#sec-Reserved-Names + // Skipping this check for all other fields avoids the cost of lazy-initializing the built-in meta-field definitions. + if (($fieldName[0] ?? '') !== '_' || ($fieldName[1] ?? '') !== '_') { + return $parentType->findField($fieldName); + } + $builtIn = $schema->getBuiltInTypes(); $this->schemaMetaFieldDef ??= $builtIn->schemaMetaFieldDef(); $this->typeMetaFieldDef ??= $builtIn->typeMetaFieldDef(); diff --git a/src/Type/BuiltInTypes.php b/src/Type/BuiltInTypes.php index 553174d58..60f322f77 100644 --- a/src/Type/BuiltInTypes.php +++ b/src/Type/BuiltInTypes.php @@ -54,6 +54,9 @@ class BuiltInTypes /** @var array */ private array $directives = []; + /** @var array|null */ + private ?array $allTypesCache = null; + /** @var array */ private array $standardTypeOverrides; @@ -772,7 +775,7 @@ public function directives(): array /** @return array */ public function allTypes(): array { - return array_merge( + return $this->allTypesCache ??= array_merge( $this->introspectionTypes(), $this->standardTypes() ); diff --git a/tests/Executor/ExecutorTest.php b/tests/Executor/ExecutorTest.php index 812c33d4a..ffb683449 100644 --- a/tests/Executor/ExecutorTest.php +++ b/tests/Executor/ExecutorTest.php @@ -902,6 +902,24 @@ public function testDoesNotIncludeIllegalFieldsInOutput(): void self::assertSame(['data' => []], $mutationResult->toArray()); } + public function testResolvesFieldsWithSingleUnderscorePrefix(): void + { + $schema = new Schema([ + 'query' => new ObjectType([ + 'name' => 'Query', + 'fields' => [ + '_privateField' => [ + 'type' => Type::string(), + 'resolve' => static fn (): string => 'secret', + ], + ], + ]), + ]); + + $result = Executor::execute($schema, Parser::parse('{ _privateField }')); + self::assertSame(['data' => ['_privateField' => 'secret']], $result->toArray()); + } + /** @see it('does not include arguments that were not set') */ public function testDoesNotIncludeArgumentsThatWereNotSet(): void { From 8a21d04c3aef2b0d62d4bb16500838a9754ad319 Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Sun, 8 Mar 2026 14:08:51 +0100 Subject: [PATCH 3/8] Rename BuiltInTypes to BuiltInDefinitions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The class manages both built-in types and directives — matching the GraphQL spec's "built-in scalars" and "built-in directives" terminology. "Standard types" does not appear in the spec at all. Beyond the rename, consolidates all built-in concerns that were spread across Type, Introspection, and Directive into BuiltInDefinitions: - New constants: SCALAR_TYPE_NAMES, INTROSPECTION_TYPE_NAMES, BUILT_IN_TYPE_NAMES, BUILT_IN_DIRECTIVE_NAMES - New static helpers: isIntrospectionType(), isBuiltInDirective(), isBuiltInType(), isBuiltInTypeName(), overrideScalarTypes() - Renamed: standardTypes() → scalarTypes(), allTypes() → types(), $standardTypeOverrides → $scalarTypeOverrides - Scalar fallbacks use new IntType() directly (breaks circular dep) Removes all non-@api static forwarding from Introspection and Directive — those classes now only expose constants and @api methods. Removes getStandardTypes(), overrideStandardTypes(), builtInTypes() from Type — callers use BuiltInDefinitions directly. SchemaConfig/Schema rename builtInTypes → builtInDefinitions. Refs https://github.com/webonyx/graphql-php/pull/1426#discussion_r1295488370 Co-Authored-By: Claude Sonnet 4.6 --- docs/class-reference.md | 2 +- src/Executor/ReferenceExecutor.php | 14 +- src/GraphQL.php | 7 +- ...uiltInTypes.php => BuiltInDefinitions.php} | 126 +++++++++++++++--- src/Type/Definition/Directive.php | 37 ----- .../Definition/NamedTypeImplementation.php | 3 +- src/Type/Definition/Type.php | 86 +----------- src/Type/Introspection.php | 88 ------------ src/Type/Schema.php | 22 +-- src/Type/SchemaConfig.php | 16 +-- src/Type/SchemaValidationContext.php | 2 +- src/Utils/ASTDefinitionBuilder.php | 17 +-- src/Utils/BuildClientSchema.php | 3 +- src/Utils/BuildSchema.php | 30 ++--- src/Utils/SchemaExtender.php | 15 +-- src/Utils/SchemaPrinter.php | 8 +- src/Utils/TypeInfo.php | 26 ++-- .../Rules/KnownArgumentNamesOnDirectives.php | 6 +- src/Validator/Rules/KnownDirectives.php | 10 +- src/Validator/Rules/KnownTypeNames.php | 6 +- .../ProvidedRequiredArgumentsOnDirectives.php | 10 +- src/Validator/Rules/QueryComplexity.php | 5 +- src/Validator/Rules/QuerySecurityRule.php | 8 +- .../Rules/UniqueDirectivesPerLocation.php | 4 +- ...pesTest.php => BuiltInDefinitionsTest.php} | 86 +++++------- tests/Type/StandardTypesTest.php | 54 ++++---- tests/Utils/BreakingChangesFinderTest.php | 15 ++- tests/Utils/BuildSchemaTest.php | 22 +-- tests/Validator/ValidatorTestCase.php | 7 +- 29 files changed, 303 insertions(+), 432 deletions(-) rename src/Type/{BuiltInTypes.php => BuiltInDefinitions.php} (88%) rename tests/Type/{BuiltInTypesTest.php => BuiltInDefinitionsTest.php} (51%) diff --git a/docs/class-reference.md b/docs/class-reference.md index 8c2116af5..7445c0bb8 100644 --- a/docs/class-reference.md +++ b/docs/class-reference.md @@ -724,7 +724,7 @@ Usage example: types?: Types|null, directives?: array|null, typeLoader?: TypeLoader|null, - builtInTypes?: BuiltInTypes|null, + builtInDefinitions?: BuiltInDefinitions|null, assumeValid?: bool|null, astNode?: SchemaDefinitionNode|null, extensionASTNodes?: array|null, diff --git a/src/Executor/ReferenceExecutor.php b/src/Executor/ReferenceExecutor.php index 08288d311..e50814d8d 100644 --- a/src/Executor/ReferenceExecutor.php +++ b/src/Executor/ReferenceExecutor.php @@ -88,9 +88,9 @@ protected function __construct(ExecutionContext $context) $this->subFieldCache = new \SplObjectStorage(); $this->fieldArgsCache = new \SplObjectStorage(); - $builtIn = $context->schema->getBuiltInTypes(); - $this->skipDirective = $builtIn->skipDirective(); - $this->includeDirective = $builtIn->includeDirective(); + $builtInDefinitions = $context->schema->getBuiltInDefinitions(); + $this->skipDirective = $builtInDefinitions->skipDirective(); + $this->includeDirective = $builtInDefinitions->includeDirective(); } /** @@ -690,10 +690,10 @@ protected function getFieldDef(Schema $schema, ObjectType $parentType, string $f return $parentType->findField($fieldName); } - $builtIn = $schema->getBuiltInTypes(); - $this->schemaMetaFieldDef ??= $builtIn->schemaMetaFieldDef(); - $this->typeMetaFieldDef ??= $builtIn->typeMetaFieldDef(); - $this->typeNameMetaFieldDef ??= $builtIn->typeNameMetaFieldDef(); + $builtInDefinitions = $schema->getBuiltInDefinitions(); + $this->schemaMetaFieldDef ??= $builtInDefinitions->schemaMetaFieldDef(); + $this->typeMetaFieldDef ??= $builtInDefinitions->typeMetaFieldDef(); + $this->typeNameMetaFieldDef ??= $builtInDefinitions->typeNameMetaFieldDef(); $queryType = $schema->getQueryType(); diff --git a/src/GraphQL.php b/src/GraphQL.php index 919b09dec..e945eef8c 100644 --- a/src/GraphQL.php +++ b/src/GraphQL.php @@ -12,6 +12,7 @@ use GraphQL\Language\AST\DocumentNode; use GraphQL\Language\Parser; use GraphQL\Language\Source; +use GraphQL\Type\BuiltInDefinitions; use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\ScalarType; use GraphQL\Type\Definition\Type; @@ -188,7 +189,7 @@ public static function promiseToExecute( */ public static function getStandardDirectives(): array { - return Directive::getInternalDirectives(); + return BuiltInDefinitions::standard()->directives(); } /** @@ -202,7 +203,7 @@ public static function getStandardDirectives(): array */ public static function getStandardTypes(): array { - return Type::getStandardTypes(); + return BuiltInDefinitions::standard()->scalarTypes(); } /** @@ -218,7 +219,7 @@ public static function getStandardTypes(): array */ public static function overrideStandardTypes(array $types): void { - Type::overrideStandardTypes($types); + BuiltInDefinitions::overrideScalarTypes($types); } /** diff --git a/src/Type/BuiltInTypes.php b/src/Type/BuiltInDefinitions.php similarity index 88% rename from src/Type/BuiltInTypes.php rename to src/Type/BuiltInDefinitions.php index 60f322f77..89904ec26 100644 --- a/src/Type/BuiltInTypes.php +++ b/src/Type/BuiltInDefinitions.php @@ -6,31 +6,68 @@ use GraphQL\Language\DirectiveLocation; use GraphQL\Language\Printer; use GraphQL\Type\Definition\Argument; +use GraphQL\Type\Definition\BooleanType; use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\EnumType; use GraphQL\Type\Definition\EnumValueDefinition; use GraphQL\Type\Definition\FieldDefinition; +use GraphQL\Type\Definition\FloatType; +use GraphQL\Type\Definition\IDType; use GraphQL\Type\Definition\InputObjectField; use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\InterfaceType; +use GraphQL\Type\Definition\IntType; use GraphQL\Type\Definition\ListOfType; use GraphQL\Type\Definition\NamedType; use GraphQL\Type\Definition\NonNull; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\ResolveInfo; use GraphQL\Type\Definition\ScalarType; +use GraphQL\Type\Definition\StringType; use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\UnionType; use GraphQL\Type\Definition\WrappingType; use GraphQL\Utils\AST; use GraphQL\Utils\Utils; -class BuiltInTypes +class BuiltInDefinitions { + protected const SCALAR_TYPE_NAMES = [ + Type::INT, + Type::FLOAT, + Type::STRING, + Type::BOOLEAN, + Type::ID, + ]; + + public const INTROSPECTION_TYPE_NAMES = [ + Introspection::SCHEMA_OBJECT_NAME, + Introspection::TYPE_OBJECT_NAME, + Introspection::DIRECTIVE_OBJECT_NAME, + Introspection::FIELD_OBJECT_NAME, + Introspection::INPUT_VALUE_OBJECT_NAME, + Introspection::ENUM_VALUE_OBJECT_NAME, + Introspection::TYPE_KIND_ENUM_NAME, + Introspection::DIRECTIVE_LOCATION_ENUM_NAME, + ]; + + public const BUILT_IN_TYPE_NAMES = [ + ...self::SCALAR_TYPE_NAMES, + ...self::INTROSPECTION_TYPE_NAMES, + ]; + + public const BUILT_IN_DIRECTIVE_NAMES = [ + Directive::INCLUDE_NAME, + Directive::SKIP_NAME, + Directive::DEPRECATED_NAME, + Directive::ONE_OF_NAME, + ]; + + /** Singleton containing standard built-in definitions. */ private static ?self $standard = null; /** @var array */ - private array $scalars = []; + private array $scalarTypes = []; private ?ObjectType $schemaTypeInstance = null; @@ -55,15 +92,15 @@ class BuiltInTypes private array $directives = []; /** @var array|null */ - private ?array $allTypesCache = null; + private ?array $typesCache = null; /** @var array */ - private array $standardTypeOverrides; + private array $scalarTypeOverrides; - /** @param array $standardTypeOverrides */ - public function __construct(array $standardTypeOverrides = []) + /** @param array $scalarTypeOverrides */ + public function __construct(array $scalarTypeOverrides = []) { - $this->standardTypeOverrides = $standardTypeOverrides; + $this->scalarTypeOverrides = $scalarTypeOverrides; } public static function standard(): self @@ -71,38 +108,81 @@ public static function standard(): self return self::$standard ??= new self(); } - public static function resetStandard(): void + /** @param array $types */ + public static function overrideScalarTypes(array $types): void { - self::$standard = null; + // Preserve non-overridden scalar instances from the current standard. + $scalarOverrides = self::$standard !== null + ? self::$standard->scalarTypes() + : []; + + foreach ($types as $type) { + // @phpstan-ignore-next-line generic type is not enforced by PHP + if (! $type instanceof ScalarType) { + $typeClass = ScalarType::class; + $notType = Utils::printSafe($type); + throw new InvariantViolation("Expecting instance of {$typeClass}, got {$notType}"); + } + + if (! in_array($type->name, self::SCALAR_TYPE_NAMES, true)) { + $builtInScalarNames = implode(', ', self::SCALAR_TYPE_NAMES); + $notBuiltInName = Utils::printSafe($type->name); + throw new InvariantViolation("Expecting one of the following names for a built-in scalar type: {$builtInScalarNames}; got {$notBuiltInName}"); // @phpstan-ignore missingType.checkedException (validation is part of the public contract) + } + + $scalarOverrides[$type->name] = $type; + } + + self::$standard = new self($scalarOverrides); + } + + public static function isIntrospectionType(NamedType $type): bool + { + return in_array($type->name, self::INTROSPECTION_TYPE_NAMES, true); + } + + public static function isBuiltInDirective(Directive $directive): bool + { + return in_array($directive->name, self::BUILT_IN_DIRECTIVE_NAMES, true); } public function int(): ScalarType { - return $this->scalars[Type::INT] ??= $this->standardTypeOverrides[Type::INT] ?? Type::int(); + return $this->scalarTypes[Type::INT] + ??= $this->scalarTypeOverrides[Type::INT] + ?? new IntType(); // @phpstan-ignore missingType.checkedException (static configuration is known to be correct) } public function float(): ScalarType { - return $this->scalars[Type::FLOAT] ??= $this->standardTypeOverrides[Type::FLOAT] ?? Type::float(); + return $this->scalarTypes[Type::FLOAT] + ??= $this->scalarTypeOverrides[Type::FLOAT] + ?? new FloatType(); // @phpstan-ignore missingType.checkedException (static configuration is known to be correct) } public function string(): ScalarType { - return $this->scalars[Type::STRING] ??= $this->standardTypeOverrides[Type::STRING] ?? Type::string(); + return $this->scalarTypes[Type::STRING] + ??= $this->scalarTypeOverrides[Type::STRING] + ?? new StringType(); // @phpstan-ignore missingType.checkedException (static configuration is known to be correct) } public function boolean(): ScalarType { - return $this->scalars[Type::BOOLEAN] ??= $this->standardTypeOverrides[Type::BOOLEAN] ?? Type::boolean(); + return $this->scalarTypes[Type::BOOLEAN] + ??= $this->scalarTypeOverrides[Type::BOOLEAN] + ?? new BooleanType(); // @phpstan-ignore missingType.checkedException (static configuration is known to be correct) } public function id(): ScalarType { - return $this->scalars[Type::ID] ??= $this->standardTypeOverrides[Type::ID] ?? Type::id(); + return $this->scalarTypes[Type::ID] + ??= $this->scalarTypeOverrides[Type::ID] + ?? new IDType(); // @phpstan-ignore missingType.checkedException (static configuration is known to be correct) } /** @return array */ - public function standardTypes(): array + public function scalarTypes(): array { return [ Type::INT => $this->int(), @@ -113,6 +193,16 @@ public function standardTypes(): array ]; } + public static function isBuiltInTypeName(string $name): bool + { + return in_array($name, BuiltInDefinitions::BUILT_IN_TYPE_NAMES, true); + } + + public static function isBuiltInType(NamedType $type): bool + { + return in_array($type->name, BuiltInDefinitions::BUILT_IN_TYPE_NAMES, true); + } + public function schemaType(): ObjectType { return $this->schemaTypeInstance ??= new ObjectType([ // @phpstan-ignore missingType.checkedException (static configuration is known to be correct) @@ -773,11 +863,11 @@ public function directives(): array } /** @return array */ - public function allTypes(): array + public function types(): array { - return $this->allTypesCache ??= array_merge( + return $this->typesCache ??= array_merge( $this->introspectionTypes(), - $this->standardTypes() + $this->scalarTypes() ); } } diff --git a/src/Type/Definition/Directive.php b/src/Type/Definition/Directive.php index 2b5303d0e..e8a45ebb7 100644 --- a/src/Type/Definition/Directive.php +++ b/src/Type/Definition/Directive.php @@ -3,7 +3,6 @@ namespace GraphQL\Type\Definition; use GraphQL\Language\AST\DirectiveDefinitionNode; -use GraphQL\Type\BuiltInTypes; /** * @phpstan-import-type ArgumentListConfig from Argument @@ -67,40 +66,4 @@ public function __construct(array $config) $this->config = $config; } - - /** @return array */ - public static function getInternalDirectives(): array - { - return BuiltInTypes::standard()->directives(); - } - - public static function includeDirective(): Directive - { - return BuiltInTypes::standard()->includeDirective(); - } - - public static function skipDirective(): Directive - { - return BuiltInTypes::standard()->skipDirective(); - } - - public static function deprecatedDirective(): Directive - { - return BuiltInTypes::standard()->deprecatedDirective(); - } - - public static function oneOfDirective(): Directive - { - return BuiltInTypes::standard()->oneOfDirective(); - } - - public static function isSpecifiedDirective(Directive $directive): bool - { - return array_key_exists($directive->name, self::getInternalDirectives()); - } - - public static function resetCachedInstances(): void - { - BuiltInTypes::resetStandard(); - } } diff --git a/src/Type/Definition/NamedTypeImplementation.php b/src/Type/Definition/NamedTypeImplementation.php index e5acae07a..3d6a8c3fd 100644 --- a/src/Type/Definition/NamedTypeImplementation.php +++ b/src/Type/Definition/NamedTypeImplementation.php @@ -3,6 +3,7 @@ namespace GraphQL\Type\Definition; use GraphQL\Error\InvariantViolation; +use GraphQL\Type\BuiltInDefinitions; /** * @see NamedType @@ -43,7 +44,7 @@ protected function inferName(): string public function isBuiltInType(): bool { - return in_array($this->name, Type::BUILT_IN_TYPE_NAMES, true); + return BuiltInDefinitions::isBuiltInType($this); } public function name(): string diff --git a/src/Type/Definition/Type.php b/src/Type/Definition/Type.php index dfee70dbf..e76b5c489 100644 --- a/src/Type/Definition/Type.php +++ b/src/Type/Definition/Type.php @@ -2,10 +2,7 @@ namespace GraphQL\Type\Definition; -use GraphQL\Error\InvariantViolation; -use GraphQL\Type\BuiltInTypes; -use GraphQL\Type\Introspection; -use GraphQL\Utils\Utils; +use GraphQL\Type\BuiltInDefinitions; /** * Registry of standard GraphQL types and base class for all other types. @@ -18,22 +15,6 @@ abstract class Type implements \JsonSerializable public const BOOLEAN = 'Boolean'; public const ID = 'ID'; - public const STANDARD_TYPE_NAMES = [ - self::INT, - self::FLOAT, - self::STRING, - self::BOOLEAN, - self::ID, - ]; - - public const BUILT_IN_TYPE_NAMES = [ - ...self::STANDARD_TYPE_NAMES, - ...Introspection::TYPE_NAMES, - ]; - - /** @var array|null */ - protected static ?array $standardTypes; - /** * Returns the registered or default standard Int type. * @@ -41,7 +22,7 @@ abstract class Type implements \JsonSerializable */ public static function int(): ScalarType { - return static::$standardTypes[self::INT] ??= new IntType(); // @phpstan-ignore missingType.checkedException (static configuration is known to be correct) + return BuiltInDefinitions::standard()->int(); } /** @@ -51,7 +32,7 @@ public static function int(): ScalarType */ public static function float(): ScalarType { - return static::$standardTypes[self::FLOAT] ??= new FloatType(); // @phpstan-ignore missingType.checkedException (static configuration is known to be correct) + return BuiltInDefinitions::standard()->float(); } /** @@ -61,7 +42,7 @@ public static function float(): ScalarType */ public static function string(): ScalarType { - return static::$standardTypes[self::STRING] ??= new StringType(); // @phpstan-ignore missingType.checkedException (static configuration is known to be correct) + return BuiltInDefinitions::standard()->string(); } /** @@ -71,7 +52,7 @@ public static function string(): ScalarType */ public static function boolean(): ScalarType { - return static::$standardTypes[self::BOOLEAN] ??= new BooleanType(); // @phpstan-ignore missingType.checkedException (static configuration is known to be correct) + return BuiltInDefinitions::standard()->boolean(); } /** @@ -81,7 +62,7 @@ public static function boolean(): ScalarType */ public static function id(): ScalarType { - return static::$standardTypes[self::ID] ??= new IDType(); // @phpstan-ignore missingType.checkedException (static configuration is known to be correct) + return BuiltInDefinitions::standard()->id(); } /** @@ -116,61 +97,6 @@ public static function nonNull($type): NonNull return new NonNull($type); } - /** - * Returns all builtin in types including base scalar and introspection types. - * - * @return array - */ - public static function builtInTypes(): array - { - return BuiltInTypes::standard()->allTypes(); - } - - /** - * Returns all builtin scalar types. - * - * @return array - */ - public static function getStandardTypes(): array - { - return [ - self::INT => static::int(), - self::FLOAT => static::float(), - self::STRING => static::string(), - self::BOOLEAN => static::boolean(), - self::ID => static::id(), - ]; - } - - /** - * Allows partially or completely overriding the standard types. - * - * @param array $types - * - * @throws InvariantViolation - */ - public static function overrideStandardTypes(array $types): void - { - BuiltInTypes::resetStandard(); - - foreach ($types as $type) { - // @phpstan-ignore-next-line generic type is not enforced by PHP - if (! $type instanceof ScalarType) { - $typeClass = ScalarType::class; - $notType = Utils::printSafe($type); - throw new InvariantViolation("Expecting instance of {$typeClass}, got {$notType}"); - } - - if (! in_array($type->name, self::STANDARD_TYPE_NAMES, true)) { - $standardTypeNames = implode(', ', self::STANDARD_TYPE_NAMES); - $notStandardTypeName = Utils::printSafe($type->name); - throw new InvariantViolation("Expecting one of the following names for a standard type: {$standardTypeNames}; got {$notStandardTypeName}"); - } - - static::$standardTypes[$type->name] = $type; - } - } - /** * Determines if the given type is an input type. * diff --git a/src/Type/Introspection.php b/src/Type/Introspection.php index 0f1fd476a..8e845055c 100644 --- a/src/Type/Introspection.php +++ b/src/Type/Introspection.php @@ -4,11 +4,6 @@ use GraphQL\Error\InvariantViolation; use GraphQL\GraphQL; -use GraphQL\Type\Definition\EnumType; -use GraphQL\Type\Definition\FieldDefinition; -use GraphQL\Type\Definition\NamedType; -use GraphQL\Type\Definition\ObjectType; -use GraphQL\Type\Definition\Type; use GraphQL\Utils\Utils; /** @@ -47,17 +42,6 @@ class Introspection public const TYPE_KIND_ENUM_NAME = '__TypeKind'; public const DIRECTIVE_LOCATION_ENUM_NAME = '__DirectiveLocation'; - public const TYPE_NAMES = [ - self::SCHEMA_OBJECT_NAME, - self::TYPE_OBJECT_NAME, - self::DIRECTIVE_OBJECT_NAME, - self::FIELD_OBJECT_NAME, - self::INPUT_VALUE_OBJECT_NAME, - self::ENUM_VALUE_OBJECT_NAME, - self::TYPE_KIND_ENUM_NAME, - self::DIRECTIVE_LOCATION_ENUM_NAME, - ]; - /** * @param IntrospectionOptions $options * @@ -225,76 +209,4 @@ public static function fromSchema(Schema $schema, array $options = []): array return $data; } - - /** @param Type&NamedType $type */ - public static function isIntrospectionType(NamedType $type): bool - { - return in_array($type->name, self::TYPE_NAMES, true); - } - - /** @return array */ - public static function getTypes(): array - { - return BuiltInTypes::standard()->introspectionTypes(); - } - - public static function _schema(): ObjectType - { - return BuiltInTypes::standard()->schemaType(); - } - - public static function _type(): ObjectType - { - return BuiltInTypes::standard()->typeType(); - } - - public static function _typeKind(): EnumType - { - return BuiltInTypes::standard()->typeKindType(); - } - - public static function _field(): ObjectType - { - return BuiltInTypes::standard()->fieldType(); - } - - public static function _inputValue(): ObjectType - { - return BuiltInTypes::standard()->inputValueType(); - } - - public static function _enumValue(): ObjectType - { - return BuiltInTypes::standard()->enumValueType(); - } - - public static function _directive(): ObjectType - { - return BuiltInTypes::standard()->directiveType(); - } - - public static function _directiveLocation(): EnumType - { - return BuiltInTypes::standard()->directiveLocationType(); - } - - public static function schemaMetaFieldDef(): FieldDefinition - { - return BuiltInTypes::standard()->schemaMetaFieldDef(); - } - - public static function typeMetaFieldDef(): FieldDefinition - { - return BuiltInTypes::standard()->typeMetaFieldDef(); - } - - public static function typeNameMetaFieldDef(): FieldDefinition - { - return BuiltInTypes::standard()->typeNameMetaFieldDef(); - } - - public static function resetCachedInstances(): void - { - BuiltInTypes::resetStandard(); - } } diff --git a/src/Type/Schema.php b/src/Type/Schema.php index a81545b48..b966c71be 100644 --- a/src/Type/Schema.php +++ b/src/Type/Schema.php @@ -62,7 +62,7 @@ class Schema */ private array $implementationsMap; - private BuiltInTypes $builtInTypes; + private BuiltInDefinitions $builtInDefinitions; /** True when $resolvedTypes contains all possible schema types. */ private bool $fullyLoaded = false; @@ -101,7 +101,7 @@ public function __construct($config) $this->description = $config->description; $this->astNode = $config->astNode; $this->extensionASTNodes = $config->extensionASTNodes; - $this->builtInTypes = $config->builtInTypes ?? BuiltInTypes::standard(); + $this->builtInDefinitions = $config->builtInDefinitions ?? BuiltInDefinitions::standard(); $this->config = $config; } @@ -166,7 +166,7 @@ public function getTypeMap(): array TypeInfo::extractTypesFromDirectives($directive, $allReferencedTypes); } } - TypeInfo::extractTypes($this->builtInTypes->schemaType(), $allReferencedTypes); + TypeInfo::extractTypes($this->builtInDefinitions->schemaType(), $allReferencedTypes); $this->resolvedTypes = $allReferencedTypes; $this->fullyLoaded = true; @@ -186,7 +186,8 @@ public function getTypeMap(): array */ public function getDirectives(): array { - return $this->config->directives ?? array_values($this->builtInTypes->directives()); + return $this->config->directives + ?? $this->builtInDefinitions->directives(); } /** @param mixed $typeLoaderReturn could be anything */ @@ -274,9 +275,9 @@ public function getSubscriptionType(): ?ObjectType return $subscription; } - public function getBuiltInTypes(): BuiltInTypes + public function getBuiltInDefinitions(): BuiltInDefinitions { - return $this->builtInTypes; + return $this->builtInDefinitions; } /** @api */ @@ -300,9 +301,9 @@ public function getType(string $name): ?Type return $this->resolvedTypes[$name]; } - $builtIn = $this->builtInTypes->allTypes(); - if (isset($builtIn[$name])) { - return $builtIn[$name]; + $builtInTypes = $this->builtInDefinitions->types(); + if (isset($builtInTypes[$name])) { + return $builtInTypes[$name]; } $type = $this->loadType($name); @@ -513,9 +514,8 @@ public function assertValid(): void throw new InvariantViolation(implode("\n\n", $this->validationErrors)); } - $internalTypes = $this->builtInTypes->allTypes(); foreach ($this->getTypeMap() as $name => $type) { - if (isset($internalTypes[$name])) { + if ($type->isBuiltInType()) { continue; } diff --git a/src/Type/SchemaConfig.php b/src/Type/SchemaConfig.php index dbd5b7f3e..8075b408f 100644 --- a/src/Type/SchemaConfig.php +++ b/src/Type/SchemaConfig.php @@ -37,7 +37,7 @@ * types?: Types|null, * directives?: array|null, * typeLoader?: TypeLoader|null, - * builtInTypes?: BuiltInTypes|null, + * builtInDefinitions?: BuiltInDefinitions|null, * assumeValid?: bool|null, * astNode?: SchemaDefinitionNode|null, * extensionASTNodes?: array|null, @@ -66,7 +66,7 @@ class SchemaConfig /** @var array|null */ public ?array $directives = null; - public ?BuiltInTypes $builtInTypes = null; + public ?BuiltInDefinitions $builtInDefinitions = null; /** * @var callable|null @@ -120,8 +120,8 @@ public static function create(array $options = []): self $config->setDirectives($options['directives']); } - if (isset($options['builtInTypes'])) { - $config->setBuiltInTypes($options['builtInTypes']); + if (isset($options['builtInDefinitions'])) { + $config->setBuiltInDefinitions($options['builtInDefinitions']); } if (isset($options['typeLoader'])) { @@ -281,14 +281,14 @@ public function setDirectives(?array $directives): self return $this; } - public function getBuiltInTypes(): ?BuiltInTypes + public function getBuiltInDefinitions(): ?BuiltInDefinitions { - return $this->builtInTypes; + return $this->builtInDefinitions; } - public function setBuiltInTypes(?BuiltInTypes $builtInTypes): self + public function setBuiltInDefinitions(?BuiltInDefinitions $builtInDefinitions): self { - $this->builtInTypes = $builtInTypes; + $this->builtInDefinitions = $builtInDefinitions; return $this; } diff --git a/src/Type/SchemaValidationContext.php b/src/Type/SchemaValidationContext.php index d683bdf7b..13ff8c184 100644 --- a/src/Type/SchemaValidationContext.php +++ b/src/Type/SchemaValidationContext.php @@ -186,7 +186,7 @@ private function validateName(object $object): void $error = Utils::isValidNameError($object->name, $object->astNode); if ( $error === null - || ($object instanceof Type && Introspection::isIntrospectionType($object)) + || ($object instanceof Type && BuiltInDefinitions::isIntrospectionType($object)) ) { return; } diff --git a/src/Utils/ASTDefinitionBuilder.php b/src/Utils/ASTDefinitionBuilder.php index 07764bb9d..7d135c463 100644 --- a/src/Utils/ASTDefinitionBuilder.php +++ b/src/Utils/ASTDefinitionBuilder.php @@ -29,6 +29,7 @@ use GraphQL\Language\AST\TypeNode; use GraphQL\Language\AST\UnionTypeDefinitionNode; use GraphQL\Language\AST\UnionTypeExtensionNode; +use GraphQL\Type\BuiltInDefinitions; use GraphQL\Type\Definition\CustomScalarType; use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\EnumType; @@ -81,7 +82,7 @@ class ASTDefinitionBuilder private $fieldConfigDecorator; /** @var array */ - private array $cache; + private array $typeMap; /** @var array> */ private array $typeExtensionsMap; @@ -108,7 +109,7 @@ public function __construct( $this->typeConfigDecorator = $typeConfigDecorator; $this->fieldConfigDecorator = $fieldConfigDecorator; - $this->cache = Type::builtInTypes(); + $this->typeMap = BuiltInDefinitions::standard()->types(); } /** @throws \Exception */ @@ -259,8 +260,8 @@ public function maybeBuildType(string $name): ?Type */ private function internalBuildType(string $typeName, ?Node $typeNode = null): Type { - if (isset($this->cache[$typeName])) { - return $this->cache[$typeName]; + if (isset($this->typeMap[$typeName])) { + return $this->typeMap[$typeName]; } if (isset($this->typeDefinitionsMap[$typeName])) { @@ -288,10 +289,10 @@ private function internalBuildType(string $typeName, ?Node $typeNode = null): Ty $type = $this->makeSchemaDefFromConfig($this->typeDefinitionsMap[$typeName], $config); } - return $this->cache[$typeName] = $type; + return $this->typeMap[$typeName] = $type; } - return $this->cache[$typeName] = ($this->resolveType)($typeName, $typeNode); + return $this->typeMap[$typeName] = ($this->resolveType)($typeName, $typeNode); } /** @@ -409,7 +410,7 @@ public function buildField(FieldDefinitionNode $field, object $node): array private function getDeprecationReason(Node $node): ?string { $deprecated = Values::getDirectiveValues( - Directive::deprecatedDirective(), + BuiltInDefinitions::standard()->deprecatedDirective(), $node ); @@ -547,7 +548,7 @@ private function makeInputObjectDef(InputObjectTypeDefinitionNode $def): InputOb /** @var array $extensionASTNodes (proven by schema validation) */ $extensionASTNodes = $this->typeExtensionsMap[$name] ?? []; - $oneOfDirective = Directive::oneOfDirective(); + $oneOfDirective = BuiltInDefinitions::standard()->oneOfDirective(); // Check for @oneOf directive in the definition node $isOneOf = Values::getDirectiveValues($oneOfDirective, $def) !== null; diff --git a/src/Utils/BuildClientSchema.php b/src/Utils/BuildClientSchema.php index 1136ca4ff..f0fa7ab1c 100644 --- a/src/Utils/BuildClientSchema.php +++ b/src/Utils/BuildClientSchema.php @@ -5,6 +5,7 @@ use GraphQL\Error\InvariantViolation; use GraphQL\Error\SyntaxError; use GraphQL\Language\Parser; +use GraphQL\Type\BuiltInDefinitions; use GraphQL\Type\Definition\CustomScalarType; use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\EnumType; @@ -109,7 +110,7 @@ public function buildSchema(): Schema $schemaIntrospection = $this->introspection['__schema']; - $builtInTypes = Type::builtInTypes(); + $builtInTypes = BuiltInDefinitions::standard()->types(); foreach ($schemaIntrospection['types'] as $typeIntrospection) { if (! isset($typeIntrospection['name'])) { diff --git a/src/Utils/BuildSchema.php b/src/Utils/BuildSchema.php index 669c34c47..4a1fe926f 100644 --- a/src/Utils/BuildSchema.php +++ b/src/Utils/BuildSchema.php @@ -13,7 +13,7 @@ use GraphQL\Language\AST\TypeExtensionNode; use GraphQL\Language\Parser; use GraphQL\Language\Source; -use GraphQL\Type\BuiltInTypes; +use GraphQL\Type\BuiltInDefinitions; use GraphQL\Type\Definition\Type; use GraphQL\Type\Schema; use GraphQL\Type\SchemaConfig; @@ -217,25 +217,19 @@ static function (string $typeName): Type { $directiveDefs ); + /** @var array $directivesByName */ $directivesByName = []; foreach ($directives as $directive) { - $directivesByName[$directive->name][] = $directive; + $directivesByName[$directive->name] = true; } - $builtIn = BuiltInTypes::standard(); + $builtInDirectives = BuiltInDefinitions::standard()->directives(); + foreach ($builtInDirectives as $name => $directive) { + if (isset($directivesByName[$name])) { + continue; + } - // If specified directives were not explicitly declared, add them. - if (! isset($directivesByName['include'])) { - $directives[] = $builtIn->includeDirective(); - } - if (! isset($directivesByName['skip'])) { - $directives[] = $builtIn->skipDirective(); - } - if (! isset($directivesByName['deprecated'])) { - $directives[] = $builtIn->deprecatedDirective(); - } - if (! isset($directivesByName['oneOf'])) { - $directives[] = $builtIn->oneOfDirective(); + $directives[] = $directive; } // Note: While this could make early assertions to get the correctly @@ -244,15 +238,15 @@ static function (string $typeName): Type { return new Schema( (new SchemaConfig()) ->setDescription($schemaDef->description->value ?? null) - // @phpstan-ignore-next-line + // @phpstan-ignore-next-line ->setQuery(isset($operationTypes['query']) ? $definitionBuilder->maybeBuildType($operationTypes['query']) : null) - // @phpstan-ignore-next-line + // @phpstan-ignore-next-line ->setMutation(isset($operationTypes['mutation']) ? $definitionBuilder->maybeBuildType($operationTypes['mutation']) : null) - // @phpstan-ignore-next-line + // @phpstan-ignore-next-line ->setSubscription(isset($operationTypes['subscription']) ? $definitionBuilder->maybeBuildType($operationTypes['subscription']) : null) diff --git a/src/Utils/SchemaExtender.php b/src/Utils/SchemaExtender.php index 965859251..b8cfd3770 100644 --- a/src/Utils/SchemaExtender.php +++ b/src/Utils/SchemaExtender.php @@ -32,7 +32,6 @@ use GraphQL\Type\Definition\ScalarType; use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\UnionType; -use GraphQL\Type\Introspection; use GraphQL\Type\Schema; use GraphQL\Type\SchemaConfig; use GraphQL\Validator\DocumentValidator; @@ -562,18 +561,6 @@ protected function extendInterfaceType(InterfaceType $type): InterfaceType ]); } - protected function isSpecifiedScalarType(Type $type): bool - { - return $type instanceof NamedType - && in_array($type->name, [ - Type::STRING, - Type::INT, - Type::FLOAT, - Type::BOOLEAN, - Type::ID, - ], true); - } - /** * @template T of Type * @@ -586,7 +573,7 @@ protected function isSpecifiedScalarType(Type $type): bool */ protected function extendNamedType(Type $type): Type { - if (Introspection::isIntrospectionType($type) || $this->isSpecifiedScalarType($type)) { + if ($type->isBuiltInType()) { return $type; } diff --git a/src/Utils/SchemaPrinter.php b/src/Utils/SchemaPrinter.php index f30a19f0b..4ff5ee2c2 100644 --- a/src/Utils/SchemaPrinter.php +++ b/src/Utils/SchemaPrinter.php @@ -8,6 +8,7 @@ use GraphQL\Language\AST\StringValueNode; use GraphQL\Language\BlockString; use GraphQL\Language\Printer; +use GraphQL\Type\BuiltInDefinitions; use GraphQL\Type\Definition\Argument; use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\EnumType; @@ -22,7 +23,6 @@ use GraphQL\Type\Definition\ScalarType; use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\UnionType; -use GraphQL\Type\Introspection; use GraphQL\Type\Schema; /** @@ -58,7 +58,7 @@ public static function doPrint(Schema $schema, array $options = []): string { return static::printFilteredSchema( $schema, - static fn (Directive $directive): bool => ! Directive::isSpecifiedDirective($directive), + static fn (Directive $directive): bool => ! BuiltInDefinitions::isBuiltInDirective($directive), static fn (NamedType $type): bool => ! $type->isBuiltInType(), $options ); @@ -80,8 +80,8 @@ public static function printIntrospectionSchema(Schema $schema, array $options = { return static::printFilteredSchema( $schema, - [Directive::class, 'isSpecifiedDirective'], - [Introspection::class, 'isIntrospectionType'], + [BuiltInDefinitions::class, 'isBuiltInDirective'], + [BuiltInDefinitions::class, 'isIntrospectionType'], $options ); } diff --git a/src/Utils/TypeInfo.php b/src/Utils/TypeInfo.php index e25407590..93370aa86 100644 --- a/src/Utils/TypeInfo.php +++ b/src/Utils/TypeInfo.php @@ -33,6 +33,7 @@ use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\UnionType; use GraphQL\Type\Definition\WrappingType; +use GraphQL\Type\Introspection; use GraphQL\Type\Schema; class TypeInfo @@ -337,21 +338,26 @@ public function getParentType(): ?CompositeType private static function getFieldDefinition(Schema $schema, Type $parentType, FieldNode $fieldNode): ?FieldDefinition { $name = $fieldNode->name->value; - $builtIn = $schema->getBuiltInTypes(); - $schemaMeta = $builtIn->schemaMetaFieldDef(); - if ($name === $schemaMeta->name && $schema->getQueryType() === $parentType) { - return $schemaMeta; + if ($name === Introspection::SCHEMA_FIELD_NAME + && $schema->getQueryType() === $parentType + ) { + return $schema->getBuiltInDefinitions() + ->schemaMetaFieldDef(); } - $typeMeta = $builtIn->typeMetaFieldDef(); - if ($name === $typeMeta->name && $schema->getQueryType() === $parentType) { - return $typeMeta; + if ($name === Introspection::TYPE_FIELD_NAME + && $schema->getQueryType() === $parentType + ) { + return $schema->getBuiltInDefinitions() + ->typeMetaFieldDef(); } - $typeNameMeta = $builtIn->typeNameMetaFieldDef(); - if ($name === $typeNameMeta->name && $parentType instanceof CompositeType) { - return $typeNameMeta; + if ($name === Introspection::TYPE_NAME_FIELD_NAME + && $parentType instanceof CompositeType + ) { + return $schema->getBuiltInDefinitions() + ->typeNameMetaFieldDef(); } if ( diff --git a/src/Validator/Rules/KnownArgumentNamesOnDirectives.php b/src/Validator/Rules/KnownArgumentNamesOnDirectives.php index 036ff0632..28906574f 100644 --- a/src/Validator/Rules/KnownArgumentNamesOnDirectives.php +++ b/src/Validator/Rules/KnownArgumentNamesOnDirectives.php @@ -9,8 +9,8 @@ use GraphQL\Language\AST\NodeKind; use GraphQL\Language\Visitor; use GraphQL\Language\VisitorOperation; +use GraphQL\Type\BuiltInDefinitions; use GraphQL\Type\Definition\Argument; -use GraphQL\Type\Definition\Directive; use GraphQL\Utils\Utils; use GraphQL\Validator\QueryValidationContext; use GraphQL\Validator\SDLValidationContext; @@ -58,11 +58,13 @@ public function getVisitor(QueryValidationContext $context): array */ public function getASTVisitor(ValidationContext $context): array { + /** @var array> $directiveArgs */ $directiveArgs = []; + $schema = $context->getSchema(); $definedDirectives = $schema !== null ? $schema->getDirectives() - : Directive::getInternalDirectives(); + : BuiltInDefinitions::standard()->directives(); foreach ($definedDirectives as $directive) { $directiveArgs[$directive->name] = array_map( diff --git a/src/Validator/Rules/KnownDirectives.php b/src/Validator/Rules/KnownDirectives.php index 808e9e27c..bd7db61d6 100644 --- a/src/Validator/Rules/KnownDirectives.php +++ b/src/Validator/Rules/KnownDirectives.php @@ -34,7 +34,7 @@ use GraphQL\Language\AST\VariableDefinitionNode; use GraphQL\Language\DirectiveLocation; use GraphQL\Language\Visitor; -use GraphQL\Type\Definition\Directive; +use GraphQL\Type\BuiltInDefinitions; use GraphQL\Validator\QueryValidationContext; use GraphQL\Validator\SDLValidationContext; use GraphQL\Validator\ValidationContext; @@ -63,11 +63,13 @@ public function getSDLVisitor(SDLValidationContext $context): array */ public function getASTVisitor(ValidationContext $context): array { + /** @var array> $locationsMap */ $locationsMap = []; + $schema = $context->getSchema(); - $definedDirectives = $schema === null - ? Directive::getInternalDirectives() - : $schema->getDirectives(); + $definedDirectives = $schema !== null + ? $schema->getDirectives() + : BuiltInDefinitions::standard()->directives(); foreach ($definedDirectives as $directive) { $locationsMap[$directive->name] = $directive->locations; diff --git a/src/Validator/Rules/KnownTypeNames.php b/src/Validator/Rules/KnownTypeNames.php index 93fd019b7..9165002ff 100644 --- a/src/Validator/Rules/KnownTypeNames.php +++ b/src/Validator/Rules/KnownTypeNames.php @@ -8,7 +8,7 @@ use GraphQL\Language\AST\TypeDefinitionNode; use GraphQL\Language\AST\TypeSystemDefinitionNode; use GraphQL\Language\AST\TypeSystemExtensionNode; -use GraphQL\Type\Definition\Type; +use GraphQL\Type\BuiltInDefinitions; use GraphQL\Utils\Utils; use GraphQL\Validator\QueryValidationContext; use GraphQL\Validator\SDLValidationContext; @@ -60,7 +60,7 @@ public function getASTVisitor(ValidationContext $context): array $definitionNode = $ancestors[2] ?? $parent; $isSDL = $definitionNode instanceof TypeSystemDefinitionNode || $definitionNode instanceof TypeSystemExtensionNode; - if ($isSDL && in_array($typeName, Type::BUILT_IN_TYPE_NAMES, true)) { + if ($isSDL && BuiltInDefinitions::isBuiltInTypeName($typeName)) { return; } @@ -77,7 +77,7 @@ public function getASTVisitor(ValidationContext $context): array Utils::suggestionList( $typeName, $isSDL - ? [...Type::BUILT_IN_TYPE_NAMES, ...$typeNames] + ? [...BuiltInDefinitions::BUILT_IN_TYPE_NAMES, ...$typeNames] : $typeNames ) ), diff --git a/src/Validator/Rules/ProvidedRequiredArgumentsOnDirectives.php b/src/Validator/Rules/ProvidedRequiredArgumentsOnDirectives.php index a903ad9a6..dfde6ffaa 100644 --- a/src/Validator/Rules/ProvidedRequiredArgumentsOnDirectives.php +++ b/src/Validator/Rules/ProvidedRequiredArgumentsOnDirectives.php @@ -10,8 +10,8 @@ use GraphQL\Language\AST\NonNullTypeNode; use GraphQL\Language\Printer; use GraphQL\Language\Visitor; +use GraphQL\Type\BuiltInDefinitions; use GraphQL\Type\Definition\Argument; -use GraphQL\Type\Definition\Directive; use GraphQL\Validator\QueryValidationContext; use GraphQL\Validator\SDLValidationContext; use GraphQL\Validator\ValidationContext; @@ -54,11 +54,13 @@ public function getVisitor(QueryValidationContext $context): array */ public function getASTVisitor(ValidationContext $context): array { + /** @var array> $requiredArgsMap */ $requiredArgsMap = []; + $schema = $context->getSchema(); - $definedDirectives = $schema === null - ? Directive::getInternalDirectives() - : $schema->getDirectives(); + $definedDirectives = $schema !== null + ? $schema->getDirectives() + : BuiltInDefinitions::standard()->directives(); foreach ($definedDirectives as $directive) { $directiveArgs = []; diff --git a/src/Validator/Rules/QueryComplexity.php b/src/Validator/Rules/QueryComplexity.php index 9d5a6bf12..9a05e883b 100644 --- a/src/Validator/Rules/QueryComplexity.php +++ b/src/Validator/Rules/QueryComplexity.php @@ -17,6 +17,7 @@ use GraphQL\Language\AST\VariableDefinitionNode; use GraphQL\Language\Visitor; use GraphQL\Language\VisitorOperation; +use GraphQL\Type\BuiltInDefinitions; use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\FieldDefinition; use GraphQL\Type\Introspection; @@ -197,7 +198,7 @@ protected function directiveExcludesField(FieldNode $node): bool if ($directiveNode->name->value === Directive::INCLUDE_NAME) { $includeArguments = Values::getArgumentValues( - Directive::includeDirective(), + BuiltInDefinitions::standard()->includeDirective(), $directiveNode, $variableValues ); @@ -208,7 +209,7 @@ protected function directiveExcludesField(FieldNode $node): bool if ($directiveNode->name->value === Directive::SKIP_NAME) { $skipArguments = Values::getArgumentValues( - Directive::skipDirective(), + BuiltInDefinitions::standard()->skipDirective(), $directiveNode, $variableValues ); diff --git a/src/Validator/Rules/QuerySecurityRule.php b/src/Validator/Rules/QuerySecurityRule.php index 0d118e4b7..780f5a6fc 100644 --- a/src/Validator/Rules/QuerySecurityRule.php +++ b/src/Validator/Rules/QuerySecurityRule.php @@ -114,10 +114,10 @@ protected function collectFieldASTsAndDefs( $fieldDef = null; if ($parentType instanceof HasFieldsType) { - $builtIn = $context->getSchema()->getBuiltInTypes(); - $schemaMetaFieldDef = $builtIn->schemaMetaFieldDef(); - $typeMetaFieldDef = $builtIn->typeMetaFieldDef(); - $typeNameMetaFieldDef = $builtIn->typeNameMetaFieldDef(); + $builtInDefinitions = $context->getSchema()->getBuiltInDefinitions(); + $schemaMetaFieldDef = $builtInDefinitions->schemaMetaFieldDef(); + $typeMetaFieldDef = $builtInDefinitions->typeMetaFieldDef(); + $typeNameMetaFieldDef = $builtInDefinitions->typeNameMetaFieldDef(); $queryType = $context->getSchema()->getQueryType(); diff --git a/src/Validator/Rules/UniqueDirectivesPerLocation.php b/src/Validator/Rules/UniqueDirectivesPerLocation.php index 8cbc028d5..62d255cf8 100644 --- a/src/Validator/Rules/UniqueDirectivesPerLocation.php +++ b/src/Validator/Rules/UniqueDirectivesPerLocation.php @@ -7,6 +7,7 @@ use GraphQL\Language\AST\DirectiveDefinitionNode; use GraphQL\Language\AST\Node; use GraphQL\Language\Visitor; +use GraphQL\Type\BuiltInDefinitions; use GraphQL\Type\Definition\Directive; use GraphQL\Validator\QueryValidationContext; use GraphQL\Validator\SDLValidationContext; @@ -47,7 +48,8 @@ public function getASTVisitor(ValidationContext $context): array $schema = $context->getSchema(); $definedDirectives = $schema !== null ? $schema->getDirectives() - : Directive::getInternalDirectives(); + : BuiltInDefinitions::standard()->directives(); + foreach ($definedDirectives as $directive) { if (! $directive->isRepeatable) { $uniqueDirectiveMap[$directive->name] = true; diff --git a/tests/Type/BuiltInTypesTest.php b/tests/Type/BuiltInDefinitionsTest.php similarity index 51% rename from tests/Type/BuiltInTypesTest.php rename to tests/Type/BuiltInDefinitionsTest.php index 2e55c047a..2cb75226a 100644 --- a/tests/Type/BuiltInTypesTest.php +++ b/tests/Type/BuiltInDefinitionsTest.php @@ -2,7 +2,7 @@ namespace GraphQL\Tests\Type; -use GraphQL\Type\BuiltInTypes; +use GraphQL\Type\BuiltInDefinitions; use GraphQL\Type\Definition\CustomScalarType; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type; @@ -10,52 +10,30 @@ use GraphQL\Type\SchemaConfig; use PHPUnit\Framework\TestCase; -final class BuiltInTypesTest extends TestCase +final class BuiltInDefinitionsTest extends TestCase { - /** @var array */ - private static array $originalStandardTypes; - - public static function setUpBeforeClass(): void - { - self::$originalStandardTypes = Type::getStandardTypes(); - } - - public function tearDown(): void - { - parent::tearDown(); - Type::overrideStandardTypes(self::$originalStandardTypes); - } - public function testStandardReturnsSingleton(): void { - $a = BuiltInTypes::standard(); - $b = BuiltInTypes::standard(); + $a = BuiltInDefinitions::standard(); + $b = BuiltInDefinitions::standard(); self::assertSame($a, $b); } - public function testResetStandardClearsSingleton(): void - { - $before = BuiltInTypes::standard(); - BuiltInTypes::resetStandard(); - $after = BuiltInTypes::standard(); - self::assertNotSame($before, $after); - } - public function testDefaultInstanceMatchesStaticTypes(): void { - $builtIn = BuiltInTypes::standard(); + $builtInDefinitions = BuiltInDefinitions::standard(); - self::assertSame(Type::int(), $builtIn->int()); - self::assertSame(Type::float(), $builtIn->float()); - self::assertSame(Type::string(), $builtIn->string()); - self::assertSame(Type::boolean(), $builtIn->boolean()); - self::assertSame(Type::id(), $builtIn->id()); + self::assertSame(Type::int(), $builtInDefinitions->int()); + self::assertSame(Type::float(), $builtInDefinitions->float()); + self::assertSame(Type::string(), $builtInDefinitions->string()); + self::assertSame(Type::boolean(), $builtInDefinitions->boolean()); + self::assertSame(Type::id(), $builtInDefinitions->id()); } public function testTwoInstancesProduceDistinctTypes(): void { - $a = new BuiltInTypes(); - $b = new BuiltInTypes(); + $a = new BuiltInDefinitions(); + $b = new BuiltInDefinitions(); self::assertNotSame($a->schemaType(), $b->schemaType()); self::assertNotSame($a->typeType(), $b->typeType()); @@ -70,58 +48,58 @@ public function testCustomScalarOverridePropagates(): void 'serialize' => static fn ($value) => $value, ]); - $builtIn = new BuiltInTypes([Type::STRING => $customString]); + $builtInDefinitions = new BuiltInDefinitions([Type::STRING => $customString]); - self::assertSame($customString, $builtIn->string()); - self::assertSame($customString, $builtIn->standardTypes()[Type::STRING]); - self::assertSame($customString, $builtIn->allTypes()[Type::STRING]); + self::assertSame($customString, $builtInDefinitions->string()); + self::assertSame($customString, $builtInDefinitions->scalarTypes()[Type::STRING]); + self::assertSame($customString, $builtInDefinitions->types()[Type::STRING]); - $typeNameMeta = $builtIn->typeNameMetaFieldDef(); + $typeNameMeta = $builtInDefinitions->typeNameMetaFieldDef(); $innerType = Type::getNullableType($typeNameMeta->getType()); self::assertSame($customString, $innerType); - $deprecatedDirective = $builtIn->deprecatedDirective(); + $deprecatedDirective = $builtInDefinitions->deprecatedDirective(); self::assertSame($customString, $deprecatedDirective->args[0]->getType()); } - public function testTwoSchemasWithDifferentBuiltInTypes(): void + public function testTwoSchemasWithDifferentBuiltInDefinitions(): void { $queryType = new ObjectType([ 'name' => 'Query', 'fields' => ['hello' => Type::string()], ]); - $builtInA = new BuiltInTypes(); - $builtInB = new BuiltInTypes(); + $builtInDefinitionsA = new BuiltInDefinitions(); + $builtInDefinitionsB = new BuiltInDefinitions(); $schemaA = new Schema( (new SchemaConfig()) ->setQuery($queryType) - ->setBuiltInTypes($builtInA) + ->setBuiltInDefinitions($builtInDefinitionsA) ->setAssumeValid(true) ); $schemaB = new Schema( (new SchemaConfig()) ->setQuery($queryType) - ->setBuiltInTypes($builtInB) + ->setBuiltInDefinitions($builtInDefinitionsB) ->setAssumeValid(true) ); - self::assertSame($builtInA, $schemaA->getBuiltInTypes()); - self::assertSame($builtInB, $schemaB->getBuiltInTypes()); - self::assertNotSame($schemaA->getBuiltInTypes(), $schemaB->getBuiltInTypes()); + self::assertSame($builtInDefinitionsA, $schemaA->getBuiltInDefinitions()); + self::assertSame($builtInDefinitionsB, $schemaB->getBuiltInDefinitions()); + self::assertNotSame($schemaA->getBuiltInDefinitions(), $schemaB->getBuiltInDefinitions()); self::assertNotSame( - $schemaA->getBuiltInTypes()->schemaType(), - $schemaB->getBuiltInTypes()->schemaType() + $schemaA->getBuiltInDefinitions()->schemaType(), + $schemaB->getBuiltInDefinitions()->schemaType() ); } public function testAllTypesContainsScalarsAndIntrospection(): void { - $builtIn = new BuiltInTypes(); - $allTypes = $builtIn->allTypes(); + $builtInDefinitions = new BuiltInDefinitions(); + $allTypes = $builtInDefinitions->types(); self::assertArrayHasKey('Int', $allTypes); self::assertArrayHasKey('String', $allTypes); @@ -133,8 +111,8 @@ public function testAllTypesContainsScalarsAndIntrospection(): void public function testDirectivesReturnsAllStandard(): void { - $builtIn = new BuiltInTypes(); - $directives = $builtIn->directives(); + $builtInDefinitions = new BuiltInDefinitions(); + $directives = $builtInDefinitions->directives(); self::assertArrayHasKey('include', $directives); self::assertArrayHasKey('skip', $directives); diff --git a/tests/Type/StandardTypesTest.php b/tests/Type/StandardTypesTest.php index 6a569f45e..eb3f15dba 100644 --- a/tests/Type/StandardTypesTest.php +++ b/tests/Type/StandardTypesTest.php @@ -3,35 +3,35 @@ namespace GraphQL\Tests\Type; use GraphQL\Error\InvariantViolation; +use GraphQL\Type\BuiltInDefinitions; 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 { /** @var array */ - private static array $originalStandardTypes; + private static array $originalScalarTypes; public static function setUpBeforeClass(): void { - self::$originalStandardTypes = Type::getStandardTypes(); + self::$originalScalarTypes = BuiltInDefinitions::standard()->scalarTypes(); } public function tearDown(): void { parent::tearDown(); - Type::overrideStandardTypes(self::$originalStandardTypes); + + BuiltInDefinitions::overrideScalarTypes(self::$originalScalarTypes); } - public function testAllowsOverridingStandardTypes(): void + public function testAllowsOverridingScalarTypes(): void { - $originalTypes = Type::getStandardTypes(); - self::assertCount(5, $originalTypes); - self::assertSame(self::$originalStandardTypes, $originalTypes); + $originalScalarTypes = BuiltInDefinitions::standard()->scalarTypes(); + self::assertCount(5, $originalScalarTypes); + self::assertSame(self::$originalScalarTypes, $originalScalarTypes); $newBooleanType = self::createCustomScalarType(Type::BOOLEAN); $newFloatType = self::createCustomScalarType(Type::FLOAT); @@ -39,7 +39,7 @@ public function testAllowsOverridingStandardTypes(): void $newIntType = self::createCustomScalarType(Type::INT); $newStringType = self::createCustomScalarType(Type::STRING); - Type::overrideStandardTypes([ + BuiltInDefinitions::overrideScalarTypes([ $newBooleanType, $newFloatType, $newIDType, @@ -47,7 +47,7 @@ public function testAllowsOverridingStandardTypes(): void $newStringType, ]); - $types = Type::getStandardTypes(); + $types = BuiltInDefinitions::standard()->scalarTypes(); self::assertCount(5, $types); self::assertSame($newBooleanType, $types[Type::BOOLEAN]); @@ -63,21 +63,21 @@ public function testAllowsOverridingStandardTypes(): void self::assertSame($newStringType, Type::string()); } - public function testPreservesOriginalStandardTypes(): void + public function testPreservesOriginalScalarTypes(): void { - $originalTypes = Type::getStandardTypes(); + $originalTypes = BuiltInDefinitions::standard()->scalarTypes(); self::assertCount(5, $originalTypes); - self::assertSame(self::$originalStandardTypes, $originalTypes); + self::assertSame(self::$originalScalarTypes, $originalTypes); $newIDType = self::createCustomScalarType(Type::ID); $newStringType = self::createCustomScalarType(Type::STRING); - Type::overrideStandardTypes([ + BuiltInDefinitions::overrideScalarTypes([ $newStringType, $newIDType, ]); - $types = Type::getStandardTypes(); + $types = BuiltInDefinitions::standard()->scalarTypes(); self::assertCount(5, $types); self::assertSame($originalTypes[Type::BOOLEAN], $types[Type::BOOLEAN]); @@ -100,7 +100,7 @@ public function testPreservesOriginalStandardTypes(): void * * @return iterable */ - public static function invalidStandardTypes(): iterable + public static function invalidScalarTypes(): iterable { yield [null, 'Expecting instance of GraphQL\Type\Definition\ScalarType, got null']; yield [5, 'Expecting instance of GraphQL\Type\Definition\ScalarType, got 5']; @@ -108,41 +108,41 @@ public static function invalidStandardTypes(): iterable yield [new \stdClass(), 'Expecting instance of GraphQL\Type\Definition\ScalarType, got instance of stdClass']; yield [[], 'Expecting instance of GraphQL\Type\Definition\ScalarType, got []']; yield [new ObjectType(['name' => 'ID', 'fields' => []]), 'Expecting instance of GraphQL\Type\Definition\ScalarType, got ID']; - yield [self::createCustomScalarType('NonStandardName'), 'Expecting one of the following names for a standard type: Int, Float, String, Boolean, ID; got "NonStandardName"']; + yield [self::createCustomScalarType('NonStandardName'), 'Expecting one of the following names for a built-in scalar type: Int, Float, String, Boolean, ID; got "NonStandardName"']; } /** * @param mixed $notType invalid type * - * @dataProvider invalidStandardTypes + * @dataProvider invalidScalarTypes */ - public function testStandardTypesOverrideDoesSanityChecks($notType, string $expectedMessage): void + public function testScalarTypesOverrideDoesSanityChecks($notType, string $expectedMessage): void { $this->expectException(InvariantViolation::class); $this->expectExceptionMessage($expectedMessage); - Type::overrideStandardTypes([$notType]); + BuiltInDefinitions::overrideScalarTypes([$notType]); } - public function testCachesShouldResetWhenOverridingStandardTypes(): void + public function testCachesShouldResetWhenOverridingScalarTypes(): void { $string = Type::string(); - $typeNameMetaFieldDef = Introspection::typeNameMetaFieldDef(); + $typeNameMetaFieldDef = BuiltInDefinitions::standard()->typeNameMetaFieldDef(); self::assertSame($string, Type::getNullableType($typeNameMetaFieldDef->getType())); - $deprecatedDirective = Directive::deprecatedDirective(); + $deprecatedDirective = BuiltInDefinitions::standard()->deprecatedDirective(); self::assertSame($string, $deprecatedDirective->args[0]->getType()); $newString = self::createCustomScalarType(Type::STRING); self::assertNotSame($string, $newString); - Type::overrideStandardTypes([$newString]); + BuiltInDefinitions::overrideScalarTypes([$newString]); - $newTypeNameMetaFieldDef = Introspection::typeNameMetaFieldDef(); + $newTypeNameMetaFieldDef = BuiltInDefinitions::standard()->typeNameMetaFieldDef(); self::assertNotSame($typeNameMetaFieldDef, $newTypeNameMetaFieldDef); self::assertSame($newString, Type::getNullableType($newTypeNameMetaFieldDef->getType())); - $newDeprecatedDirective = Directive::deprecatedDirective(); + $newDeprecatedDirective = BuiltInDefinitions::standard()->deprecatedDirective(); self::assertNotSame($deprecatedDirective, $newDeprecatedDirective); self::assertSame($newString, $newDeprecatedDirective->args[0]->getType()); } diff --git a/tests/Utils/BreakingChangesFinderTest.php b/tests/Utils/BreakingChangesFinderTest.php index 2f4a6c760..7824ead46 100644 --- a/tests/Utils/BreakingChangesFinderTest.php +++ b/tests/Utils/BreakingChangesFinderTest.php @@ -3,6 +3,7 @@ namespace GraphQL\Tests\Utils; use GraphQL\Language\DirectiveLocation; +use GraphQL\Type\BuiltInDefinitions; use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\EnumType; use GraphQL\Type\Definition\InputObjectType; @@ -1161,7 +1162,7 @@ public function testShouldDetectAllBreakingChanges(): void ], ]); - $directiveThatIsRemoved = Directive::skipDirective(); + $directiveThatIsRemoved = BuiltInDefinitions::standard()->skipDirective(); $directiveThatRemovesArgOld = new Directive([ 'name' => 'DirectiveThatRemovesArg', 'locations' => [DirectiveLocation::FIELD_DEFINITION], @@ -1303,14 +1304,14 @@ public function testShouldDetectAllBreakingChanges(): void public function testShouldDetectIfADirectiveWasExplicitlyRemoved(): void { $oldSchema = new Schema([ - 'directives' => [Directive::skipDirective(), Directive::includeDirective()], + 'directives' => [BuiltInDefinitions::standard()->skipDirective(), BuiltInDefinitions::standard()->includeDirective()], ]); $newSchema = new Schema([ - 'directives' => [Directive::skipDirective()], + 'directives' => [BuiltInDefinitions::standard()->skipDirective()], ]); - $includeDirective = Directive::includeDirective(); + $includeDirective = BuiltInDefinitions::standard()->includeDirective(); self::assertEquals( [ @@ -1329,11 +1330,11 @@ public function testShouldDetectIfADirectiveWasImplicitlyRemoved(): void $oldSchema = new Schema([]); $newSchema = new Schema([ - 'directives' => [Directive::skipDirective(), Directive::includeDirective()], + 'directives' => [BuiltInDefinitions::standard()->skipDirective(), BuiltInDefinitions::standard()->includeDirective()], ]); - $deprecatedDirective = Directive::deprecatedDirective(); - $oneOfDirective = Directive::oneOfDirective(); + $deprecatedDirective = BuiltInDefinitions::standard()->deprecatedDirective(); + $oneOfDirective = BuiltInDefinitions::standard()->oneOfDirective(); self::assertEquals( [ diff --git a/tests/Utils/BuildSchemaTest.php b/tests/Utils/BuildSchemaTest.php index 877fdb3ff..34541fb42 100644 --- a/tests/Utils/BuildSchemaTest.php +++ b/tests/Utils/BuildSchemaTest.php @@ -24,6 +24,7 @@ use GraphQL\Language\Parser; use GraphQL\Language\Printer; use GraphQL\Tests\TestCaseBase; +use GraphQL\Type\BuiltInDefinitions; use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\EnumType; use GraphQL\Type\Definition\EnumValueDefinition; @@ -36,7 +37,6 @@ use GraphQL\Type\Definition\StringType; use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\UnionType; -use GraphQL\Type\Introspection; use GraphQL\Type\Schema; use GraphQL\Utils\BuildSchema; use GraphQL\Utils\SchemaPrinter; @@ -279,10 +279,10 @@ public function testMaintainsIncludeSkipAndSpecifiedBy(): void // TODO switch to 5 when adding @specifiedBy - see https://github.com/webonyx/graphql-php/issues/1140 self::assertCount(4, $schema->getDirectives()); - self::assertSame(Directive::skipDirective(), $schema->getDirective('skip')); - self::assertSame(Directive::includeDirective(), $schema->getDirective('include')); - self::assertSame(Directive::deprecatedDirective(), $schema->getDirective('deprecated')); - self::assertSame(Directive::oneOfDirective(), $schema->getDirective('oneOf')); + self::assertSame(BuiltInDefinitions::standard()->skipDirective(), $schema->getDirective('skip')); + self::assertSame(BuiltInDefinitions::standard()->includeDirective(), $schema->getDirective('include')); + self::assertSame(BuiltInDefinitions::standard()->deprecatedDirective(), $schema->getDirective('deprecated')); + self::assertSame(BuiltInDefinitions::standard()->oneOfDirective(), $schema->getDirective('oneOf')); self::markTestIncomplete('See https://github.com/webonyx/graphql-php/issues/1140'); self::assertSame(Directive::specifiedByDirective(), $schema->getDirective('specifiedBy')); @@ -299,10 +299,10 @@ public function testOverridingDirectivesExcludesSpecified(): void ')); self::assertCount(5, $schema->getDirectives()); - self::assertNotEquals(Directive::skipDirective(), $schema->getDirective('skip')); - self::assertNotEquals(Directive::includeDirective(), $schema->getDirective('include')); - self::assertNotEquals(Directive::deprecatedDirective(), $schema->getDirective('deprecated')); - self::assertSame(Directive::oneOfDirective(), $schema->getDirective('oneOf')); + self::assertNotEquals(BuiltInDefinitions::standard()->skipDirective(), $schema->getDirective('skip')); + self::assertNotEquals(BuiltInDefinitions::standard()->includeDirective(), $schema->getDirective('include')); + self::assertNotEquals(BuiltInDefinitions::standard()->deprecatedDirective(), $schema->getDirective('deprecated')); + self::assertSame(BuiltInDefinitions::standard()->oneOfDirective(), $schema->getDirective('oneOf')); self::markTestIncomplete('See https://github.com/webonyx/graphql-php/issues/1140'); self::assertNotEquals(Directive::specifiedByDirective(), $schema->getDirective('specifiedBy')); @@ -1263,7 +1263,7 @@ public function testDoNotOverrideStandardTypes(): void '); self::assertSame(Type::id(), $schema->getType('ID')); - self::assertSame(Introspection::_schema(), $schema->getType('__Schema')); + self::assertSame(BuiltInDefinitions::standard()->schemaType(), $schema->getType('__Schema')); } /** @see it('Allows to reference introspection types') */ @@ -1280,7 +1280,7 @@ public function testAllowsToReferenceIntrospectionTypes(): void $type = $queryType->getField('introspectionField')->getType(); self::assertInstanceOf(ObjectType::class, $type); self::assertSame('__EnumValue', $type->name); - self::assertSame(Introspection::_enumValue(), $schema->getType('__EnumValue')); + self::assertSame(BuiltInDefinitions::standard()->enumValueType(), $schema->getType('__EnumValue')); } /** @see it('Rejects invalid SDL') */ diff --git a/tests/Validator/ValidatorTestCase.php b/tests/Validator/ValidatorTestCase.php index eb006cfc0..938ca3ef8 100644 --- a/tests/Validator/ValidatorTestCase.php +++ b/tests/Validator/ValidatorTestCase.php @@ -10,6 +10,7 @@ use GraphQL\Language\AST\Node; use GraphQL\Language\DirectiveLocation; use GraphQL\Language\Parser; +use GraphQL\Type\BuiltInDefinitions; use GraphQL\Type\Definition\CustomScalarType; use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\EnumType; @@ -368,9 +369,9 @@ public static function getTestSchema(): Schema 'query' => $queryRoot, 'subscription' => $subscriptionRoot, 'directives' => [ - Directive::includeDirective(), - Directive::skipDirective(), - Directive::deprecatedDirective(), + BuiltInDefinitions::standard()->includeDirective(), + BuiltInDefinitions::standard()->skipDirective(), + BuiltInDefinitions::standard()->deprecatedDirective(), new Directive([ 'name' => 'directive', 'locations' => [DirectiveLocation::FIELD, DirectiveLocation::FRAGMENT_DEFINITION], From 82c120b4774f85871b3b193b1b023d956c5ceb2a Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Sun, 8 Mar 2026 21:33:05 +0100 Subject: [PATCH 4/8] Add failing test cases --- tests/Type/BuiltInDefinitionsTest.php | 180 +++++++++++++++++++++++++- 1 file changed, 178 insertions(+), 2 deletions(-) diff --git a/tests/Type/BuiltInDefinitionsTest.php b/tests/Type/BuiltInDefinitionsTest.php index 2cb75226a..187bf4c5d 100644 --- a/tests/Type/BuiltInDefinitionsTest.php +++ b/tests/Type/BuiltInDefinitionsTest.php @@ -2,6 +2,7 @@ namespace GraphQL\Tests\Type; +use GraphQL\GraphQL; use GraphQL\Type\BuiltInDefinitions; use GraphQL\Type\Definition\CustomScalarType; use GraphQL\Type\Definition\ObjectType; @@ -76,14 +77,12 @@ public function testTwoSchemasWithDifferentBuiltInDefinitions(): void (new SchemaConfig()) ->setQuery($queryType) ->setBuiltInDefinitions($builtInDefinitionsA) - ->setAssumeValid(true) ); $schemaB = new Schema( (new SchemaConfig()) ->setQuery($queryType) ->setBuiltInDefinitions($builtInDefinitionsB) - ->setAssumeValid(true) ); self::assertSame($builtInDefinitionsA, $schemaA->getBuiltInDefinitions()); @@ -96,6 +95,47 @@ public function testTwoSchemasWithDifferentBuiltInDefinitions(): void ); } + /** + * The type isolation case — no scalar overrides, just fresh BuiltInDefinitions instances + * for each schema — already breaks at runtime when setAssumeValid(true) is used to skip + * validation. A field typed via Type::string() holds the global singleton StringType, + * but the schema's BuiltInDefinitions owns a different StringType instance. The executor + * detects the mismatch and rejects the field with a "Found duplicate type" error. + * + * This is the scenario described in https://github.com/webonyx/graphql-php/issues/1424: + * schemas that should be independent still collide through built-in type instances. + * + * This test documents that failure. It should pass once the issue is resolved. + */ + public function testIsolatedSchemaFailsAtRuntime(): void + { + $queryType = new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'hello' => [ + 'type' => Type::string(), // natural, naive usage + 'resolve' => static fn (): string => 'world', + ], + ], + ]); + + $schema = new Schema( + (new SchemaConfig()) + ->setQuery($queryType) + ->setBuiltInDefinitions(new BuiltInDefinitions()) // no overrides, just isolation + ->setAssumeValid(true) // skip validation; the real problem surfaces at runtime + ); + + $result = GraphQL::executeQuery($schema, '{ hello }'); + + // Expects no errors and hello => 'world'. + // Actual: field error "Found duplicate type in schema at Query.hello: String" + // because Type::string() (singleton) != new BuiltInDefinitions()->string() (fresh instance). + self::assertSame([], $result->errors); + assert(is_array($result->data)); + self::assertSame('world', $result->data['hello']); + } + public function testAllTypesContainsScalarsAndIntrospection(): void { $builtInDefinitions = new BuiltInDefinitions(); @@ -120,4 +160,140 @@ public function testDirectivesReturnsAllStandard(): void self::assertArrayHasKey('oneOf', $directives); self::assertCount(4, $directives); } + + /** + * In production PHP (zend.assertions = -1), the executor's runtime type-identity + * assert() at ReferenceExecutor.php is compiled out. setAssumeValid(true) already + * skips schema validation. With both disabled, execution completes without error — + * but silently uses the singleton StringType's serialize(), not the custom one. + * The result is wrong and there is no indication that the override was ignored. + * + * This test simulates that scenario by disabling assert.active at runtime. + * It documents that failure. It should pass once the issue is resolved. + */ + public function testNaiveScalarOverrideIsIgnoredSilentlyInProduction(): void + { + $upperString = new CustomScalarType([ + 'name' => Type::STRING, + 'serialize' => static fn ($value) => strtoupper((string) $value), + ]); + + $builtInDefs = new BuiltInDefinitions([Type::STRING => $upperString]); + + $queryType = new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'greeting' => [ + 'type' => Type::string(), // natural, naive usage + 'resolve' => static fn (): string => 'hello world', + ], + ], + ]); + + $schema = new Schema( + (new SchemaConfig()) + ->setQuery($queryType) + ->setBuiltInDefinitions($builtInDefs) + ->setAssumeValid(true) + ); + + $prevAssertActive = ini_get('assert.active'); + // ini_set('assert.active') is deprecated in PHP 8.3 but still functional; + // we use it here only to simulate production mode (zend.assertions = -1). + @ini_set('assert.active', '0'); + try { + $result = GraphQL::executeQuery($schema, '{ greeting }'); + } finally { + @ini_set('assert.active', $prevAssertActive); + } + + // No errors — setAssumeValid skips schema validation and the disabled assert + // suppresses the executor's runtime identity check. Execution "succeeds". + self::assertSame([], $result->errors); + assert(is_array($result->data)); + // Expects 'HELLO WORLD' from the custom String serializer. + // Actual: 'hello world' — the field holds a reference to the singleton StringType, + // which is what the executor calls serialize() on. The override is silently lost. + self::assertSame('HELLO WORLD', $result->data['greeting']); + } + + /** + * A user naturally reaches for Type::string() when defining fields — the same way every + * example and doc in this repository does. When they pair that with a BuiltInDefinitions + * override they expect the custom scalar to take effect. Instead, schema validation blows + * up because two different StringType instances with the same name end up in the type map. + * + * This test documents that failure. It should pass once the issue is resolved. + */ + public function testNaiveScalarOverridePassesValidation(): void + { + $upperString = new CustomScalarType([ + 'name' => Type::STRING, + 'serialize' => static fn ($value) => strtoupper((string) $value), + ]); + + $builtInDefs = new BuiltInDefinitions([Type::STRING => $upperString]); + + $queryType = new ObjectType([ + 'name' => 'Query', + 'fields' => ['greeting' => Type::string()], // natural, naive usage + ]); + + $schema = new Schema( + (new SchemaConfig()) + ->setQuery($queryType) + ->setBuiltInDefinitions($builtInDefs) + ); + + // Fails: "Schema must contain unique named types but contains multiple types named "String"" + $schema->assertValid(); + } + + /** + * Even when validation is skipped, the custom String scalar is never invoked at runtime. + * Fields defined via Type::string() hold a direct reference to the singleton StringType. + * The executor detects that the field's type instance differs from the schema's String + * (the two instances have the same name but are not identical), and rejects the field + * with: "Found duplicate type in schema at Query.greeting: String.". + * + * The field resolves to null with a GraphQL error — not even falling back to the + * singleton's standard serialization, let alone invoking the custom one. + * + * This test documents that failure. It should pass once the issue is resolved. + */ + public function testNaiveScalarOverrideIsUsedAtRuntime(): void + { + $upperString = new CustomScalarType([ + 'name' => Type::STRING, + 'serialize' => static fn ($value) => strtoupper((string) $value), + ]); + + $builtInDefs = new BuiltInDefinitions([Type::STRING => $upperString]); + + $queryType = new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'greeting' => [ + 'type' => Type::string(), // natural, naive usage + 'resolve' => static fn (): string => 'hello world', + ], + ], + ]); + + $schema = new Schema( + (new SchemaConfig()) + ->setQuery($queryType) + ->setBuiltInDefinitions($builtInDefs) + ->setAssumeValid(true) // skip schema validation to reach execution + ); + + $result = GraphQL::executeQuery($schema, '{ greeting }'); + + // Expects no errors and 'HELLO WORLD' from the custom String serializer. + // Actual: field error "Found duplicate type in schema at Query.greeting: String" + // and greeting => null, because the executor sees two different StringType instances. + self::assertSame([], $result->errors); + assert(is_array($result->data)); + self::assertSame('HELLO WORLD', $result->data['greeting']); + } } From 8306136e90fb683fff3c6ba65ac1df0e307f8140 Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Sun, 8 Mar 2026 21:45:24 +0100 Subject: [PATCH 5/8] Add working test case and example --- .../06-per-schema-scalar-override/example.php | 112 ++++++++++++++++++ tests/Type/BuiltInDefinitionsTest.php | 33 ++++++ 2 files changed, 145 insertions(+) create mode 100644 examples/06-per-schema-scalar-override/example.php diff --git a/examples/06-per-schema-scalar-override/example.php b/examples/06-per-schema-scalar-override/example.php new file mode 100644 index 000000000..e6b591f2c --- /dev/null +++ b/examples/06-per-schema-scalar-override/example.php @@ -0,0 +1,112 @@ + Type::STRING, + 'serialize' => static fn (mixed $value): string => trim((string) $value), + 'parseValue' => static fn (mixed $value): string => trim((string) $value), + 'parseLiteral' => static fn (mixed $ast): string => trim($ast->value ?? ''), +]); + +$builtInDefs = new BuiltInDefinitions([Type::STRING => $trimmedString]); + +/** + * Instance-based type registry that threads BuiltInDefinitions through all type + * definitions. + * Each type accessor that returns a string field must delegate to + * $this->builtInDefs->string() rather than Type::string(), so every field in + * the schema references the same scalar instance that was registered with the + * schema. + */ +final class TypeRegistry +{ + /** @var array */ + private array $cache = []; + + public function __construct(private readonly BuiltInDefinitions $builtInDefs) {} + + public function string(): ScalarType + { + return $this->builtInDefs->string(); + } + + public function user(): ObjectType + { + return $this->cache['User'] ??= new ObjectType([ // @phpstan-ignore missingType.checkedException (static configuration is known to be correct) + 'name' => 'User', + 'fields' => fn (): array => [ + 'name' => ['type' => $this->string()], + 'email' => ['type' => $this->string()], + ], + ]); + } +} + +$registry = new TypeRegistry($builtInDefs); + +$queryType = new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'user' => [ + 'type' => $registry->user(), + 'resolve' => static fn (): array => [ + 'name' => ' Alice ', + 'email' => ' alice@example.com ', + ], + ], + 'greeting' => [ + 'type' => $registry->string(), + 'resolve' => static fn (): string => ' hello world ', + ], + ], +]); + +$schema = new Schema( + (new SchemaConfig()) + ->setQuery($queryType) + ->setBuiltInDefinitions($builtInDefs) +); + +$schema->assertValid(); + +$result = GraphQL::executeQuery($schema, '{ greeting user { name email } }'); + +if ($result->errors !== []) { + foreach ($result->errors as $error) { + echo "Error: {$error->getMessage()}\n"; + } + exit(1); +} + +$data = $result->toArray()['data'] ?? []; +echo json_encode($data, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT) . "\n"; + +// Expected output (whitespace trimmed by the custom scalar): +// { +// "greeting": "hello world", +// "user": { +// "name": "Alice", +// "email": "alice@example.com" +// } +// } diff --git a/tests/Type/BuiltInDefinitionsTest.php b/tests/Type/BuiltInDefinitionsTest.php index 187bf4c5d..02dd13b4d 100644 --- a/tests/Type/BuiltInDefinitionsTest.php +++ b/tests/Type/BuiltInDefinitionsTest.php @@ -95,6 +95,39 @@ public function testTwoSchemasWithDifferentBuiltInDefinitions(): void ); } + public function testCorrectScalarOverrideWorksEndToEnd(): void + { + $upperString = new CustomScalarType([ + 'name' => Type::STRING, + 'serialize' => static fn ($value) => strtoupper((string) $value), + ]); + + $builtInDefs = new BuiltInDefinitions([Type::STRING => $upperString]); + + $queryType = new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'greeting' => [ + 'type' => $builtInDefs->string(), // use the instance, not the singleton + 'resolve' => static fn (): string => 'hello world', + ], + ], + ]); + + $schema = new Schema( + (new SchemaConfig()) + ->setQuery($queryType) + ->setBuiltInDefinitions($builtInDefs) + ); + + $schema->assertValid(); + + $result = GraphQL::executeQuery($schema, '{ greeting }'); + self::assertSame([], $result->errors); + assert(is_array($result->data)); + self::assertSame('HELLO WORLD', $result->data['greeting']); + } + /** * The type isolation case — no scalar overrides, just fresh BuiltInDefinitions instances * for each schema — already breaks at runtime when setAssumeValid(true) is used to skip From 327fbd3f26d9c19ae8f36233738a416ba99754f2 Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Sun, 8 Mar 2026 22:19:43 +0100 Subject: [PATCH 6/8] Resolve built-in scalar overrides by name, not identity When a schema has custom BuiltInDefinitions (e.g. a trimming String scalar), fields defined with Type::string() now transparently use the schema's override instead of the global singleton. Schema::getTypeMap() replaces standard singleton scalars with the schema's BuiltInDefinitions instances before extracting directive and introspection types, preventing "duplicate type" conflicts. ReferenceExecutor resolves built-in scalars from the schema by name at leaf completion, ensuring the override's serialize/parseValue runs. https://github.com/webonyx/graphql-php/issues/1424 --- .../06-per-schema-scalar-override/example.php | 52 ++++------------ src/Executor/ReferenceExecutor.php | 13 +++- src/Type/BuiltInDefinitions.php | 5 ++ src/Type/Schema.php | 14 +++++ tests/Type/BuiltInDefinitionsTest.php | 59 ++----------------- 5 files changed, 48 insertions(+), 95 deletions(-) diff --git a/examples/06-per-schema-scalar-override/example.php b/examples/06-per-schema-scalar-override/example.php index e6b591f2c..e8fdce5e5 100644 --- a/examples/06-per-schema-scalar-override/example.php +++ b/examples/06-per-schema-scalar-override/example.php @@ -5,9 +5,9 @@ * * Run: php examples/06-per-schema-scalar-override/example.php * - * The key pattern: pass a BuiltInDefinitions instance to SchemaConfig and use - * that same instance's scalar accessors in field definitions — never Type::string() - * or other static singleton accessors. + * Register a custom scalar with the same name as a built-in (e.g. "String") + * via BuiltInDefinitions, and the schema transparently uses it — even for + * fields that reference the global Type::string() singleton. */ require_once __DIR__ . '/../../vendor/autoload.php'; @@ -16,12 +16,10 @@ use GraphQL\Type\BuiltInDefinitions; use GraphQL\Type\Definition\CustomScalarType; use GraphQL\Type\Definition\ObjectType; -use GraphQL\Type\Definition\ScalarType; use GraphQL\Type\Definition\Type; use GraphQL\Type\Schema; use GraphQL\Type\SchemaConfig; -// A custom String scalar that trims leading/trailing whitespace on serialization. $trimmedString = new CustomScalarType([ 'name' => Type::STRING, 'serialize' => static fn (mixed $value): string => trim((string) $value), @@ -31,52 +29,26 @@ $builtInDefs = new BuiltInDefinitions([Type::STRING => $trimmedString]); -/** - * Instance-based type registry that threads BuiltInDefinitions through all type - * definitions. - * Each type accessor that returns a string field must delegate to - * $this->builtInDefs->string() rather than Type::string(), so every field in - * the schema references the same scalar instance that was registered with the - * schema. - */ -final class TypeRegistry -{ - /** @var array */ - private array $cache = []; - - public function __construct(private readonly BuiltInDefinitions $builtInDefs) {} - - public function string(): ScalarType - { - return $this->builtInDefs->string(); - } - - public function user(): ObjectType - { - return $this->cache['User'] ??= new ObjectType([ // @phpstan-ignore missingType.checkedException (static configuration is known to be correct) - 'name' => 'User', - 'fields' => fn (): array => [ - 'name' => ['type' => $this->string()], - 'email' => ['type' => $this->string()], - ], - ]); - } -} - -$registry = new TypeRegistry($builtInDefs); +$userType = new ObjectType([ // @phpstan-ignore missingType.checkedException (static configuration is known to be correct) + 'name' => 'User', + 'fields' => [ + 'name' => Type::string(), + 'email' => Type::string(), + ], +]); $queryType = new ObjectType([ 'name' => 'Query', 'fields' => [ 'user' => [ - 'type' => $registry->user(), + 'type' => $userType, 'resolve' => static fn (): array => [ 'name' => ' Alice ', 'email' => ' alice@example.com ', ], ], 'greeting' => [ - 'type' => $registry->string(), + 'type' => Type::string(), 'resolve' => static fn (): string => ' hello world ', ], ], diff --git a/src/Executor/ReferenceExecutor.php b/src/Executor/ReferenceExecutor.php index e50814d8d..e47f663c0 100644 --- a/src/Executor/ReferenceExecutor.php +++ b/src/Executor/ReferenceExecutor.php @@ -16,6 +16,7 @@ use GraphQL\Language\AST\OperationDefinitionNode; use GraphQL\Language\AST\SelectionNode; use GraphQL\Language\AST\SelectionSetNode; +use GraphQL\Type\BuiltInDefinitions; use GraphQL\Type\Definition\AbstractType; use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\FieldDefinition; @@ -918,11 +919,21 @@ protected function completeValue( // Account for invalid schema definition when typeLoader returns different // instance than `resolveType` or $field->getType() or $arg->getType() assert( - $returnType === $this->exeContext->schema->getType($returnType->name), + $returnType === $this->exeContext->schema->getType($returnType->name) + || BuiltInDefinitions::isBuiltInScalarName($returnType->name), SchemaValidationContext::duplicateType($this->exeContext->schema, "{$info->parentType}.{$info->fieldName}", $returnType->name) ); if ($returnType instanceof LeafType) { + // For built-in scalars, resolve from the schema so that per-schema overrides + // (via BuiltInDefinitions) take effect even when fields reference the global + // singleton (e.g. Type::string()). + if (BuiltInDefinitions::isBuiltInScalarName($returnType->name)) { + $schemaScalar = $this->exeContext->schema->getType($returnType->name); + assert($schemaScalar instanceof LeafType); + $returnType = $schemaScalar; + } + return $this->completeLeafValue($returnType, $result); } diff --git a/src/Type/BuiltInDefinitions.php b/src/Type/BuiltInDefinitions.php index 89904ec26..af1f016aa 100644 --- a/src/Type/BuiltInDefinitions.php +++ b/src/Type/BuiltInDefinitions.php @@ -198,6 +198,11 @@ public static function isBuiltInTypeName(string $name): bool return in_array($name, BuiltInDefinitions::BUILT_IN_TYPE_NAMES, true); } + public static function isBuiltInScalarName(string $name): bool + { + return in_array($name, self::SCALAR_TYPE_NAMES, true); + } + public static function isBuiltInType(NamedType $type): bool { return in_array($type->name, BuiltInDefinitions::BUILT_IN_TYPE_NAMES, true); diff --git a/src/Type/Schema.php b/src/Type/Schema.php index b966c71be..e0dae2f93 100644 --- a/src/Type/Schema.php +++ b/src/Type/Schema.php @@ -160,6 +160,20 @@ public function getTypeMap(): array } } + // Replace standard singleton scalars (from Type::string() etc.) with the + // schema's own instances from BuiltInDefinitions. This allows scalar overrides + // to take effect transparently — fields that reference the global singleton are + // resolved to the schema's version before directive/introspection types are + // extracted, preventing "duplicate type" conflicts. + $standardDefs = BuiltInDefinitions::standard(); + foreach ($this->builtInDefinitions->scalarTypes() as $scalarName => $scalar) { + if (isset($allReferencedTypes[$scalarName]) + && $allReferencedTypes[$scalarName] === $standardDefs->scalarTypes()[$scalarName] + && $scalar !== $allReferencedTypes[$scalarName]) { + $allReferencedTypes[$scalarName] = $scalar; + } + } + foreach ($this->getDirectives() as $directive) { // @phpstan-ignore-next-line generics are not strictly enforceable, error will be caught during schema validation if ($directive instanceof Directive) { diff --git a/tests/Type/BuiltInDefinitionsTest.php b/tests/Type/BuiltInDefinitionsTest.php index 02dd13b4d..91e00dd0d 100644 --- a/tests/Type/BuiltInDefinitionsTest.php +++ b/tests/Type/BuiltInDefinitionsTest.php @@ -128,18 +128,7 @@ public function testCorrectScalarOverrideWorksEndToEnd(): void self::assertSame('HELLO WORLD', $result->data['greeting']); } - /** - * The type isolation case — no scalar overrides, just fresh BuiltInDefinitions instances - * for each schema — already breaks at runtime when setAssumeValid(true) is used to skip - * validation. A field typed via Type::string() holds the global singleton StringType, - * but the schema's BuiltInDefinitions owns a different StringType instance. The executor - * detects the mismatch and rejects the field with a "Found duplicate type" error. - * - * This is the scenario described in https://github.com/webonyx/graphql-php/issues/1424: - * schemas that should be independent still collide through built-in type instances. - * - * This test documents that failure. It should pass once the issue is resolved. - */ + /** @see https://github.com/webonyx/graphql-php/issues/1424 */ public function testIsolatedSchemaFailsAtRuntime(): void { $queryType = new ObjectType([ @@ -161,9 +150,6 @@ public function testIsolatedSchemaFailsAtRuntime(): void $result = GraphQL::executeQuery($schema, '{ hello }'); - // Expects no errors and hello => 'world'. - // Actual: field error "Found duplicate type in schema at Query.hello: String" - // because Type::string() (singleton) != new BuiltInDefinitions()->string() (fresh instance). self::assertSame([], $result->errors); assert(is_array($result->data)); self::assertSame('world', $result->data['hello']); @@ -194,16 +180,7 @@ public function testDirectivesReturnsAllStandard(): void self::assertCount(4, $directives); } - /** - * In production PHP (zend.assertions = -1), the executor's runtime type-identity - * assert() at ReferenceExecutor.php is compiled out. setAssumeValid(true) already - * skips schema validation. With both disabled, execution completes without error — - * but silently uses the singleton StringType's serialize(), not the custom one. - * The result is wrong and there is no indication that the override was ignored. - * - * This test simulates that scenario by disabling assert.active at runtime. - * It documents that failure. It should pass once the issue is resolved. - */ + /** Scalar override must take effect even with assertions disabled and validation skipped. */ public function testNaiveScalarOverrideIsIgnoredSilentlyInProduction(): void { $upperString = new CustomScalarType([ @@ -240,24 +217,12 @@ public function testNaiveScalarOverrideIsIgnoredSilentlyInProduction(): void @ini_set('assert.active', $prevAssertActive); } - // No errors — setAssumeValid skips schema validation and the disabled assert - // suppresses the executor's runtime identity check. Execution "succeeds". self::assertSame([], $result->errors); assert(is_array($result->data)); - // Expects 'HELLO WORLD' from the custom String serializer. - // Actual: 'hello world' — the field holds a reference to the singleton StringType, - // which is what the executor calls serialize() on. The override is silently lost. self::assertSame('HELLO WORLD', $result->data['greeting']); } - /** - * A user naturally reaches for Type::string() when defining fields — the same way every - * example and doc in this repository does. When they pair that with a BuiltInDefinitions - * override they expect the custom scalar to take effect. Instead, schema validation blows - * up because two different StringType instances with the same name end up in the type map. - * - * This test documents that failure. It should pass once the issue is resolved. - */ + /** Scalar override via BuiltInDefinitions must not break validation when fields use Type::string(). */ public function testNaiveScalarOverridePassesValidation(): void { $upperString = new CustomScalarType([ @@ -278,22 +243,11 @@ public function testNaiveScalarOverridePassesValidation(): void ->setBuiltInDefinitions($builtInDefs) ); - // Fails: "Schema must contain unique named types but contains multiple types named "String"" $schema->assertValid(); + self::assertSame($upperString, $schema->getType(Type::STRING)); } - /** - * Even when validation is skipped, the custom String scalar is never invoked at runtime. - * Fields defined via Type::string() hold a direct reference to the singleton StringType. - * The executor detects that the field's type instance differs from the schema's String - * (the two instances have the same name but are not identical), and rejects the field - * with: "Found duplicate type in schema at Query.greeting: String.". - * - * The field resolves to null with a GraphQL error — not even falling back to the - * singleton's standard serialization, let alone invoking the custom one. - * - * This test documents that failure. It should pass once the issue is resolved. - */ + /** Scalar override must work at runtime even when fields use Type::string(). */ public function testNaiveScalarOverrideIsUsedAtRuntime(): void { $upperString = new CustomScalarType([ @@ -322,9 +276,6 @@ public function testNaiveScalarOverrideIsUsedAtRuntime(): void $result = GraphQL::executeQuery($schema, '{ greeting }'); - // Expects no errors and 'HELLO WORLD' from the custom String serializer. - // Actual: field error "Found duplicate type in schema at Query.greeting: String" - // and greeting => null, because the executor sees two different StringType instances. self::assertSame([], $result->errors); assert(is_array($result->data)); self::assertSame('HELLO WORLD', $result->data['greeting']); From 990c9af003e2cbc4713acaa77df3b094c33030af Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Sun, 8 Mar 2026 22:29:06 +0100 Subject: [PATCH 7/8] Fix static analysis errors in example and test - Remove `mixed` type hints from closures in example.php (PHP 7.4 incompatible) - Remove stale @phpstan-ignore on ObjectType construction (no longer needed) - Guard ini_set restore with is_string() to fix string|false type mismatch --- examples/06-per-schema-scalar-override/example.php | 8 ++++---- tests/Type/BuiltInDefinitionsTest.php | 4 +++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/examples/06-per-schema-scalar-override/example.php b/examples/06-per-schema-scalar-override/example.php index e8fdce5e5..09ea91258 100644 --- a/examples/06-per-schema-scalar-override/example.php +++ b/examples/06-per-schema-scalar-override/example.php @@ -22,14 +22,14 @@ $trimmedString = new CustomScalarType([ 'name' => Type::STRING, - 'serialize' => static fn (mixed $value): string => trim((string) $value), - 'parseValue' => static fn (mixed $value): string => trim((string) $value), - 'parseLiteral' => static fn (mixed $ast): string => trim($ast->value ?? ''), + 'serialize' => static fn ($value): string => trim((string) $value), + 'parseValue' => static fn ($value): string => trim((string) $value), + 'parseLiteral' => static fn ($ast): string => trim($ast->value ?? ''), ]); $builtInDefs = new BuiltInDefinitions([Type::STRING => $trimmedString]); -$userType = new ObjectType([ // @phpstan-ignore missingType.checkedException (static configuration is known to be correct) +$userType = new ObjectType([ 'name' => 'User', 'fields' => [ 'name' => Type::string(), diff --git a/tests/Type/BuiltInDefinitionsTest.php b/tests/Type/BuiltInDefinitionsTest.php index 91e00dd0d..bdc3fc747 100644 --- a/tests/Type/BuiltInDefinitionsTest.php +++ b/tests/Type/BuiltInDefinitionsTest.php @@ -214,7 +214,9 @@ public function testNaiveScalarOverrideIsIgnoredSilentlyInProduction(): void try { $result = GraphQL::executeQuery($schema, '{ greeting }'); } finally { - @ini_set('assert.active', $prevAssertActive); + if (is_string($prevAssertActive)) { + @ini_set('assert.active', $prevAssertActive); + } } self::assertSame([], $result->errors); From b3498ebf6feb950803c36b173fedff53325b844f Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Mon, 9 Mar 2026 18:06:31 +0100 Subject: [PATCH 8/8] Keep deprecated shims for removed static methods, mark BuiltInDefinitions @api Downstream consumers like Lighthouse, Magento, rebing/graphql-laravel, api-platform, and others rely on static methods that were removed in the BuiltInDefinitions refactor. Add them back as thin deprecated delegates to BuiltInDefinitions::standard(), providing a migration path without breaking existing code. Shims added: - Type: getStandardTypes(), builtInTypes(), overrideStandardTypes(), STANDARD_TYPE_NAMES, BUILT_IN_TYPE_NAMES - Directive: getInternalDirectives(), includeDirective(), skipDirective(), deprecatedDirective(), oneOfDirective(), isSpecifiedDirective() - Introspection: getTypes(), isIntrospectionType(), schemaMetaFieldDef(), typeMetaFieldDef(), typeNameMetaFieldDef(), TYPE_NAMES Mark all public BuiltInDefinitions methods @api with docblocks and include the class in generated class-reference docs. Co-Authored-By: Claude Opus 4.6 --- docs/class-reference.md | 269 ++++++++++++++++++++++++++++++ generate-class-reference.php | 1 + src/Type/BuiltInDefinitions.php | 139 ++++++++++++++- src/Type/Definition/Directive.php | 47 ++++++ src/Type/Definition/Type.php | 60 +++++++ src/Type/Introspection.php | 59 +++++++ 6 files changed, 569 insertions(+), 6 deletions(-) diff --git a/docs/class-reference.md b/docs/class-reference.md index 7445c0bb8..521c22b65 100644 --- a/docs/class-reference.md +++ b/docs/class-reference.md @@ -178,6 +178,275 @@ static function setDefaultFieldResolver(callable $fn): void static function setDefaultArgsMapper(callable $fn): void ``` +## GraphQL\Type\BuiltInDefinitions + +Per-instance container for all built-in GraphQL definitions: scalars, introspection types, meta-fields, and directives. + +Each Schema can own its own instance to avoid global state conflicts in multi-schema environments. +Use {@see BuiltInDefinitions::standard()} for the shared default singleton. + +### GraphQL\Type\BuiltInDefinitions Constants + +```php +const SCALAR_TYPE_NAMES = [ + 'Int', + 'Float', + 'String', + 'Boolean', + 'ID', +]; +const INTROSPECTION_TYPE_NAMES = [ + '__Schema', + '__Type', + '__Directive', + '__Field', + '__InputValue', + '__EnumValue', + '__TypeKind', + '__DirectiveLocation', +]; +const BUILT_IN_TYPE_NAMES = [ + 'Int', + 'Float', + 'String', + 'Boolean', + 'ID', + '__Schema', + '__Type', + '__Directive', + '__Field', + '__InputValue', + '__EnumValue', + '__TypeKind', + '__DirectiveLocation', +]; +const BUILT_IN_DIRECTIVE_NAMES = [ + 'include', + 'skip', + 'deprecated', + 'oneOf', +]; +``` + +### GraphQL\Type\BuiltInDefinitions Methods + +```php +/** + * Returns the shared default singleton instance. + * + * @api + */ +static function standard(): self +``` + +```php +/** + * Replaces the standard singleton with one that uses the given scalar overrides. + * + * @param array $types + * + * @api + */ +static function overrideScalarTypes(array $types): void +``` + +```php +/** + * Checks if the given type is one of the introspection types. + * + * @api + */ +static function isIntrospectionType(GraphQL\Type\Definition\NamedType $type): bool +``` + +```php +/** + * Checks if the given directive is one of the built-in directives. + * + * @api + */ +static function isBuiltInDirective(GraphQL\Type\Definition\Directive $directive): bool +``` + +```php +/** + * Returns the built-in Int scalar type. + * + * @api + */ +function int(): GraphQL\Type\Definition\ScalarType +``` + +```php +/** + * Returns the built-in Float scalar type. + * + * @api + */ +function float(): GraphQL\Type\Definition\ScalarType +``` + +```php +/** + * Returns the built-in String scalar type. + * + * @api + */ +function string(): GraphQL\Type\Definition\ScalarType +``` + +```php +/** + * Returns the built-in Boolean scalar type. + * + * @api + */ +function boolean(): GraphQL\Type\Definition\ScalarType +``` + +```php +/** + * Returns the built-in ID scalar type. + * + * @api + */ +function id(): GraphQL\Type\Definition\ScalarType +``` + +```php +/** + * Returns all five standard scalar types keyed by name. + * + * @return array + * + * @api + */ +function scalarTypes(): array +``` + +```php +/** + * Checks if the given name is a built-in type (scalar or introspection). + * + * @api + */ +static function isBuiltInTypeName(string $name): bool +``` + +```php +/** + * Checks if the given name is one of the five standard scalar types. + * + * @api + */ +static function isBuiltInScalarName(string $name): bool +``` + +```php +/** + * Checks if the given type instance is a built-in type. + * + * @api + */ +static function isBuiltInType(GraphQL\Type\Definition\NamedType $type): bool +``` + +```php +/** + * Returns all eight introspection types keyed by name. + * + * @return array + * + * @api + */ +function introspectionTypes(): array +``` + +```php +/** + * Returns the __schema meta-field definition. + * + * @api + */ +function schemaMetaFieldDef(): GraphQL\Type\Definition\FieldDefinition +``` + +```php +/** + * Returns the __type meta-field definition. + * + * @api + */ +function typeMetaFieldDef(): GraphQL\Type\Definition\FieldDefinition +``` + +```php +/** + * Returns the __typename meta-field definition. + * + * @api + */ +function typeNameMetaFieldDef(): GraphQL\Type\Definition\FieldDefinition +``` + +```php +/** + * Returns the built-in @include directive. + * + * @api + */ +function includeDirective(): GraphQL\Type\Definition\Directive +``` + +```php +/** + * Returns the built-in @skip directive. + * + * @api + */ +function skipDirective(): GraphQL\Type\Definition\Directive +``` + +```php +/** + * Returns the built-in @deprecated directive. + * + * @api + */ +function deprecatedDirective(): GraphQL\Type\Definition\Directive +``` + +```php +/** + * Returns the built-in @oneOf directive. + * + * @api + */ +function oneOfDirective(): GraphQL\Type\Definition\Directive +``` + +```php +/** + * Returns all four built-in directives keyed by name. + * + * @return array + * + * @api + */ +function directives(): array +``` + +```php +/** + * Returns all built-in types (scalars + introspection types) keyed by name. + * + * @return array + * + * @api + */ +function types(): array +``` + ## GraphQL\Type\Definition\Type Registry of standard GraphQL types and base class for all other types. diff --git a/generate-class-reference.php b/generate-class-reference.php index 70f25e11e..28c57a042 100644 --- a/generate-class-reference.php +++ b/generate-class-reference.php @@ -10,6 +10,7 @@ const ENTRIES = [ GraphQL\GraphQL::class => [], + GraphQL\Type\BuiltInDefinitions::class => ['constants' => true], GraphQL\Type\Definition\Type::class => [], GraphQL\Type\Definition\ResolveInfo::class => [], GraphQL\Language\DirectiveLocation::class => ['constants' => true], diff --git a/src/Type/BuiltInDefinitions.php b/src/Type/BuiltInDefinitions.php index af1f016aa..33c7c994c 100644 --- a/src/Type/BuiltInDefinitions.php +++ b/src/Type/BuiltInDefinitions.php @@ -30,9 +30,16 @@ use GraphQL\Utils\AST; use GraphQL\Utils\Utils; +/** + * Per-instance container for all built-in GraphQL definitions: scalars, introspection types, meta-fields, and directives. + * + * Each Schema can own its own instance to avoid global state conflicts in multi-schema environments. + * Use {@see BuiltInDefinitions::standard()} for the shared default singleton. + */ class BuiltInDefinitions { - protected const SCALAR_TYPE_NAMES = [ + /** @var array */ + public const SCALAR_TYPE_NAMES = [ Type::INT, Type::FLOAT, Type::STRING, @@ -103,12 +110,23 @@ public function __construct(array $scalarTypeOverrides = []) $this->scalarTypeOverrides = $scalarTypeOverrides; } + /** + * Returns the shared default singleton instance. + * + * @api + */ public static function standard(): self { return self::$standard ??= new self(); } - /** @param array $types */ + /** + * Replaces the standard singleton with one that uses the given scalar overrides. + * + * @param array $types + * + * @api + */ public static function overrideScalarTypes(array $types): void { // Preserve non-overridden scalar instances from the current standard. @@ -136,16 +154,31 @@ public static function overrideScalarTypes(array $types): void self::$standard = new self($scalarOverrides); } + /** + * Checks if the given type is one of the introspection types. + * + * @api + */ public static function isIntrospectionType(NamedType $type): bool { return in_array($type->name, self::INTROSPECTION_TYPE_NAMES, true); } + /** + * Checks if the given directive is one of the built-in directives. + * + * @api + */ public static function isBuiltInDirective(Directive $directive): bool { return in_array($directive->name, self::BUILT_IN_DIRECTIVE_NAMES, true); } + /** + * Returns the built-in Int scalar type. + * + * @api + */ public function int(): ScalarType { return $this->scalarTypes[Type::INT] @@ -153,6 +186,11 @@ public function int(): ScalarType ?? new IntType(); // @phpstan-ignore missingType.checkedException (static configuration is known to be correct) } + /** + * Returns the built-in Float scalar type. + * + * @api + */ public function float(): ScalarType { return $this->scalarTypes[Type::FLOAT] @@ -160,6 +198,11 @@ public function float(): ScalarType ?? new FloatType(); // @phpstan-ignore missingType.checkedException (static configuration is known to be correct) } + /** + * Returns the built-in String scalar type. + * + * @api + */ public function string(): ScalarType { return $this->scalarTypes[Type::STRING] @@ -167,6 +210,11 @@ public function string(): ScalarType ?? new StringType(); // @phpstan-ignore missingType.checkedException (static configuration is known to be correct) } + /** + * Returns the built-in Boolean scalar type. + * + * @api + */ public function boolean(): ScalarType { return $this->scalarTypes[Type::BOOLEAN] @@ -174,6 +222,11 @@ public function boolean(): ScalarType ?? new BooleanType(); // @phpstan-ignore missingType.checkedException (static configuration is known to be correct) } + /** + * Returns the built-in ID scalar type. + * + * @api + */ public function id(): ScalarType { return $this->scalarTypes[Type::ID] @@ -181,7 +234,13 @@ public function id(): ScalarType ?? new IDType(); // @phpstan-ignore missingType.checkedException (static configuration is known to be correct) } - /** @return array */ + /** + * Returns all five standard scalar types keyed by name. + * + * @return array + * + * @api + */ public function scalarTypes(): array { return [ @@ -193,16 +252,31 @@ public function scalarTypes(): array ]; } + /** + * Checks if the given name is a built-in type (scalar or introspection). + * + * @api + */ public static function isBuiltInTypeName(string $name): bool { return in_array($name, BuiltInDefinitions::BUILT_IN_TYPE_NAMES, true); } + /** + * Checks if the given name is one of the five standard scalar types. + * + * @api + */ public static function isBuiltInScalarName(string $name): bool { return in_array($name, self::SCALAR_TYPE_NAMES, true); } + /** + * Checks if the given type instance is a built-in type. + * + * @api + */ public static function isBuiltInType(NamedType $type): bool { return in_array($type->name, BuiltInDefinitions::BUILT_IN_TYPE_NAMES, true); @@ -732,7 +806,13 @@ public function directiveLocationType(): EnumType ]); } - /** @return array */ + /** + * Returns all eight introspection types keyed by name. + * + * @return array + * + * @api + */ public function introspectionTypes(): array { return [ @@ -747,6 +827,11 @@ public function introspectionTypes(): array ]; } + /** + * Returns the __schema meta-field definition. + * + * @api + */ public function schemaMetaFieldDef(): FieldDefinition { return $this->metaFieldDefs[Introspection::SCHEMA_FIELD_NAME] ??= new FieldDefinition([ @@ -758,6 +843,11 @@ public function schemaMetaFieldDef(): FieldDefinition ]); } + /** + * Returns the __type meta-field definition. + * + * @api + */ public function typeMetaFieldDef(): FieldDefinition { return $this->metaFieldDefs[Introspection::TYPE_FIELD_NAME] ??= new FieldDefinition([ @@ -774,6 +864,11 @@ public function typeMetaFieldDef(): FieldDefinition ]); } + /** + * Returns the __typename meta-field definition. + * + * @api + */ public function typeNameMetaFieldDef(): FieldDefinition { return $this->metaFieldDefs[Introspection::TYPE_NAME_FIELD_NAME] ??= new FieldDefinition([ @@ -785,6 +880,11 @@ public function typeNameMetaFieldDef(): FieldDefinition ]); } + /** + * Returns the built-in @include directive. + * + * @api + */ public function includeDirective(): Directive { return $this->directives[Directive::INCLUDE_NAME] ??= new Directive([ @@ -804,6 +904,11 @@ public function includeDirective(): Directive ]); } + /** + * Returns the built-in @skip directive. + * + * @api + */ public function skipDirective(): Directive { return $this->directives[Directive::SKIP_NAME] ??= new Directive([ @@ -823,6 +928,11 @@ public function skipDirective(): Directive ]); } + /** + * Returns the built-in @deprecated directive. + * + * @api + */ public function deprecatedDirective(): Directive { return $this->directives[Directive::DEPRECATED_NAME] ??= new Directive([ @@ -844,6 +954,11 @@ public function deprecatedDirective(): Directive ]); } + /** + * Returns the built-in @oneOf directive. + * + * @api + */ public function oneOfDirective(): Directive { return $this->directives[Directive::ONE_OF_NAME] ??= new Directive([ @@ -856,7 +971,13 @@ public function oneOfDirective(): Directive ]); } - /** @return array */ + /** + * Returns all four built-in directives keyed by name. + * + * @return array + * + * @api + */ public function directives(): array { return [ @@ -867,7 +988,13 @@ public function directives(): array ]; } - /** @return array */ + /** + * Returns all built-in types (scalars + introspection types) keyed by name. + * + * @return array + * + * @api + */ public function types(): array { return $this->typesCache ??= array_merge( diff --git a/src/Type/Definition/Directive.php b/src/Type/Definition/Directive.php index e8a45ebb7..430c7e36d 100644 --- a/src/Type/Definition/Directive.php +++ b/src/Type/Definition/Directive.php @@ -3,6 +3,7 @@ namespace GraphQL\Type\Definition; use GraphQL\Language\AST\DirectiveDefinitionNode; +use GraphQL\Type\BuiltInDefinitions; /** * @phpstan-import-type ArgumentListConfig from Argument @@ -66,4 +67,50 @@ public function __construct(array $config) $this->config = $config; } + + /** + * Returns all built-in directives. + * + * @deprecated use {@see BuiltInDefinitions::standard()}->directives() + * + * @return array + */ + public static function getInternalDirectives(): array + { + return BuiltInDefinitions::standard()->directives(); + } + + /** @deprecated use {@see BuiltInDefinitions::standard()}->includeDirective() */ + public static function includeDirective(): self + { + return BuiltInDefinitions::standard()->includeDirective(); + } + + /** @deprecated use {@see BuiltInDefinitions::standard()}->skipDirective() */ + public static function skipDirective(): self + { + return BuiltInDefinitions::standard()->skipDirective(); + } + + /** @deprecated use {@see BuiltInDefinitions::standard()}->deprecatedDirective() */ + public static function deprecatedDirective(): self + { + return BuiltInDefinitions::standard()->deprecatedDirective(); + } + + /** @deprecated use {@see BuiltInDefinitions::standard()}->oneOfDirective() */ + public static function oneOfDirective(): self + { + return BuiltInDefinitions::standard()->oneOfDirective(); + } + + /** + * Checks if the given directive is one of the built-in directives. + * + * @deprecated use {@see BuiltInDefinitions::isBuiltInDirective()} + */ + public static function isSpecifiedDirective(self $directive): bool + { + return BuiltInDefinitions::isBuiltInDirective($directive); + } } diff --git a/src/Type/Definition/Type.php b/src/Type/Definition/Type.php index e76b5c489..f5b7b5ad2 100644 --- a/src/Type/Definition/Type.php +++ b/src/Type/Definition/Type.php @@ -3,6 +3,7 @@ namespace GraphQL\Type\Definition; use GraphQL\Type\BuiltInDefinitions; +use GraphQL\Type\Introspection; /** * Registry of standard GraphQL types and base class for all other types. @@ -15,6 +16,29 @@ abstract class Type implements \JsonSerializable public const BOOLEAN = 'Boolean'; public const ID = 'ID'; + /** + * @deprecated use {@see BuiltInDefinitions::SCALAR_TYPE_NAMES} or {@see BuiltInDefinitions::isBuiltInScalarName()} + * + * @var array + */ + public const STANDARD_TYPE_NAMES = [ + self::INT, + self::FLOAT, + self::STRING, + self::BOOLEAN, + self::ID, + ]; + + /** + * @deprecated use {@see BuiltInDefinitions::BUILT_IN_TYPE_NAMES} or {@see BuiltInDefinitions::isBuiltInTypeName()} + * + * @var array + */ + public const BUILT_IN_TYPE_NAMES = [ + ...self::STANDARD_TYPE_NAMES, + ...Introspection::TYPE_NAMES, + ]; + /** * Returns the registered or default standard Int type. * @@ -195,6 +219,42 @@ public static function getNullableType(Type $type): Type return $type; } + /** + * Returns all built-in types (scalars + introspection types). + * + * @deprecated use {@see BuiltInDefinitions::standard()}->types() + * + * @return array + */ + public static function builtInTypes(): array + { + return BuiltInDefinitions::standard()->types(); + } + + /** + * Returns the standard scalar types (Int, Float, String, Boolean, ID). + * + * @deprecated use {@see BuiltInDefinitions::standard()}->scalarTypes() + * + * @return array + */ + public static function getStandardTypes(): array + { + return BuiltInDefinitions::standard()->scalarTypes(); + } + + /** + * Replaces standard scalar types with the given types. + * + * @deprecated use {@see BuiltInDefinitions::overrideScalarTypes()} + * + * @param array $types + */ + public static function overrideStandardTypes(array $types): void + { + BuiltInDefinitions::overrideScalarTypes($types); + } + abstract public function toString(): string; public function __toString(): string diff --git a/src/Type/Introspection.php b/src/Type/Introspection.php index 8e845055c..4b8ccb719 100644 --- a/src/Type/Introspection.php +++ b/src/Type/Introspection.php @@ -4,6 +4,9 @@ use GraphQL\Error\InvariantViolation; use GraphQL\GraphQL; +use GraphQL\Type\Definition\FieldDefinition; +use GraphQL\Type\Definition\NamedType; +use GraphQL\Type\Definition\Type; use GraphQL\Utils\Utils; /** @@ -42,6 +45,62 @@ class Introspection public const TYPE_KIND_ENUM_NAME = '__TypeKind'; public const DIRECTIVE_LOCATION_ENUM_NAME = '__DirectiveLocation'; + /** + * @deprecated use {@see BuiltInDefinitions::INTROSPECTION_TYPE_NAMES} + * + * @var array + */ + public const TYPE_NAMES = [ + self::SCHEMA_OBJECT_NAME, + self::TYPE_OBJECT_NAME, + self::DIRECTIVE_OBJECT_NAME, + self::FIELD_OBJECT_NAME, + self::INPUT_VALUE_OBJECT_NAME, + self::ENUM_VALUE_OBJECT_NAME, + self::TYPE_KIND_ENUM_NAME, + self::DIRECTIVE_LOCATION_ENUM_NAME, + ]; + + /** + * Checks if the given type is an introspection type. + * + * @deprecated use {@see BuiltInDefinitions::isIntrospectionType()} + */ + public static function isIntrospectionType(NamedType $type): bool + { + return BuiltInDefinitions::isIntrospectionType($type); + } + + /** + * Returns all introspection types. + * + * @deprecated use {@see BuiltInDefinitions::standard()}->introspectionTypes() + * + * @return array + */ + public static function getTypes(): array + { + return BuiltInDefinitions::standard()->introspectionTypes(); + } + + /** @deprecated use {@see BuiltInDefinitions::standard()}->schemaMetaFieldDef() */ + public static function schemaMetaFieldDef(): FieldDefinition + { + return BuiltInDefinitions::standard()->schemaMetaFieldDef(); + } + + /** @deprecated use {@see BuiltInDefinitions::standard()}->typeMetaFieldDef() */ + public static function typeMetaFieldDef(): FieldDefinition + { + return BuiltInDefinitions::standard()->typeMetaFieldDef(); + } + + /** @deprecated use {@see BuiltInDefinitions::standard()}->typeNameMetaFieldDef() */ + public static function typeNameMetaFieldDef(): FieldDefinition + { + return BuiltInDefinitions::standard()->typeNameMetaFieldDef(); + } + /** * @param IntrospectionOptions $options *