From d9836d821b4dbb9af609b24c7370ff5348b0a0d2 Mon Sep 17 00:00:00 2001 From: Jeroen de Graaf Date: Mon, 17 Feb 2025 20:54:51 +0100 Subject: [PATCH] [New] Introducing Union Types Allows the GraphQL abstract type: union. --- docs/todo.md | 2 +- docs/usage.md | 66 ++- phpstan-baseline.php | 102 +++- src/Attribute/Option/UnionType.php | 23 + src/Node/TypeReference/UnionTypeReference.php | 83 +++ src/NodeParser/TypeReferenceDecider.php | 60 +- src/Resolver/Type/UnionTypeResolver.php | 69 +++ src/SchemaBuilderFactory.php | 6 + .../Query/WithUnionOutputQuery.php | 18 + .../Doubles/Query/TestQueryWithUnionType.php | 41 ++ tests/NodeParser/TypeReferenceDeciderTest.php | 96 ++++ tests/ParserTest.php | 11 + tests/SchemaBuilderTest.php | 541 ++++++++++++++++++ tests/Util/Finder/Native/NativeFinderTest.php | 1 + 14 files changed, 1099 insertions(+), 20 deletions(-) create mode 100644 src/Attribute/Option/UnionType.php create mode 100644 src/Node/TypeReference/UnionTypeReference.php create mode 100644 src/Resolver/Type/UnionTypeResolver.php create mode 100644 tests/Doubles/FullFeatured/Query/WithUnionOutputQuery.php create mode 100644 tests/Doubles/Query/TestQueryWithUnionType.php diff --git a/docs/todo.md b/docs/todo.md index 05f0977..809fa70 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -10,6 +10,6 @@ This library is still work in progress: - ~~Connection, edge, nodes (see https://relay.dev/graphql/connections.htm)~~ - ~~GraphQL interfaces, inheritance~~ - ~~Interfaces extending interfaces (see https://graphql.org/learn/schema/#interface-types)~~ +- ~~Union types (see https://graphql.org/learn/schema/#union-types)~~ - Subscriptions -- Union types (see https://graphql.org/learn/schema/#union-types) - Directives (see https://graphql.org/learn/schema/#directives) \ No newline at end of file diff --git a/docs/usage.md b/docs/usage.md index 9a88df0..37ced9d 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -1,6 +1,10 @@ # Usage Guide -At a minimum, you need to define a query and a mutation to build a valid schema. +- [Attributes](#attributes) +- [Union types](#union-types) +- [Connections (Pagination)](#connections-pagination) + +đź“Ś At a minimum, you need to define a query and a mutation to build a valid schema. ## Attributes @@ -17,7 +21,7 @@ You can use the following attributes: - [#[Arg]](#arg) - [#[Autowire]](#autowire) - [#[Scalar]](#scalar) -- [#[Cursor]](#cursor) (as part of [Connections (Pagination)](#connections-pagination)) +- [#[Cursor]](#cursor) More details on each attribute are provided below. @@ -67,12 +71,12 @@ Mutations and queries must: You can configure both `#[Mutation]` and `#[Query]` attributes: -| Option | Description | -|---------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `name` | Custom name for the mutation or query (instead of using the method name). | -| `description` | Description of the mutation or query, visible in the GraphQL schema. | -| `type` | Custom return type, which can be:
- A Type (FQCN)
- A `ScalarType` (e.g., `ScalarType::Int`)
- A `ListType` (e.g., `new ListType(ScalarType::Int)`)
- A `NullableType` (e.g., `new NullableType(SomeType::class)`)
- A combination of `ListType`, `NullableType`, and a Type FQCN or `ScalarType` (e.g., `new NullableType(new ListType(ScalarType::String))`) | -| `deprecationReason` | Marks the mutation or query as deprecated if set. | +| Option | Description | +|---------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `name` | Custom name for the mutation or query (instead of using the method name). | +| `description` | Description of the mutation or query, visible in the GraphQL schema. | +| `type` | Custom return type, which can be:
- A Type (FQCN)
- A `ScalarType` (e.g., `ScalarType::Int`)
- A `ListType` (e.g., `new ListType(ScalarType::Int)`)
- A `NullableType` (e.g., `new NullableType(SomeType::class)`)
- A combination of `ListType`, `NullableType`, and a Type FQCN or `ScalarType` (e.g., `new NullableType(new ListType(ScalarType::String))`)
- A `UnionType` (see [Union types](#union-types)) | +| `deprecationReason` | Marks the mutation or query as deprecated if set. | ### #[Type] @@ -381,7 +385,7 @@ You can configure the `#[Field]` attribute: |---------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `name` | Custom name for the field (instead of using the property/method name). | | `description` | Description of the field, visible in the GraphQL schema. | -| `type` | Custom return type, which can be:
- A Type (FQCN)
- A `ScalarType` (e.g., `ScalarType::Int`)
- A `ListType` (e.g., `new ListType(ScalarType::Int)`)
- A `NullableType` (e.g., `new NullableType(SomeType::class)`)
- A combination of `ListType`, `NullableType`, and a Type FQCN or `ScalarType` (e.g., `new NullableType(new ListType(ScalarType::String))`) | +| `type` | Custom return type, which can be:
- A Type (FQCN)
- A `ScalarType` (e.g., `ScalarType::Int`)
- A `ListType` (e.g., `new ListType(ScalarType::Int)`)
- A `NullableType` (e.g., `new NullableType(SomeType::class)`)
- A combination of `ListType`, `NullableType`, and a Type FQCN or `ScalarType` (e.g., `new NullableType(new ListType(ScalarType::String))`)
- A `UnionType` (see [Union types](#union-types)) | | `deprecationReason` | Marks the field as deprecated (only applicable in `#[Type]`). | ### #[Arg] @@ -556,6 +560,50 @@ You can configure the `#[Cursor]` attribute: |--------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `type` | Defines a custom return type. It can be:
- A class implementing `ScalarType` (FQCN)
- `ScalarType::String`
- A `NullableType` wrapping one of the above (e.g., `new NullableType(ScalarType::String)`)

*All cursor values resolve to a string.* | +## Union types +*GraphQL Attribute Schema* allows union types as defined by the GraphQL specification. + +> "Union types share similarities with Interface types, but they cannot define any shared fields among the constituent types." + +_See https://graphql.org/learn/schema/#union-types_ + +By default, a GraphQL union type cannot not define any shared fields. Instead, it acts like a group of object types. + +Therefore, in contrary to all other types, a *GraphQL Attribute Schema* union type **cannot** be defined by an attribute (as that would be an empty class/interface). + +Instead, you can define a union type by (a) a native PHP union return type, or (b) a custom set type `UnionType` in a `#[Query]` or `#[Mutation]`. + +With using `UnionType`, it is possible to define a custom name for the type. See below: + +```php +use Jerowork\GraphqlAttributeSchema\Attribute\Option\UnionType; +use Jerowork\GraphqlAttributeSchema\Attribute\Query; + +final readonly class Query +{ + // Define GraphQL union type by PHP return type only + #[Query] + public function getFoobar() : UserType|OtherType + { + // todo + } + + // Or define GraphQL union type by type in attribute + #[Query(type: new UnionType('FoobarUnionType', UserType::class, OtherType::class))] + public function getFoobar() + { + // todo + } + + // Or a combination of the above + #[Query(type: new UnionType('FoobarUnionType'))] + public function getFoobar() : UserType|OtherType + { + // todo + } +} +``` + ## Connections (Pagination) *GraphQL Attribute Schema* provides **built-in pagination** following diff --git a/phpstan-baseline.php b/phpstan-baseline.php index 2ce4074..e7f3738 100644 --- a/phpstan-baseline.php +++ b/phpstan-baseline.php @@ -313,6 +313,12 @@ 'count' => 1, 'path' => __DIR__ . '/src/Resolver/Type/InterfaceTypeResolver.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Parameter \\#1 \\$config of class GraphQL\\\\Type\\\\Definition\\\\UnionType constructor expects array\\{name\\?\\: string\\|null, description\\?\\: string\\|null, types\\: \\(callable\\(\\)\\: iterable\\\\)\\|iterable\\<\\(callable\\(\\)\\: GraphQL\\\\Type\\\\Definition\\\\ObjectType\\)\\|GraphQL\\\\Type\\\\Definition\\\\ObjectType\\>, resolveType\\?\\: \\(callable\\(mixed, mixed, GraphQL\\\\Type\\\\Definition\\\\ResolveInfo\\)\\: \\(callable\\(\\)\\: \\(GraphQL\\\\Type\\\\Definition\\\\ObjectType\\|string\\|null\\)\\|GraphQL\\\\Deferred\\|GraphQL\\\\Type\\\\Definition\\\\ObjectType\\|string\\|null\\)\\)\\|null, astNode\\?\\: GraphQL\\\\Language\\\\AST\\\\UnionTypeDefinitionNode\\|null, extensionASTNodes\\?\\: array\\\\|null\\}, array\\{name\\: string, types\\: list\\, resolveType\\: Closure\\(object\\)\\: GraphQL\\\\Type\\\\Definition\\\\Type\\} given\\.$#', + 'identifier' => 'argument.type', + 'count' => 1, + 'path' => __DIR__ . '/src/Resolver/Type/UnionTypeResolver.php', +]; $ignoreErrors[] = [ 'message' => '#^Method Jerowork\\\\GraphqlAttributeSchema\\\\Util\\\\Reflector\\\\Reflector\\:\\:getClasses\\(\\) return type with generic class ReflectionClass does not specify its types\\: T$#', 'identifier' => 'missingType.generics', @@ -331,6 +337,24 @@ 'count' => 1, 'path' => __DIR__ . '/tests/Doubles/FullFeatured/Query/DeprecatedQuery.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Method Jerowork\\\\GraphqlAttributeSchema\\\\Test\\\\Doubles\\\\FullFeatured\\\\Query\\\\WithUnionOutputQuery\\:\\:getUnionQuery\\(\\) never returns Jerowork\\\\GraphqlAttributeSchema\\\\Test\\\\Doubles\\\\FullFeatured\\\\Type\\\\AgentType so it can be removed from the return type\\.$#', + 'identifier' => 'return.unusedType', + 'count' => 1, + 'path' => __DIR__ . '/tests/Doubles/FullFeatured/Query/WithUnionOutputQuery.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method Jerowork\\\\GraphqlAttributeSchema\\\\Test\\\\Doubles\\\\FullFeatured\\\\Query\\\\WithUnionOutputQuery\\:\\:getUnionQuery\\(\\) never returns Jerowork\\\\GraphqlAttributeSchema\\\\Test\\\\Doubles\\\\FullFeatured\\\\Type\\\\FoobarType so it can be removed from the return type\\.$#', + 'identifier' => 'return.unusedType', + 'count' => 1, + 'path' => __DIR__ . '/tests/Doubles/FullFeatured/Query/WithUnionOutputQuery.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method Jerowork\\\\GraphqlAttributeSchema\\\\Test\\\\Doubles\\\\FullFeatured\\\\Query\\\\WithUnionOutputQuery\\:\\:getUnionQuery\\(\\) should return Jerowork\\\\GraphqlAttributeSchema\\\\Test\\\\Doubles\\\\FullFeatured\\\\Type\\\\AgentType\\|Jerowork\\\\GraphqlAttributeSchema\\\\Test\\\\Doubles\\\\FullFeatured\\\\Type\\\\FoobarType but returns string\\.$#', + 'identifier' => 'return.type', + 'count' => 1, + 'path' => __DIR__ . '/tests/Doubles/FullFeatured/Query/WithUnionOutputQuery.php', +]; $ignoreErrors[] = [ 'message' => '#^Method Jerowork\\\\GraphqlAttributeSchema\\\\Test\\\\Doubles\\\\FullFeatured\\\\Type\\\\FoobarType\\:\\:getDate\\(\\) never returns null so it can be removed from the return type\\.$#', 'identifier' => 'return.unusedType', @@ -343,6 +367,72 @@ 'count' => 1, 'path' => __DIR__ . '/tests/Doubles/Mutation/TestInvalidMutationWithInvalidMethodArgument.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Method Jerowork\\\\GraphqlAttributeSchema\\\\Test\\\\Doubles\\\\Query\\\\TestQueryWithUnionType\\:\\:getBazs\\(\\) never returns Jerowork\\\\GraphqlAttributeSchema\\\\Test\\\\Doubles\\\\Type\\\\TestType so it can be removed from the return type\\.$#', + 'identifier' => 'return.unusedType', + 'count' => 1, + 'path' => __DIR__ . '/tests/Doubles/Query/TestQueryWithUnionType.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method Jerowork\\\\GraphqlAttributeSchema\\\\Test\\\\Doubles\\\\Query\\\\TestQueryWithUnionType\\:\\:getBazs\\(\\) never returns Jerowork\\\\GraphqlAttributeSchema\\\\Test\\\\Doubles\\\\Type\\\\TestTypeWithAutowire so it can be removed from the return type\\.$#', + 'identifier' => 'return.unusedType', + 'count' => 1, + 'path' => __DIR__ . '/tests/Doubles/Query/TestQueryWithUnionType.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method Jerowork\\\\GraphqlAttributeSchema\\\\Test\\\\Doubles\\\\Query\\\\TestQueryWithUnionType\\:\\:getBazs\\(\\) should return Jerowork\\\\GraphqlAttributeSchema\\\\Test\\\\Doubles\\\\Type\\\\TestType\\|Jerowork\\\\GraphqlAttributeSchema\\\\Test\\\\Doubles\\\\Type\\\\TestTypeWithAutowire but returns string\\.$#', + 'identifier' => 'return.type', + 'count' => 1, + 'path' => __DIR__ . '/tests/Doubles/Query/TestQueryWithUnionType.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method Jerowork\\\\GraphqlAttributeSchema\\\\Test\\\\Doubles\\\\Query\\\\TestQueryWithUnionType\\:\\:getFoobars\\(\\) never returns Jerowork\\\\GraphqlAttributeSchema\\\\Test\\\\Doubles\\\\Type\\\\TestType so it can be removed from the return type\\.$#', + 'identifier' => 'return.unusedType', + 'count' => 1, + 'path' => __DIR__ . '/tests/Doubles/Query/TestQueryWithUnionType.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method Jerowork\\\\GraphqlAttributeSchema\\\\Test\\\\Doubles\\\\Query\\\\TestQueryWithUnionType\\:\\:getFoobars\\(\\) never returns Jerowork\\\\GraphqlAttributeSchema\\\\Test\\\\Doubles\\\\Type\\\\TestTypeWithAutowire so it can be removed from the return type\\.$#', + 'identifier' => 'return.unusedType', + 'count' => 1, + 'path' => __DIR__ . '/tests/Doubles/Query/TestQueryWithUnionType.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method Jerowork\\\\GraphqlAttributeSchema\\\\Test\\\\Doubles\\\\Query\\\\TestQueryWithUnionType\\:\\:getFoobars\\(\\) should return Jerowork\\\\GraphqlAttributeSchema\\\\Test\\\\Doubles\\\\Type\\\\TestType\\|Jerowork\\\\GraphqlAttributeSchema\\\\Test\\\\Doubles\\\\Type\\\\TestTypeWithAutowire but returns string\\.$#', + 'identifier' => 'return.type', + 'count' => 1, + 'path' => __DIR__ . '/tests/Doubles/Query/TestQueryWithUnionType.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method Jerowork\\\\GraphqlAttributeSchema\\\\Test\\\\Doubles\\\\Query\\\\TestQueryWithUnionType\\:\\:getListOfQuxs\\(\\) return type has no value type specified in iterable type array\\.$#', + 'identifier' => 'missingType.iterableValue', + 'count' => 1, + 'path' => __DIR__ . '/tests/Doubles/Query/TestQueryWithUnionType.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method Jerowork\\\\GraphqlAttributeSchema\\\\Test\\\\Doubles\\\\Query\\\\TestQueryWithUnionType\\:\\:getNullableBazs\\(\\) never returns Jerowork\\\\GraphqlAttributeSchema\\\\Test\\\\Doubles\\\\Type\\\\TestType so it can be removed from the return type\\.$#', + 'identifier' => 'return.unusedType', + 'count' => 1, + 'path' => __DIR__ . '/tests/Doubles/Query/TestQueryWithUnionType.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method Jerowork\\\\GraphqlAttributeSchema\\\\Test\\\\Doubles\\\\Query\\\\TestQueryWithUnionType\\:\\:getNullableBazs\\(\\) never returns Jerowork\\\\GraphqlAttributeSchema\\\\Test\\\\Doubles\\\\Type\\\\TestTypeWithAutowire so it can be removed from the return type\\.$#', + 'identifier' => 'return.unusedType', + 'count' => 1, + 'path' => __DIR__ . '/tests/Doubles/Query/TestQueryWithUnionType.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method Jerowork\\\\GraphqlAttributeSchema\\\\Test\\\\Doubles\\\\Query\\\\TestQueryWithUnionType\\:\\:getNullableBazs\\(\\) never returns null so it can be removed from the return type\\.$#', + 'identifier' => 'return.unusedType', + 'count' => 1, + 'path' => __DIR__ . '/tests/Doubles/Query/TestQueryWithUnionType.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method Jerowork\\\\GraphqlAttributeSchema\\\\Test\\\\Doubles\\\\Query\\\\TestQueryWithUnionType\\:\\:getNullableBazs\\(\\) should return Jerowork\\\\GraphqlAttributeSchema\\\\Test\\\\Doubles\\\\Type\\\\TestType\\|Jerowork\\\\GraphqlAttributeSchema\\\\Test\\\\Doubles\\\\Type\\\\TestTypeWithAutowire\\|null but returns string\\.$#', + 'identifier' => 'return.type', + 'count' => 1, + 'path' => __DIR__ . '/tests/Doubles/Query/TestQueryWithUnionType.php', +]; $ignoreErrors[] = [ 'message' => '#^Method Jerowork\\\\GraphqlAttributeSchema\\\\Test\\\\Doubles\\\\Type\\\\TestCascadingInterfaceType\\:\\:getStatus\\(\\) never returns null so it can be removed from the return type\\.$#', 'identifier' => 'return.unusedType', @@ -472,19 +562,25 @@ $ignoreErrors[] = [ 'message' => '#^Parameter \\#1 \\$config of class GraphQL\\\\Type\\\\Definition\\\\InterfaceType constructor expects array\\{name\\?\\: string\\|null, description\\?\\: string\\|null, fields\\: \\(callable\\(\\)\\: iterable\\)\\|iterable, interfaces\\?\\: \\(callable\\(\\)\\: iterable\\\\)\\|iterable\\<\\(callable\\(\\)\\: GraphQL\\\\Type\\\\Definition\\\\InterfaceType\\)\\|GraphQL\\\\Type\\\\Definition\\\\InterfaceType\\>, resolveType\\?\\: \\(callable\\(mixed, mixed, GraphQL\\\\Type\\\\Definition\\\\ResolveInfo\\)\\: \\(callable\\(\\)\\: \\(GraphQL\\\\Type\\\\Definition\\\\ObjectType\\|string\\|null\\)\\|GraphQL\\\\Deferred\\|GraphQL\\\\Type\\\\Definition\\\\ObjectType\\|string\\|null\\)\\)\\|null, astNode\\?\\: GraphQL\\\\Language\\\\AST\\\\InterfaceTypeDefinitionNode\\|null, extensionASTNodes\\?\\: array\\\\|null\\}, array\\{name\\: \'Admin\', description\\: null, fields\\: array\\{array\\{name\\: \'adminName\', type\\: GraphQL\\\\Type\\\\Definition\\\\NonNull, description\\: null, args\\: array\\{\\}, resolve\\: Closure\\(\\)\\: true\\}, array\\{name\\: \'password\', type\\: GraphQL\\\\Type\\\\Definition\\\\NonNull, description\\: null, args\\: array\\{\\}, resolve\\: Closure\\(\\)\\: true\\}, array\\{name\\: \'isAdmin\', type\\: GraphQL\\\\Type\\\\Definition\\\\NonNull, description\\: null, args\\: array\\{\\}, resolve\\: Closure\\(\\)\\: true\\}, array\\{name\\: \'recipientId\', type\\: GraphQL\\\\Type\\\\Definition\\\\NonNull, description\\: null, args\\: array\\{\\}, resolve\\: Closure\\(\\)\\: true\\}\\}, resolveType\\: Closure\\(\\)\\: true\\} given\\.$#', 'identifier' => 'argument.type', - 'count' => 2, + 'count' => 5, 'path' => __DIR__ . '/tests/SchemaBuilderTest.php', ]; $ignoreErrors[] = [ 'message' => '#^Parameter \\#1 \\$config of class GraphQL\\\\Type\\\\Definition\\\\InterfaceType constructor expects array\\{name\\?\\: string\\|null, description\\?\\: string\\|null, fields\\: \\(callable\\(\\)\\: iterable\\)\\|iterable, interfaces\\?\\: \\(callable\\(\\)\\: iterable\\\\)\\|iterable\\<\\(callable\\(\\)\\: GraphQL\\\\Type\\\\Definition\\\\InterfaceType\\)\\|GraphQL\\\\Type\\\\Definition\\\\InterfaceType\\>, resolveType\\?\\: \\(callable\\(mixed, mixed, GraphQL\\\\Type\\\\Definition\\\\ResolveInfo\\)\\: \\(callable\\(\\)\\: \\(GraphQL\\\\Type\\\\Definition\\\\ObjectType\\|string\\|null\\)\\|GraphQL\\\\Deferred\\|GraphQL\\\\Type\\\\Definition\\\\ObjectType\\|string\\|null\\)\\)\\|null, astNode\\?\\: GraphQL\\\\Language\\\\AST\\\\InterfaceTypeDefinitionNode\\|null, extensionASTNodes\\?\\: array\\\\|null\\}, array\\{name\\: \'Recipient\', description\\: null, fields\\: array\\{array\\{name\\: \'recipientId\', type\\: GraphQL\\\\Type\\\\Definition\\\\NonNull, description\\: null, args\\: array\\{\\}, resolve\\: Closure\\(\\)\\: true\\}\\}, resolveType\\: Closure\\(\\)\\: true\\} given\\.$#', 'identifier' => 'argument.type', - 'count' => 2, + 'count' => 5, 'path' => __DIR__ . '/tests/SchemaBuilderTest.php', ]; $ignoreErrors[] = [ 'message' => '#^Parameter \\#1 \\$config of class GraphQL\\\\Type\\\\Definition\\\\InterfaceType constructor expects array\\{name\\?\\: string\\|null, description\\?\\: string\\|null, fields\\: \\(callable\\(\\)\\: iterable\\)\\|iterable, interfaces\\?\\: \\(callable\\(\\)\\: iterable\\\\)\\|iterable\\<\\(callable\\(\\)\\: GraphQL\\\\Type\\\\Definition\\\\InterfaceType\\)\\|GraphQL\\\\Type\\\\Definition\\\\InterfaceType\\>, resolveType\\?\\: \\(callable\\(mixed, mixed, GraphQL\\\\Type\\\\Definition\\\\ResolveInfo\\)\\: \\(callable\\(\\)\\: \\(GraphQL\\\\Type\\\\Definition\\\\ObjectType\\|string\\|null\\)\\|GraphQL\\\\Deferred\\|GraphQL\\\\Type\\\\Definition\\\\ObjectType\\|string\\|null\\)\\)\\|null, astNode\\?\\: GraphQL\\\\Language\\\\AST\\\\InterfaceTypeDefinitionNode\\|null, extensionASTNodes\\?\\: array\\\\|null\\}, array\\{name\\: \'User\', description\\: null, fields\\: array\\{array\\{name\\: \'userId\', type\\: GraphQL\\\\Type\\\\Definition\\\\NonNull, description\\: null, args\\: array\\{\\}, resolve\\: Closure\\(\\)\\: true\\}\\}, resolveType\\: Closure\\(\\)\\: true\\} given\\.$#', 'identifier' => 'argument.type', - 'count' => 5, + 'count' => 8, + 'path' => __DIR__ . '/tests/SchemaBuilderTest.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Parameter \\#1 \\$config of class GraphQL\\\\Type\\\\Definition\\\\UnionType constructor expects array\\{name\\?\\: string\\|null, description\\?\\: string\\|null, types\\: \\(callable\\(\\)\\: iterable\\\\)\\|iterable\\<\\(callable\\(\\)\\: GraphQL\\\\Type\\\\Definition\\\\ObjectType\\)\\|GraphQL\\\\Type\\\\Definition\\\\ObjectType\\>, resolveType\\?\\: \\(callable\\(mixed, mixed, GraphQL\\\\Type\\\\Definition\\\\ResolveInfo\\)\\: \\(callable\\(\\)\\: \\(GraphQL\\\\Type\\\\Definition\\\\ObjectType\\|string\\|null\\)\\|GraphQL\\\\Deferred\\|GraphQL\\\\Type\\\\Definition\\\\ObjectType\\|string\\|null\\)\\)\\|null, astNode\\?\\: GraphQL\\\\Language\\\\AST\\\\UnionTypeDefinitionNode\\|null, extensionASTNodes\\?\\: array\\\\|null\\}, array\\{name\\: \'Union_AgentType…\', types\\: array\\{GraphQL\\\\Type\\\\Definition\\\\ObjectType, GraphQL\\\\Type\\\\Definition\\\\ObjectType\\}, resolveType\\: Closure\\(\\)\\: true\\} given\\.$#', + 'identifier' => 'argument.type', + 'count' => 1, 'path' => __DIR__ . '/tests/SchemaBuilderTest.php', ]; diff --git a/src/Attribute/Option/UnionType.php b/src/Attribute/Option/UnionType.php new file mode 100644 index 0000000..342cc84 --- /dev/null +++ b/src/Attribute/Option/UnionType.php @@ -0,0 +1,23 @@ + + */ + public array $objectTypes; + + /** + * @param class-string ...$objectTypes + */ + public function __construct( + public string $name, + string ...$objectTypes, + ) { + $this->objectTypes = array_values($objectTypes); + } +} diff --git a/src/Node/TypeReference/UnionTypeReference.php b/src/Node/TypeReference/UnionTypeReference.php new file mode 100644 index 0000000..810a0e8 --- /dev/null +++ b/src/Node/TypeReference/UnionTypeReference.php @@ -0,0 +1,83 @@ +, + * isValueNullable: bool, + * isList: bool, + * isListNullable: bool + * } + * + * @internal + */ +final class UnionTypeReference implements ListableTypeReference +{ + use TypeReferenceTrait; + use ListableReferenceTrait; + + /** + * @param list $classNames + */ + public function __construct( + public readonly string $name, + public readonly array $classNames, + bool $isValueNullable, + bool $isList, + bool $isListNullable, + ) { + $this->isValueNullable = $isValueNullable; + $this->isList = $isList; + $this->isListNullable = $isListNullable; + } + + /** + * @param list $classNames + */ + public static function create(string $name, array $classNames): self + { + return new self($name, $classNames, false, false, false); + } + + /** + * @return UnionTypeReferencePayload + */ + public function toArray(): array + { + return [ + 'name' => $this->name, + 'classNames' => $this->classNames, + 'isValueNullable' => $this->isValueNullable(), + 'isList' => $this->isList(), + 'isListNullable' => $this->isListNullable(), + ]; + } + + /** + * @param UnionTypeReferencePayload $payload + */ + public static function fromArray(array $payload): UnionTypeReference + { + return new self( + $payload['name'], + $payload['classNames'], + $payload['isValueNullable'], + $payload['isList'], + $payload['isListNullable'], + ); + } + + public function equals(TypeReference $reference): bool + { + return $reference instanceof self + && $reference->name === $this->name + && $reference->classNames === $this->classNames + && $reference->isValueNullable === $this->isValueNullable + && $reference->isList === $this->isList + && $reference->isListNullable === $this->isListNullable; + } +} diff --git a/src/NodeParser/TypeReferenceDecider.php b/src/NodeParser/TypeReferenceDecider.php index 15b26e5..4cfb4f7 100644 --- a/src/NodeParser/TypeReferenceDecider.php +++ b/src/NodeParser/TypeReferenceDecider.php @@ -10,15 +10,18 @@ use Jerowork\GraphqlAttributeSchema\Attribute\Option\ObjectType; use Jerowork\GraphqlAttributeSchema\Attribute\Option\ScalarType; use Jerowork\GraphqlAttributeSchema\Attribute\Option\Type; +use Jerowork\GraphqlAttributeSchema\Attribute\Option\UnionType; use Jerowork\GraphqlAttributeSchema\Attribute\TypedAttribute; use Jerowork\GraphqlAttributeSchema\Node\TypeReference\ConnectionTypeReference; use Jerowork\GraphqlAttributeSchema\Node\TypeReference\ListableTypeReference; use Jerowork\GraphqlAttributeSchema\Node\TypeReference\ObjectTypeReference; use Jerowork\GraphqlAttributeSchema\Node\TypeReference\ScalarTypeReference; use Jerowork\GraphqlAttributeSchema\Node\TypeReference\TypeReference; +use Jerowork\GraphqlAttributeSchema\Node\TypeReference\UnionTypeReference; use LogicException; use ReflectionNamedType; use ReflectionType; +use ReflectionUnionType; /** * @internal @@ -42,7 +45,7 @@ public function getTypeReference(?ReflectionType $reflectionType, ?TypedAttribut if ($attributeType instanceof ListType) { if ($attributeType->type instanceof NullableType) { - $type = $this->getReferenceFromAttribute($attributeType->type->type); + $type = $this->getReferenceFromAttribute($attributeType->type->type, $reflectionType); if (!$type instanceof ListableTypeReference) { throw ParseException::invalidListTypeConfiguration($type::class); @@ -51,7 +54,7 @@ public function getTypeReference(?ReflectionType $reflectionType, ?TypedAttribut return $type->setList()->setNullableValue(); } - $type = $this->getReferenceFromAttribute($attributeType->type); + $type = $this->getReferenceFromAttribute($attributeType->type, $reflectionType); if (!$type instanceof ListableTypeReference) { throw ParseException::invalidListTypeConfiguration($type::class); @@ -68,7 +71,7 @@ public function getTypeReference(?ReflectionType $reflectionType, ?TypedAttribut if ($attributeType->type instanceof ListType) { if ($attributeType->type->type instanceof NullableType) { - $type = $this->getReferenceFromAttribute($attributeType->type->type->type); + $type = $this->getReferenceFromAttribute($attributeType->type->type->type, $reflectionType); if (!$type instanceof ListableTypeReference) { throw ParseException::invalidListTypeConfiguration($type::class); @@ -77,7 +80,7 @@ public function getTypeReference(?ReflectionType $reflectionType, ?TypedAttribut return $type->setList()->setNullableList()->setNullableValue(); } - $type = $this->getReferenceFromAttribute($attributeType->type->type); + $type = $this->getReferenceFromAttribute($attributeType->type->type, $reflectionType); if (!$type instanceof ListableTypeReference) { throw ParseException::invalidListTypeConfiguration($type::class); @@ -86,14 +89,36 @@ public function getTypeReference(?ReflectionType $reflectionType, ?TypedAttribut return $type->setList()->setNullableList(); } - return $this->getReferenceFromAttribute($attributeType->type) + return $this->getReferenceFromAttribute($attributeType->type, $reflectionType) ->setNullableValue(); } - return $this->getReferenceFromAttribute($attributeType); + return $this->getReferenceFromAttribute($attributeType, $reflectionType); } // Retrieve from class + + if ($reflectionType instanceof ReflectionUnionType) { + /** @var list $classNames */ + $classNames = array_values(array_map( + fn($type) => $type->getName(), + array_filter( + $reflectionType->getTypes(), + fn($type) => $type instanceof ReflectionNamedType && !$type->isBuiltin(), + ), + )); + + $type = UnionTypeReference::create( + sprintf('Union_%s', implode('_', array_map( + fn($classname) => array_values(array_reverse(explode('\\', $classname)))[0], + $classNames, + ))), + $classNames, + ); + + return $reflectionType->allowsNull() ? $type->setNullableValue() : $type; + } + if (!$reflectionType instanceof ReflectionNamedType) { return null; } @@ -114,7 +139,7 @@ public function getTypeReference(?ReflectionType $reflectionType, ?TypedAttribut return $reflectionType->allowsNull() ? $type->setNullableValue() : $type; } - private function getReferenceFromAttribute(ScalarType|string|Type $type): TypeReference + private function getReferenceFromAttribute(ScalarType|string|Type $type, ?ReflectionType $reflectionType): TypeReference { if ($type instanceof ScalarType) { return ScalarTypeReference::create($type->value); @@ -124,6 +149,27 @@ private function getReferenceFromAttribute(ScalarType|string|Type $type): TypeRe return ObjectTypeReference::create($type->className); } + if ($type instanceof UnionType) { + $classNames = $type->objectTypes; + + if ($classNames === []) { + if (!$reflectionType instanceof ReflectionUnionType) { + throw new LogicException('UnionType is missing union types as return type'); + } + + /** @var list $classNames */ + $classNames = array_values(array_map( + fn($returnType) => $returnType->getName(), + array_filter( + $reflectionType->getTypes(), + fn($type) => $type instanceof ReflectionNamedType && !$type->isBuiltin(), + ), + )); + } + + return UnionTypeReference::create($type->name, $classNames); + } + if (is_string($type)) { /** @var class-string $type */ return ObjectTypeReference::create($type); diff --git a/src/Resolver/Type/UnionTypeResolver.php b/src/Resolver/Type/UnionTypeResolver.php new file mode 100644 index 0000000..93366d4 --- /dev/null +++ b/src/Resolver/Type/UnionTypeResolver.php @@ -0,0 +1,69 @@ + $reference->name, + 'types' => array_map( + function (string $className) { + $reference = ObjectTypeReference::create($className); + $type = $this->getTypeResolverSelector()->getResolver($reference)->createType($reference); + + return $type instanceof WrappingType ? $type->getWrappedType() : $type; + }, + $reference->classNames, + ), + 'resolveType' => fn(object $objectValue) => $this->builtTypesRegistry->getType($objectValue::class), + ]); + } + + #[Override] + public function resolve(TypeReference $reference, Closure $callback): mixed + { + throw new LogicException('UnionType does not need to resolve'); + } + + #[Override] + public function abstract(ArgumentNode|FieldNode $node, array $args): mixed + { + throw new LogicException('UnionType does not need to abstract'); + } +} diff --git a/src/SchemaBuilderFactory.php b/src/SchemaBuilderFactory.php index 68ffea7..9ed247d 100644 --- a/src/SchemaBuilderFactory.php +++ b/src/SchemaBuilderFactory.php @@ -17,6 +17,7 @@ use Jerowork\GraphqlAttributeSchema\Resolver\Type\ListAndNullableTypeResolverDecorator; use Jerowork\GraphqlAttributeSchema\Resolver\Type\ObjectTypeResolver; use Jerowork\GraphqlAttributeSchema\Resolver\Type\TypeResolverSelector; +use Jerowork\GraphqlAttributeSchema\Resolver\Type\UnionTypeResolver; use Psr\Container\ContainerInterface; final readonly class SchemaBuilderFactory @@ -60,6 +61,11 @@ public function create( new InterfaceTypeResolver($astContainer, $builtTypesRegistry, $fieldResolver), $builtTypesRegistry, )), + new ListAndNullableTypeResolverDecorator(new BuiltTypesRegistryTypeResolverDecorator( + $astContainer, + new UnionTypeResolver($builtTypesRegistry), + $builtTypesRegistry, + )), new ListAndNullableTypeResolverDecorator( new ConnectionTypeResolver($astContainer, $builtTypesRegistry, $container, $fieldResolver), ), diff --git a/tests/Doubles/FullFeatured/Query/WithUnionOutputQuery.php b/tests/Doubles/FullFeatured/Query/WithUnionOutputQuery.php new file mode 100644 index 0000000..0a5bf13 --- /dev/null +++ b/tests/Doubles/FullFeatured/Query/WithUnionOutputQuery.php @@ -0,0 +1,18 @@ +equals(ObjectTypeReference::create(DateTime::class))); } + #[Test] + public function itShouldReturnUnionFromAttributeWithSetObjectTypes(): void + { + $decider = new TypeReferenceDecider(); + + $class = new ReflectionClass(TestQueryWithUnionType::class); + $methods = $class->getMethod('getQuxs'); + $type = $methods->getReturnType(); + + $nodeType = $decider->getTypeReference( + $type, + new Query(type: new UnionType('FoobarResults', TestType::class, TestTypeWithAutowire::class)), + ); + + self::assertInstanceOf(ReflectionNamedType::class, $type); + self::assertTrue($nodeType?->equals(UnionTypeReference::create('FoobarResults', [TestType::class, TestTypeWithAutowire::class]))); + } + + #[Test] + public function itShouldReturnUnionFromAttributeWithSetObjectTypesAsReturnType(): void + { + $decider = new TypeReferenceDecider(); + + $class = new ReflectionClass(TestQueryWithUnionType::class); + $methods = $class->getMethod('getFoobars'); + $type = $methods->getReturnType(); + + $nodeType = $decider->getTypeReference( + $type, + new Query(type: new UnionType('FoobarResults')), + ); + + self::assertInstanceOf(ReflectionUnionType::class, $type); + self::assertTrue($nodeType?->equals(UnionTypeReference::create('FoobarResults', [TestType::class, TestTypeWithAutowire::class]))); + } + + #[Test] + public function itShouldReturnListOfUnionFromAttribute(): void + { + $decider = new TypeReferenceDecider(); + + $class = new ReflectionClass(TestQueryWithUnionType::class); + $methods = $class->getMethod('getListOfQuxs'); + $type = $methods->getReturnType(); + + $nodeType = $decider->getTypeReference( + $type, + new Query(type: new ListType(new UnionType('FoobarResults', TestType::class, TestTypeWithAutowire::class))), + ); + + self::assertInstanceOf(ReflectionNamedType::class, $type); + self::assertTrue($nodeType?->equals(UnionTypeReference::create( + 'FoobarResults', + [TestType::class, TestTypeWithAutowire::class], + )->setList())); + } + #[Test] public function itShouldReturnNullableScalarFromAttribute(): void { @@ -224,4 +287,37 @@ public function itShouldReturnNullableConnectionFromAttribute(): void self::assertInstanceOf(ReflectionNamedType::class, $type); self::assertTrue($reference?->equals(ConnectionTypeReference::create(TestType::class, 12)->setNullableValue())); } + + #[Test] + public function itShouldReturnUnionFromReturnType(): void + { + $decider = new TypeReferenceDecider(); + + $class = new ReflectionClass(TestQueryWithUnionType::class); + $methods = $class->getMethod('getBazs'); + $type = $methods->getReturnType(); + + $nodeType = $decider->getTypeReference($type, new Query()); + + self::assertInstanceOf(ReflectionUnionType::class, $type); + self::assertTrue($nodeType?->equals(UnionTypeReference::create('Union_TestType_TestTypeWithAutowire', [TestType::class, TestTypeWithAutowire::class]))); + } + + #[Test] + public function itShouldReturnNullableUnionFromReturnType(): void + { + $decider = new TypeReferenceDecider(); + + $class = new ReflectionClass(TestQueryWithUnionType::class); + $methods = $class->getMethod('getNullableBazs'); + $type = $methods->getReturnType(); + + $nodeType = $decider->getTypeReference($type, new Query()); + + self::assertInstanceOf(ReflectionUnionType::class, $type); + self::assertTrue($nodeType?->equals(UnionTypeReference::create( + 'Union_TestType_TestTypeWithAutowire', + [TestType::class, TestTypeWithAutowire::class], + )->setNullableValue())); + } } diff --git a/tests/ParserTest.php b/tests/ParserTest.php index 1e58222..7cc2036 100644 --- a/tests/ParserTest.php +++ b/tests/ParserTest.php @@ -22,6 +22,7 @@ use Jerowork\GraphqlAttributeSchema\Node\TypeReference\ConnectionTypeReference; use Jerowork\GraphqlAttributeSchema\Node\TypeReference\ObjectTypeReference; use Jerowork\GraphqlAttributeSchema\Node\TypeReference\ScalarTypeReference; +use Jerowork\GraphqlAttributeSchema\Node\TypeReference\UnionTypeReference; use Jerowork\GraphqlAttributeSchema\Parser; use Jerowork\GraphqlAttributeSchema\ParserFactory; use Jerowork\GraphqlAttributeSchema\Test\Doubles\FullFeatured\Mutation\BasicMutation; @@ -32,6 +33,7 @@ use Jerowork\GraphqlAttributeSchema\Test\Doubles\FullFeatured\Query\WithInterfaceOutputQuery; use Jerowork\GraphqlAttributeSchema\Test\Doubles\FullFeatured\Query\WithListOutputQuery; use Jerowork\GraphqlAttributeSchema\Test\Doubles\FullFeatured\Query\WithOverwrittenTypeQuery; +use Jerowork\GraphqlAttributeSchema\Test\Doubles\FullFeatured\Query\WithUnionOutputQuery; use Jerowork\GraphqlAttributeSchema\Test\Doubles\FullFeatured\Type\AbstractAdminType; use Jerowork\GraphqlAttributeSchema\Test\Doubles\FullFeatured\Type\AgentType; use Jerowork\GraphqlAttributeSchema\Test\Doubles\FullFeatured\Type\FoobarStatusType; @@ -166,6 +168,15 @@ public function itShouldParseDirectory(): void 'query', null, ), + new QueryNode( + WithUnionOutputQuery::class, + 'unionQuery', + null, + [], + UnionTypeReference::create('Union_AgentType_FoobarType', [AgentType::class, FoobarType::class]), + 'getUnionQuery', + null, + ), new QueryNode( WithConnectionOutputQuery::class, 'withConnectionOutput', diff --git a/tests/SchemaBuilderTest.php b/tests/SchemaBuilderTest.php index 548d5fd..65ffbef 100644 --- a/tests/SchemaBuilderTest.php +++ b/tests/SchemaBuilderTest.php @@ -11,6 +11,7 @@ use GraphQL\Type\Definition\InterfaceType; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type; +use GraphQL\Type\Definition\UnionType; use Jerowork\GraphqlAttributeSchema\Ast; use Jerowork\GraphqlAttributeSchema\Node\QueryNode; use Jerowork\GraphqlAttributeSchema\Node\TypeReference\ScalarTypeReference; @@ -28,6 +29,7 @@ use Jerowork\GraphqlAttributeSchema\Test\Doubles\FullFeatured\Query\WithInterfaceOutputQuery; use Jerowork\GraphqlAttributeSchema\Test\Doubles\FullFeatured\Query\WithListOutputQuery; use Jerowork\GraphqlAttributeSchema\Test\Doubles\FullFeatured\Query\WithOverwrittenTypeQuery; +use Jerowork\GraphqlAttributeSchema\Test\Doubles\FullFeatured\Query\WithUnionOutputQuery; use Jerowork\GraphqlAttributeSchema\Test\Doubles\Query\TestQuery; use Override; use PHPUnit\Framework\Attributes\Test; @@ -92,6 +94,7 @@ public function itShouldBuildSchema(): void $this->container->set(WithInterfaceOutputQuery::class, new WithInterfaceOutputQuery()); $this->container->set(WithListOutputQuery::class, new WithListOutputQuery()); $this->container->set(WithOverwrittenTypeQuery::class, new WithOverwrittenTypeQuery()); + $this->container->set(WithUnionOutputQuery::class, new WithUnionOutputQuery()); $ast = $this->parser->parse(__DIR__ . '/Doubles/FullFeatured'); @@ -366,6 +369,544 @@ public function itShouldBuildSchema(): void 'args' => [], 'resolve' => fn() => true, ]), + 'unionQuery' => new FieldDefinition([ + 'name' => 'unionQuery', + 'type' => Type::nonNull(new UnionType([ + 'name' => 'Union_AgentType_FoobarType', + 'types' => [ + new ObjectType([ + 'name' => 'Agent', + 'description' => null, + 'fields' => [ + [ + 'name' => 'recipientId', + 'type' => Type::nonNull(Type::int()), + 'description' => null, + 'args' => [], + 'resolve' => fn() => true, + ], + [ + 'name' => 'userId', + 'type' => Type::nonNull(Type::int()), + 'description' => null, + 'args' => [], + 'resolve' => fn() => true, + ], + [ + 'name' => 'adminName', + 'type' => Type::nonNull(Type::string()), + 'description' => null, + 'args' => [], + 'resolve' => fn() => true, + ], + [ + 'name' => 'password', + 'type' => Type::nonNull(Type::string()), + 'description' => null, + 'args' => [], + 'resolve' => fn() => true, + ], + [ + 'name' => 'isAdmin', + 'type' => Type::nonNull(Type::boolean()), + 'description' => null, + 'args' => [], + 'resolve' => fn() => true, + ], + [ + 'name' => 'name', + 'type' => Type::nonNull(Type::string()), + 'description' => null, + 'args' => [], + 'resolve' => fn() => true, + ], + [ + 'name' => 'number', + 'type' => Type::nonNull(Type::int()), + 'description' => null, + 'args' => [], + 'resolve' => fn() => true, + ], + [ + 'name' => 'other', + 'type' => Type::nonNull(Type::string()), + 'description' => null, + 'args' => [], + 'resolve' => fn() => true, + ], + ], + 'interfaces' => [ + new InterfaceType([ + 'name' => 'Recipient', + 'description' => null, + 'fields' => [ + [ + 'name' => 'recipientId', + 'type' => Type::nonNull(Type::int()), + 'description' => null, + 'args' => [], + 'resolve' => fn() => true, + ], + ], + 'resolveType' => fn() => true, + ]), + new InterfaceType([ + 'name' => 'User', + 'description' => null, + 'fields' => [ + [ + 'name' => 'userId', + 'type' => Type::nonNull(Type::int()), + 'description' => null, + 'args' => [], + 'resolve' => fn() => true, + ], + ], + 'resolveType' => fn() => true, + ]), + new InterfaceType([ + 'name' => 'Admin', + 'description' => null, + 'fields' => [ + [ + 'name' => 'adminName', + 'type' => Type::nonNull(Type::string()), + 'description' => null, + 'args' => [], + 'resolve' => fn() => true, + ], + [ + 'name' => 'password', + 'type' => Type::nonNull(Type::string()), + 'description' => null, + 'args' => [], + 'resolve' => fn() => true, + ], + [ + 'name' => 'isAdmin', + 'type' => Type::nonNull(Type::boolean()), + 'description' => null, + 'args' => [], + 'resolve' => fn() => true, + ], + [ + 'name' => 'recipientId', + 'type' => Type::nonNull(Type::int()), + 'description' => null, + 'args' => [], + 'resolve' => fn() => true, + ], + ], + 'resolveType' => fn() => true, + ]), + ], + ]), + new ObjectType([ + 'name' => 'Foobar', + 'description' => 'A foobar', + 'fields' => [ + [ + 'name' => 'foobarId', + 'type' => Type::nonNull(Type::string()), + 'description' => 'A foobar ID', + 'args' => [], + 'resolve' => fn() => true, + ], + [ + 'name' => 'status', + 'type' => new EnumType([ + 'name' => 'FoobarStatus', + 'description' => 'Foobar status', + 'values' => [ + 'open' => [ + 'value' => 'open', + 'description' => null, + ], + 'closed' => [ + 'value' => 'closed', + 'description' => 'Foobar status Closed', + 'deprecationReason' => 'Its deprecated', + ], + ], + ]), + 'description' => null, + 'args' => [], + 'resolve' => fn() => true, + ], + [ + 'name' => 'date', + 'type' => new CustomScalarType([ + 'name' => 'DateTime', + 'serialize' => fn() => true, + 'parseValue' => fn() => true, + 'parseLiteral' => fn() => true, + 'description' => 'Date and time (ISO-8601)', + ]), + 'description' => 'A foobar date', + 'args' => [ + [ + 'name' => 'limiting', + 'type' => Type::nonNull(Type::string()), + 'description' => null, + ], + [ + 'name' => 'value', + 'type' => Type::int(), + 'description' => 'The value', + ], + ], + 'resolve' => fn() => true, + ], + [ + 'name' => 'users', + 'type' => new ObjectType([ + 'name' => 'AgentConnection', + 'fields' => [ + [ + 'name' => 'edges', + 'type' => Type::nonNull(Type::listOf(Type::nonNull(new ObjectType([ + 'name' => 'AgentEdge', + 'fields' => [ + [ + 'name' => 'node', + 'type' => Type::nonNull(new ObjectType([ + 'name' => 'Agent', + 'description' => null, + 'fields' => [ + [ + 'name' => 'recipientId', + 'type' => Type::nonNull(Type::int()), + 'description' => null, + 'args' => [], + 'resolve' => fn() => true, + ], + [ + 'name' => 'userId', + 'type' => Type::nonNull(Type::int()), + 'description' => null, + 'args' => [], + 'resolve' => fn() => true, + ], + [ + 'name' => 'adminName', + 'type' => Type::nonNull(Type::string()), + 'description' => null, + 'args' => [], + 'resolve' => fn() => true, + ], + [ + 'name' => 'password', + 'type' => Type::nonNull(Type::string()), + 'description' => null, + 'args' => [], + 'resolve' => fn() => true, + ], + [ + 'name' => 'isAdmin', + 'type' => Type::nonNull(Type::boolean()), + 'description' => null, + 'args' => [], + 'resolve' => fn() => true, + ], + [ + 'name' => 'name', + 'type' => Type::nonNull(Type::string()), + 'description' => null, + 'args' => [], + 'resolve' => fn() => true, + ], + [ + 'name' => 'number', + 'type' => Type::nonNull(Type::int()), + 'description' => null, + 'args' => [], + 'resolve' => fn() => true, + ], + [ + 'name' => 'other', + 'type' => Type::nonNull(Type::string()), + 'description' => null, + 'args' => [], + 'resolve' => fn() => true, + ], + ], + 'interfaces' => [ + new InterfaceType([ + 'name' => 'Recipient', + 'description' => null, + 'fields' => [ + [ + 'name' => 'recipientId', + 'type' => Type::nonNull(Type::int()), + 'description' => null, + 'args' => [], + 'resolve' => fn() => true, + ], + ], + 'resolveType' => fn() => true, + ]), + new InterfaceType([ + 'name' => 'User', + 'description' => null, + 'fields' => [ + [ + 'name' => 'userId', + 'type' => Type::nonNull(Type::int()), + 'description' => null, + 'args' => [], + 'resolve' => fn() => true, + ], + ], + 'resolveType' => fn() => true, + ]), + new InterfaceType([ + 'name' => 'Admin', + 'description' => null, + 'fields' => [ + [ + 'name' => 'adminName', + 'type' => Type::nonNull(Type::string()), + 'description' => null, + 'args' => [], + 'resolve' => fn() => true, + ], + [ + 'name' => 'password', + 'type' => Type::nonNull(Type::string()), + 'description' => null, + 'args' => [], + 'resolve' => fn() => true, + ], + [ + 'name' => 'isAdmin', + 'type' => Type::nonNull(Type::boolean()), + 'description' => null, + 'args' => [], + 'resolve' => fn() => true, + ], + [ + 'name' => 'recipientId', + 'type' => Type::nonNull(Type::int()), + 'description' => null, + 'args' => [], + 'resolve' => fn() => true, + ], + ], + 'resolveType' => fn() => true, + ]), + ], + ])), + 'resolve' => fn() => true, + ], + [ + 'name' => 'cursor', + 'type' => Type::string(), + 'resolve' => fn() => true, + ], + ], + ])))), + 'resolve' => fn() => true, + ], + [ + 'name' => 'pageInfo', + 'type' => Type::nonNull(new ObjectType([ + 'name' => 'PageInfo', + 'fields' => [ + [ + 'name' => 'hasPreviousPage', + 'type' => Type::nonNull(Type::boolean()), + ], + [ + 'name' => 'hasNextPage', + 'type' => Type::nonNull(Type::boolean()), + ], + [ + 'name' => 'startCursor', + 'type' => Type::string(), + ], + [ + 'name' => 'endCursor', + 'type' => Type::string(), + ], + ], + ])), + 'resolve' => fn() => true, + ], + ], + ]), + 'description' => null, + 'args' => [ + [ + 'name' => 'status', + 'type' => Type::string(), + 'description' => null, + ], + [ + 'name' => 'first', + 'type' => Type::int(), + 'description' => 'Connection: return the first # items', + 'defaultValue' => 10, + ], + [ + 'name' => 'after', + 'type' => Type::string(), + 'description' => 'Connection: return items after cursor', + ], + [ + 'name' => 'last', + 'type' => Type::int(), + 'description' => 'Connection: return the last # items', + ], + [ + 'name' => 'before', + 'type' => Type::string(), + 'description' => 'Connection: return items before cursor', + ], + ], + 'resolve' => fn() => true, + ], + [ + 'name' => 'usersList', + 'type' => Type::listOf(Type::nonNull(new ObjectType([ + 'name' => 'Agent', + 'description' => null, + 'fields' => [ + [ + 'name' => 'recipientId', + 'type' => Type::nonNull(Type::int()), + 'description' => null, + 'args' => [], + 'resolve' => fn() => true, + ], + [ + 'name' => 'userId', + 'type' => Type::nonNull(Type::int()), + 'description' => null, + 'args' => [], + 'resolve' => fn() => true, + ], + [ + 'name' => 'adminName', + 'type' => Type::nonNull(Type::string()), + 'description' => null, + 'args' => [], + 'resolve' => fn() => true, + ], + [ + 'name' => 'password', + 'type' => Type::nonNull(Type::string()), + 'description' => null, + 'args' => [], + 'resolve' => fn() => true, + ], + [ + 'name' => 'isAdmin', + 'type' => Type::nonNull(Type::boolean()), + 'description' => null, + 'args' => [], + 'resolve' => fn() => true, + ], + [ + 'name' => 'name', + 'type' => Type::nonNull(Type::string()), + 'description' => null, + 'args' => [], + 'resolve' => fn() => true, + ], + [ + 'name' => 'number', + 'type' => Type::nonNull(Type::int()), + 'description' => null, + 'args' => [], + 'resolve' => fn() => true, + ], + [ + 'name' => 'other', + 'type' => Type::nonNull(Type::string()), + 'description' => null, + 'args' => [], + 'resolve' => fn() => true, + ], + ], + 'interfaces' => [ + new InterfaceType([ + 'name' => 'Recipient', + 'description' => null, + 'fields' => [ + [ + 'name' => 'recipientId', + 'type' => Type::nonNull(Type::int()), + 'description' => null, + 'args' => [], + 'resolve' => fn() => true, + ], + ], + 'resolveType' => fn() => true, + ]), + new InterfaceType([ + 'name' => 'User', + 'description' => null, + 'fields' => [ + [ + 'name' => 'userId', + 'type' => Type::nonNull(Type::int()), + 'description' => null, + 'args' => [], + 'resolve' => fn() => true, + ], + ], + 'resolveType' => fn() => true, + ]), + new InterfaceType([ + 'name' => 'Admin', + 'description' => null, + 'fields' => [ + [ + 'name' => 'adminName', + 'type' => Type::nonNull(Type::string()), + 'description' => null, + 'args' => [], + 'resolve' => fn() => true, + ], + [ + 'name' => 'password', + 'type' => Type::nonNull(Type::string()), + 'description' => null, + 'args' => [], + 'resolve' => fn() => true, + ], + [ + 'name' => 'isAdmin', + 'type' => Type::nonNull(Type::boolean()), + 'description' => null, + 'args' => [], + 'resolve' => fn() => true, + ], + [ + 'name' => 'recipientId', + 'type' => Type::nonNull(Type::int()), + 'description' => null, + 'args' => [], + 'resolve' => fn() => true, + ], + ], + 'resolveType' => fn() => true, + ]), + ], + ]))), + 'description' => null, + 'args' => [], + 'resolve' => fn() => true, + ], + ], + ]), + ], + 'resolveType' => fn() => true, + ])), + 'description' => null, + 'args' => [], + 'resolve' => fn() => true, + ]), ], $queries); $mutation = $schema->getConfig()->getMutation(); diff --git a/tests/Util/Finder/Native/NativeFinderTest.php b/tests/Util/Finder/Native/NativeFinderTest.php index 4070209..96af7c1 100644 --- a/tests/Util/Finder/Native/NativeFinderTest.php +++ b/tests/Util/Finder/Native/NativeFinderTest.php @@ -45,6 +45,7 @@ public function itShouldRetrieveFilesFromFolder(): void 'Doubles/FullFeatured/Query/WithInterfaceOutputQuery.php', 'Doubles/FullFeatured/Query/WithListOutputQuery.php', 'Doubles/FullFeatured/Query/WithOverwrittenTypeQuery.php', + 'Doubles/FullFeatured/Query/WithUnionOutputQuery.php', 'Doubles/FullFeatured/Type/AbstractAdminType.php', 'Doubles/FullFeatured/Type/AgentType.php', 'Doubles/FullFeatured/Type/FoobarStatusType.php',