diff --git a/composer.json b/composer.json index 41292f3bb2..b543066f51 100644 --- a/composer.json +++ b/composer.json @@ -13,8 +13,8 @@ "composer-runtime-api": "^2.0", "php": ">=8.2", "ext-json": "*", - "phpdocumentor/reflection-docblock": "^5.4", - "phpdocumentor/type-resolver": "^1.7", + "phpdocumentor/reflection-docblock": "^5.4 || ^6.0", + "phpdocumentor/type-resolver": "^1.7 || ^2.0", "psr/container": "^1.1 || ^2", "psr/http-factory": "^1", "psr/http-message": "^1.0.1 || ^2.0", diff --git a/phpstan.neon b/phpstan.neon index 11a355ed6b..cc87c35ff7 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -8,6 +8,17 @@ parameters: reportUnmatchedIgnoredErrors: false treatPhpDocTypesAsCertain: false ignoreErrors: + # Collection exists in phpdocumentor/type-resolver 1 only, Generic in phpdocumentor/type-resolver 2 only + - + message: '#phpDocumentor\\Reflection\\(Types\\Collection|PseudoTypes\\Generic)#' + identifier: class.notFound + path: src/Mappers/Parameters/TypeHandler.php + - + message: '#phpDocumentor\\Reflection\\PseudoTypes\\Generic#' + identifier: class.notFound + paths: + - src/Mappers/Root/BaseTypeMapper.php + - src/Mappers/Root/IteratorTypeMapper.php - "#PHPDoc tag \\@throws with type Psr\\\\Container\\\\ContainerExceptionInterface is not subtype of Throwable#" - "#Parameter .* of class ReflectionMethod constructor expects string(\\|null)?, object\\|string given.#" - "#PHPDoc tag @throws with type Psr\\\\SimpleCache\\\\InvalidArgumentException is not subtype of Throwable#" diff --git a/src/Mappers/Parameters/TypeHandler.php b/src/Mappers/Parameters/TypeHandler.php index 5664669657..2d7b8d5286 100644 --- a/src/Mappers/Parameters/TypeHandler.php +++ b/src/Mappers/Parameters/TypeHandler.php @@ -14,6 +14,7 @@ use phpDocumentor\Reflection\DocBlock\Tags\Return_; use phpDocumentor\Reflection\DocBlock\Tags\Var_; use phpDocumentor\Reflection\Fqsen; +use phpDocumentor\Reflection\PseudoTypes\Generic; use phpDocumentor\Reflection\Type; use phpDocumentor\Reflection\TypeResolver as PhpDocumentorTypeResolver; use phpDocumentor\Reflection\Types\Array_; @@ -453,7 +454,8 @@ private function mapType( // Try to match generic phpdoc-provided iterables with non-generic return-type-provided iterables // Example: (return type `\ArrayObject`, phpdoc `\ArrayObject`) || ($innerType instanceof Object_ - && $docBlockType instanceof Collection + // `Collection` with phpdocumentor/type-resolver 1, `Generic` with phpdocumentor/type-resolver 2 + && ($docBlockType instanceof Collection || $docBlockType instanceof Generic) && (string) $innerType->getFqsen() === (string) $docBlockType->getFqsen() ) ) { diff --git a/src/Mappers/Root/BaseTypeMapper.php b/src/Mappers/Root/BaseTypeMapper.php index a474e45c41..11c8ec281a 100644 --- a/src/Mappers/Root/BaseTypeMapper.php +++ b/src/Mappers/Root/BaseTypeMapper.php @@ -19,6 +19,7 @@ use GraphQL\Type\Definition\Type as GraphQLType; use GraphQL\Upload\UploadType; use phpDocumentor\Reflection\DocBlock; +use phpDocumentor\Reflection\PseudoTypes\Generic; use phpDocumentor\Reflection\Type; use phpDocumentor\Reflection\Types\AbstractList; use phpDocumentor\Reflection\Types\Array_; @@ -36,6 +37,7 @@ use TheCodingMachine\GraphQLite\Types\DateTimeType; use TheCodingMachine\GraphQLite\Types\ID; +use function end; use function ltrim; /** @@ -65,6 +67,14 @@ public function toGraphQLOutputType(Type $type, OutputType|null $subType, Reflec return GraphQLType::listOf($innerType); } + // phpdocumentor/type-resolver 2 represents generic iterables like `\ArrayObject` as `Generic` + // (an `Object_`) instead of `Collection` (an `AbstractList`). + if ($type instanceof Generic && $type->getTypes() !== []) { + $genericTypes = $type->getTypes(); + $innerType = $this->topRootTypeMapper->toGraphQLOutputType(end($genericTypes), $subType, $reflector, $docBlockObj); + + return GraphQLType::listOf($innerType); + } if ($type instanceof Object_) { $className = ltrim((string) $type->getFqsen(), '\\'); @@ -92,7 +102,7 @@ public function toGraphQLInputType(Type $type, InputType|null $subType, string $ return GraphQLType::listOf($innerType); } - if ($type instanceof Object_) { + if ($type instanceof Object_ && ! $type instanceof Generic) { $className = ltrim((string) $type->getFqsen(), '\\'); return $this->recursiveTypeMapper->mapClassToInputType($className); diff --git a/src/Mappers/Root/IteratorTypeMapper.php b/src/Mappers/Root/IteratorTypeMapper.php index 3d51c54a98..5a0fba0a22 100644 --- a/src/Mappers/Root/IteratorTypeMapper.php +++ b/src/Mappers/Root/IteratorTypeMapper.php @@ -14,6 +14,7 @@ use Iterator; use IteratorAggregate; use phpDocumentor\Reflection\DocBlock; +use phpDocumentor\Reflection\PseudoTypes\Generic; use phpDocumentor\Reflection\Type; use phpDocumentor\Reflection\Types\Array_; use phpDocumentor\Reflection\Types\Compound; @@ -44,7 +45,8 @@ public function toGraphQLOutputType(Type $type, OutputType|null $subType, Reflec try { return $this->next->toGraphQLOutputType($type, $subType, $reflector, $docBlockObj); } catch (CannotMapTypeException $e) { - if ($type instanceof Object_) { + // A `Generic` like `\ArrayObject` already provides its item type + if ($type instanceof Object_ && ! $type instanceof Generic) { /** @var class-string $fqcn */ $fqcn = (string) $type->getFqsen(); $refClass = new ReflectionClass($fqcn); @@ -207,7 +209,7 @@ private function toGraphQLType(Compound $type, Closure $topToGraphQLType, bool $ private function splitIteratorFromOtherTypes(array &$types): Type|null { foreach ($types as $key => $singleDocBlockType) { - if (! ($singleDocBlockType instanceof Object_)) { + if (! ($singleDocBlockType instanceof Object_) || $singleDocBlockType instanceof Generic) { continue; } diff --git a/tests/Mappers/Root/IteratorTypeMapperTest.php b/tests/Mappers/Root/IteratorTypeMapperTest.php index 684e836b73..933443353c 100644 --- a/tests/Mappers/Root/IteratorTypeMapperTest.php +++ b/tests/Mappers/Root/IteratorTypeMapperTest.php @@ -6,9 +6,11 @@ use GraphQL\Type\Definition\ListOfType; use GraphQL\Type\Definition\NonNull; use phpDocumentor\Reflection\DocBlock; +use PHPUnit\Framework\Attributes\DataProvider; use ReflectionMethod; use TheCodingMachine\GraphQLite\AbstractQueryProvider; use TheCodingMachine\GraphQLite\Fixtures\TestObject; +use TheCodingMachine\GraphQLite\Fixtures\TestObject2; use TheCodingMachine\GraphQLite\Mappers\CannotMapTypeException; class IteratorTypeMapperTest extends AbstractQueryProvider @@ -50,4 +52,35 @@ public function testIterableWithTwoArrays(): void $this->expectExceptionMessage('"\ArrayObject" is iterable. Please provide a more specific type. For instance: \ArrayObject|User[].'); $result = $typeMapper->toGraphQLOutputType(self::resolveType('ArrayObject|array|array'), null, new ReflectionMethod(__CLASS__, 'testInputIterator'), new DocBlock()); } + + #[DataProvider('unmappableGenericIterablesProvider')] + public function testUnmappableGenericIterable(string $phpdocType, string $expectedMessage): void + { + $typeMapper = $this->getRootTypeMapper(); + + $this->expectException(CannotMapTypeException::class); + $this->expectExceptionMessage($expectedMessage); + $typeMapper->toGraphQLOutputType(self::resolveType($phpdocType), null, new ReflectionMethod(__CLASS__, 'testUnmappableGenericIterable'), new DocBlock()); + } + + public static function unmappableGenericIterablesProvider(): iterable + { + // A generic iterable already defines its item type, so other union members are not used as its item type + yield 'generic iterable or bool' => [ + \sprintf('\ArrayObject|bool', TestObject::class), + 'in GraphQL, you can only use union types between objects. These types cannot be used in union types: [TestObject!]!, Boolean!', + ]; + yield 'generic iterable or object' => [ + \sprintf('\ArrayObject|%s', TestObject::class, TestObject2::class), + 'in GraphQL, you can only use union types between objects. These types cannot be used in union types: [TestObject!]!, TestObject2!', + ]; + yield 'generic iterable or array' => [ + \sprintf('\ArrayObject|%s[]', TestObject::class, TestObject2::class), + 'in GraphQL, you can only use union types between objects. These types cannot be used in union types: [TestObject!]!, [TestObject2!]!', + ]; + yield 'generic iterable with unmappable item type' => [ + \sprintf('\ArrayObject', TestObject::class, TestObject2::class), + \sprintf("don't know how to handle type \\%s&\\%s", TestObject::class, TestObject2::class), + ]; + } } diff --git a/website/docs/CHANGELOG.md b/website/docs/CHANGELOG.md index 81be29693c..70011c5fd5 100644 --- a/website/docs/CHANGELOG.md +++ b/website/docs/CHANGELOG.md @@ -54,6 +54,9 @@ sidebar_label: Changelog `withAddedDescriptionItem()`. `QueryFieldDescriptor::withAddedDescriptionLines()` is deprecated in favor of it. ([#838](https://github.com/thecodingmachine/graphqlite/pull/838)) @oojacoboo +- Supports `phpdocumentor/reflection-docblock` 6 and `phpdocumentor/type-resolver` 2 alongside 5 and 1, so GraphQLite + installs next to packages that already require them. Generic iterables such as `\ArrayObject` map to + lists with both type-resolver versions. @timtheisinger ### Bug Fixes