diff --git a/docs/class-reference.md b/docs/class-reference.md index 0102d10df..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. @@ -724,6 +993,7 @@ Usage example: types?: Types|null, directives?: array|null, typeLoader?: TypeLoader|null, + builtInDefinitions?: BuiltInDefinitions|null, assumeValid?: bool|null, astNode?: SchemaDefinitionNode|null, extensionASTNodes?: array|null, 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..09ea91258 --- /dev/null +++ b/examples/06-per-schema-scalar-override/example.php @@ -0,0 +1,84 @@ + Type::STRING, + '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([ + 'name' => 'User', + 'fields' => [ + 'name' => Type::string(), + 'email' => Type::string(), + ], +]); + +$queryType = new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'user' => [ + 'type' => $userType, + 'resolve' => static fn (): array => [ + 'name' => ' Alice ', + 'email' => ' alice@example.com ', + ], + ], + 'greeting' => [ + 'type' => Type::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/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/Executor/ReferenceExecutor.php b/src/Executor/ReferenceExecutor.php index 53c52dd8a..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; @@ -28,7 +29,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; @@ -75,6 +75,10 @@ class ReferenceExecutor implements ExecutorImplementation protected FieldDefinition $typeNameMetaFieldDef; + protected Directive $skipDirective; + + protected Directive $includeDirective; + protected function __construct(ExecutionContext $context) { if (! isset(static::$UNDEFINED)) { @@ -84,6 +88,10 @@ protected function __construct(ExecutionContext $context) $this->exeContext = $context; $this->subFieldCache = new \SplObjectStorage(); $this->fieldArgsCache = new \SplObjectStorage(); + + $builtInDefinitions = $context->schema->getBuiltInDefinitions(); + $this->skipDirective = $builtInDefinitions->skipDirective(); + $this->includeDirective = $builtInDefinitions->includeDirective(); } /** @@ -466,20 +474,12 @@ protected function shouldIncludeNode(SelectionNode $node): bool { $variableValues = $this->exeContext->variableValues; - $skip = Values::getDirectiveValues( - Directive::skipDirective(), - $node, - $variableValues - ); + $skip = Values::getDirectiveValues($this->skipDirective, $node, $variableValues); if (isset($skip['if']) && $skip['if'] === true) { return false; } - $include = Values::getDirectiveValues( - Directive::includeDirective(), - $node, - $variableValues - ); + $include = Values::getDirectiveValues($this->includeDirective, $node, $variableValues); return ! isset($include['if']) || $include['if'] !== false; } @@ -684,9 +684,17 @@ 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(); + // 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); + } + + $builtInDefinitions = $schema->getBuiltInDefinitions(); + $this->schemaMetaFieldDef ??= $builtInDefinitions->schemaMetaFieldDef(); + $this->typeMetaFieldDef ??= $builtInDefinitions->typeMetaFieldDef(); + $this->typeNameMetaFieldDef ??= $builtInDefinitions->typeNameMetaFieldDef(); $queryType = $schema->getQueryType(); @@ -911,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/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/BuiltInDefinitions.php b/src/Type/BuiltInDefinitions.php new file mode 100644 index 000000000..33c7c994c --- /dev/null +++ b/src/Type/BuiltInDefinitions.php @@ -0,0 +1,1005 @@ + */ + public 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 $scalarTypes = []; + + 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|null */ + private ?array $typesCache = null; + + /** @var array */ + private array $scalarTypeOverrides; + + /** @param array $scalarTypeOverrides */ + 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(); + } + + /** + * 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. + $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); + } + + /** + * 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] + ??= $this->scalarTypeOverrides[Type::INT] + ?? 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] + ??= $this->scalarTypeOverrides[Type::FLOAT] + ?? 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] + ??= $this->scalarTypeOverrides[Type::STRING] + ?? 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] + ??= $this->scalarTypeOverrides[Type::BOOLEAN] + ?? 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] + ??= $this->scalarTypeOverrides[Type::ID] + ?? new IDType(); // @phpstan-ignore missingType.checkedException (static configuration is known to be correct) + } + + /** + * Returns all five standard scalar types keyed by name. + * + * @return array + * + * @api + */ + public function scalarTypes(): array + { + return [ + Type::INT => $this->int(), + Type::FLOAT => $this->float(), + Type::STRING => $this->string(), + Type::BOOLEAN => $this->boolean(), + Type::ID => $this->id(), + ]; + } + + /** + * 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); + } + + 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.', + ], + ], + ]); + } + + /** + * Returns all eight introspection types keyed by name. + * + * @return array + * + * @api + */ + 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(), + ]; + } + + /** + * Returns the __schema meta-field definition. + * + * @api + */ + 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, + ]); + } + + /** + * Returns the __type meta-field definition. + * + * @api + */ + 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']), + ]); + } + + /** + * Returns the __typename meta-field definition. + * + * @api + */ + 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, + ]); + } + + /** + * Returns the built-in @include directive. + * + * @api + */ + 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.', + ], + ], + ]); + } + + /** + * Returns the built-in @skip directive. + * + * @api + */ + 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.', + ], + ], + ]); + } + + /** + * Returns the built-in @deprecated directive. + * + * @api + */ + 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, + ], + ], + ]); + } + + /** + * Returns the built-in @oneOf directive. + * + * @api + */ + 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' => [], + ]); + } + + /** + * Returns all four built-in directives keyed by name. + * + * @return array + * + * @api + */ + 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(), + ]; + } + + /** + * Returns all built-in types (scalars + introspection types) keyed by name. + * + * @return array + * + * @api + */ + public function types(): array + { + return $this->typesCache ??= array_merge( + $this->introspectionTypes(), + $this->scalarTypes() + ); + } +} diff --git a/src/Type/Definition/Directive.php b/src/Type/Definition/Directive.php index e613b85d3..430c7e36d 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\BuiltInDefinitions; /** * @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; @@ -75,95 +68,49 @@ public function __construct(array $config) $this->config = $config; } - /** @return array */ + /** + * Returns all built-in directives. + * + * @deprecated use {@see BuiltInDefinitions::standard()}->directives() + * + * @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(), - ]; - } - - 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 BuiltInDefinitions::standard()->directives(); } - public static function skipDirective(): Directive + /** @deprecated use {@see BuiltInDefinitions::standard()}->includeDirective() */ + public static function includeDirective(): self { - 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 BuiltInDefinitions::standard()->includeDirective(); } - public static function deprecatedDirective(): Directive + /** @deprecated use {@see BuiltInDefinitions::standard()}->skipDirective() */ + public static function skipDirective(): self { - 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 BuiltInDefinitions::standard()->skipDirective(); } - public static function oneOfDirective(): Directive + /** @deprecated use {@see BuiltInDefinitions::standard()}->deprecatedDirective() */ + public static function deprecatedDirective(): self { - 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 BuiltInDefinitions::standard()->deprecatedDirective(); } - public static function isSpecifiedDirective(Directive $directive): bool + /** @deprecated use {@see BuiltInDefinitions::standard()}->oneOfDirective() */ + public static function oneOfDirective(): self { - return array_key_exists($directive->name, self::getInternalDirectives()); + return BuiltInDefinitions::standard()->oneOfDirective(); } - public static function resetCachedInstances(): void + /** + * Checks if the given directive is one of the built-in directives. + * + * @deprecated use {@see BuiltInDefinitions::isBuiltInDirective()} + */ + public static function isSpecifiedDirective(self $directive): bool { - self::$internalDirectives = null; + return BuiltInDefinitions::isBuiltInDirective($directive); } } 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 8dee72b8d..f5b7b5ad2 100644 --- a/src/Type/Definition/Type.php +++ b/src/Type/Definition/Type.php @@ -2,9 +2,8 @@ namespace GraphQL\Type\Definition; -use GraphQL\Error\InvariantViolation; +use GraphQL\Type\BuiltInDefinitions; use GraphQL\Type\Introspection; -use GraphQL\Utils\Utils; /** * Registry of standard GraphQL types and base class for all other types. @@ -17,6 +16,11 @@ 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, @@ -25,17 +29,16 @@ abstract class Type implements \JsonSerializable 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, ]; - /** @var array|null */ - protected static ?array $standardTypes; - - /** @var array|null */ - protected static ?array $builtInTypes; - /** * Returns the registered or default standard Int type. * @@ -43,7 +46,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(); } /** @@ -53,7 +56,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(); } /** @@ -63,7 +66,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(); } /** @@ -73,7 +76,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(); } /** @@ -83,7 +86,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(); } /** @@ -118,67 +121,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 self::$builtInTypes ??= array_merge( - Introspection::getTypes(), - self::getStandardTypes() - ); - } - - /** - * 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 - { - // Reset caches that might contain instances of standard types - static::$builtInTypes = null; - Introspection::resetCachedInstances(); - Directive::resetCachedInstances(); - - foreach ($types as $type) { - // @phpstan-ignore-next-line generic type is not enforced by PHP - if (! $type instanceof ScalarType) { - $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. * @@ -277,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 806658611..4b8ccb719 100644 --- a/src/Type/Introspection.php +++ b/src/Type/Introspection.php @@ -4,26 +4,9 @@ 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; /** @@ -62,6 +45,11 @@ 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, @@ -73,8 +61,45 @@ class Introspection self::DIRECTIVE_LOCATION_ENUM_NAME, ]; - /** @var array|null */ - protected static ?array $cachedInstances; + /** + * 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 @@ -243,592 +268,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 [ - 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(), - ]; - } - - 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(), - ], - ], - ]); - } - - 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, - ], - ], - ]); - } - - 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.', - ], - ], - ]); - } - - 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, - ], - ], - ]); - } - - 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, - ], - ], - ]); - } - - 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, - ], - ], - ]); - } - - 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; - }, - ], - ], - ]); - } - - 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.', - ], - ], - ]); - } - - 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, - ]); - } - - 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']), - ]); - } - - 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, - ]); - } - - public static function resetCachedInstances(): void - { - self::$cachedInstances = null; - } } diff --git a/src/Type/Schema.php b/src/Type/Schema.php index 03fc0034a..e0dae2f93 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 BuiltInDefinitions $builtInDefinitions; + /** 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->builtInDefinitions = $config->builtInDefinitions ?? BuiltInDefinitions::standard(); $this->config = $config; } @@ -158,13 +160,27 @@ 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) { TypeInfo::extractTypesFromDirectives($directive, $allReferencedTypes); } } - TypeInfo::extractTypes(Introspection::_schema(), $allReferencedTypes); + TypeInfo::extractTypes($this->builtInDefinitions->schemaType(), $allReferencedTypes); $this->resolvedTypes = $allReferencedTypes; $this->fullyLoaded = true; @@ -184,7 +200,8 @@ public function getTypeMap(): array */ public function getDirectives(): array { - return $this->config->directives ?? GraphQL::getStandardDirectives(); + return $this->config->directives + ?? $this->builtInDefinitions->directives(); } /** @param mixed $typeLoaderReturn could be anything */ @@ -272,6 +289,11 @@ public function getSubscriptionType(): ?ObjectType return $subscription; } + public function getBuiltInDefinitions(): BuiltInDefinitions + { + return $this->builtInDefinitions; + } + /** @api */ public function getConfig(): SchemaConfig { @@ -293,14 +315,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]; + $builtInTypes = $this->builtInDefinitions->types(); + if (isset($builtInTypes[$name])) { + return $builtInTypes[$name]; } $type = $this->loadType($name); @@ -511,9 +528,8 @@ public function assertValid(): void throw new InvariantViolation(implode("\n\n", $this->validationErrors)); } - $internalTypes = Type::getStandardTypes() + Introspection::getTypes(); 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 abf82dd4f..8075b408f 100644 --- a/src/Type/SchemaConfig.php +++ b/src/Type/SchemaConfig.php @@ -37,6 +37,7 @@ * types?: Types|null, * directives?: array|null, * typeLoader?: TypeLoader|null, + * builtInDefinitions?: BuiltInDefinitions|null, * assumeValid?: bool|null, * astNode?: SchemaDefinitionNode|null, * extensionASTNodes?: array|null, @@ -65,6 +66,8 @@ class SchemaConfig /** @var array|null */ public ?array $directives = null; + public ?BuiltInDefinitions $builtInDefinitions = null; + /** * @var callable|null * @@ -117,6 +120,10 @@ public static function create(array $options = []): self $config->setDirectives($options['directives']); } + if (isset($options['builtInDefinitions'])) { + $config->setBuiltInDefinitions($options['builtInDefinitions']); + } + if (isset($options['typeLoader'])) { $config->setTypeLoader($options['typeLoader']); } @@ -274,6 +281,18 @@ public function setDirectives(?array $directives): self return $this; } + public function getBuiltInDefinitions(): ?BuiltInDefinitions + { + return $this->builtInDefinitions; + } + + public function setBuiltInDefinitions(?BuiltInDefinitions $builtInDefinitions): self + { + $this->builtInDefinitions = $builtInDefinitions; + + return $this; + } + /** * @return callable|null $typeLoader * 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 7cd55e78b..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; @@ -21,7 +22,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 +110,7 @@ public function buildSchema(): Schema $schemaIntrospection = $this->introspection['__schema']; - $builtInTypes = array_merge( - Type::getStandardTypes(), - Introspection::getTypes() - ); + $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 4fdade26e..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\Definition\Directive; +use GraphQL\Type\BuiltInDefinitions; use GraphQL\Type\Definition\Type; use GraphQL\Type\Schema; use GraphQL\Type\SchemaConfig; @@ -217,23 +217,19 @@ static function (string $typeName): Type { $directiveDefs ); + /** @var array $directivesByName */ $directivesByName = []; foreach ($directives as $directive) { - $directivesByName[$directive->name][] = $directive; + $directivesByName[$directive->name] = true; } - // If specified directives were not explicitly declared, add them. - if (! isset($directivesByName['include'])) { - $directives[] = Directive::includeDirective(); - } - if (! isset($directivesByName['skip'])) { - $directives[] = Directive::skipDirective(); - } - if (! isset($directivesByName['deprecated'])) { - $directives[] = Directive::deprecatedDirective(); - } - if (! isset($directivesByName['oneOf'])) { - $directives[] = Directive::oneOfDirective(); + $builtInDirectives = BuiltInDefinitions::standard()->directives(); + foreach ($builtInDirectives as $name => $directive) { + if (isset($directivesByName[$name])) { + continue; + } + + $directives[] = $directive; } // Note: While this could make early assertions to get the correctly @@ -242,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 ac60efaf2..93370aa86 100644 --- a/src/Utils/TypeInfo.php +++ b/src/Utils/TypeInfo.php @@ -338,19 +338,26 @@ public function getParentType(): ?CompositeType private static function getFieldDefinition(Schema $schema, Type $parentType, FieldNode $fieldNode): ?FieldDefinition { $name = $fieldNode->name->value; - $schemaMeta = Introspection::schemaMetaFieldDef(); - if ($name === $schemaMeta->name && $schema->getQueryType() === $parentType) { - return $schemaMeta; + + if ($name === Introspection::SCHEMA_FIELD_NAME + && $schema->getQueryType() === $parentType + ) { + return $schema->getBuiltInDefinitions() + ->schemaMetaFieldDef(); } - $typeMeta = Introspection::typeMetaFieldDef(); - if ($name === $typeMeta->name && $schema->getQueryType() === $parentType) { - return $typeMeta; + if ($name === Introspection::TYPE_FIELD_NAME + && $schema->getQueryType() === $parentType + ) { + return $schema->getBuiltInDefinitions() + ->typeMetaFieldDef(); } - $typeNameMeta = Introspection::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 b61932135..780f5a6fc 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(); + $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/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 { diff --git a/tests/Type/BuiltInDefinitionsTest.php b/tests/Type/BuiltInDefinitionsTest.php new file mode 100644 index 000000000..bdc3fc747 --- /dev/null +++ b/tests/Type/BuiltInDefinitionsTest.php @@ -0,0 +1,285 @@ +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 BuiltInDefinitions(); + $b = new BuiltInDefinitions(); + + 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, + ]); + + $builtInDefinitions = new BuiltInDefinitions([Type::STRING => $customString]); + + self::assertSame($customString, $builtInDefinitions->string()); + self::assertSame($customString, $builtInDefinitions->scalarTypes()[Type::STRING]); + self::assertSame($customString, $builtInDefinitions->types()[Type::STRING]); + + $typeNameMeta = $builtInDefinitions->typeNameMetaFieldDef(); + $innerType = Type::getNullableType($typeNameMeta->getType()); + self::assertSame($customString, $innerType); + + $deprecatedDirective = $builtInDefinitions->deprecatedDirective(); + self::assertSame($customString, $deprecatedDirective->args[0]->getType()); + } + + public function testTwoSchemasWithDifferentBuiltInDefinitions(): void + { + $queryType = new ObjectType([ + 'name' => 'Query', + 'fields' => ['hello' => Type::string()], + ]); + + $builtInDefinitionsA = new BuiltInDefinitions(); + $builtInDefinitionsB = new BuiltInDefinitions(); + + $schemaA = new Schema( + (new SchemaConfig()) + ->setQuery($queryType) + ->setBuiltInDefinitions($builtInDefinitionsA) + ); + + $schemaB = new Schema( + (new SchemaConfig()) + ->setQuery($queryType) + ->setBuiltInDefinitions($builtInDefinitionsB) + ); + + self::assertSame($builtInDefinitionsA, $schemaA->getBuiltInDefinitions()); + self::assertSame($builtInDefinitionsB, $schemaB->getBuiltInDefinitions()); + self::assertNotSame($schemaA->getBuiltInDefinitions(), $schemaB->getBuiltInDefinitions()); + + self::assertNotSame( + $schemaA->getBuiltInDefinitions()->schemaType(), + $schemaB->getBuiltInDefinitions()->schemaType() + ); + } + + 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']); + } + + /** @see https://github.com/webonyx/graphql-php/issues/1424 */ + 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 }'); + + self::assertSame([], $result->errors); + assert(is_array($result->data)); + self::assertSame('world', $result->data['hello']); + } + + public function testAllTypesContainsScalarsAndIntrospection(): void + { + $builtInDefinitions = new BuiltInDefinitions(); + $allTypes = $builtInDefinitions->types(); + + 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 + { + $builtInDefinitions = new BuiltInDefinitions(); + $directives = $builtInDefinitions->directives(); + + self::assertArrayHasKey('include', $directives); + self::assertArrayHasKey('skip', $directives); + self::assertArrayHasKey('deprecated', $directives); + self::assertArrayHasKey('oneOf', $directives); + self::assertCount(4, $directives); + } + + /** Scalar override must take effect even with assertions disabled and validation skipped. */ + 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 { + if (is_string($prevAssertActive)) { + @ini_set('assert.active', $prevAssertActive); + } + } + + self::assertSame([], $result->errors); + assert(is_array($result->data)); + self::assertSame('HELLO WORLD', $result->data['greeting']); + } + + /** Scalar override via BuiltInDefinitions must not break validation when fields use Type::string(). */ + 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) + ); + + $schema->assertValid(); + self::assertSame($upperString, $schema->getType(Type::STRING)); + } + + /** Scalar override must work at runtime even when fields use Type::string(). */ + 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 }'); + + self::assertSame([], $result->errors); + assert(is_array($result->data)); + self::assertSame('HELLO WORLD', $result->data['greeting']); + } +} 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],