Skip to content

Commit 9e3b787

Browse files
authored
Support generics typed iterables for output types (#468)
* Draft implementation for generic iterables #436 * Draft v2 implementation for generic iterables (separated mapper) #436 * Draft v3 implementation for generic iterables (base type mapper) #436 * Add iterable generic test case for integration tests #436 * Add a bit of logic for merging types logic to better support generic types #436 * Add iterable generic test case for integration tests #436 * Move logic for matching iterable types logic to another place (more appropriate) #436 * Revert own styling autoformatting changes * Allow to use object type for cannot map type exception * Add more tests for generic iterables mapping by base type mapper
1 parent 53d0018 commit 9e3b787

File tree

9 files changed

+133
-15
lines changed

9 files changed

+133
-15
lines changed

src/Mappers/CannotMapTypeException.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use phpDocumentor\Reflection\Types\Array_;
1616
use phpDocumentor\Reflection\Types\Iterable_;
1717
use phpDocumentor\Reflection\Types\Mixed_;
18+
use phpDocumentor\Reflection\Types\Object_;
1819
use ReflectionMethod;
1920
use ReflectionProperty;
2021
use TheCodingMachine\GraphQLite\Annotations\ExtendType;
@@ -132,7 +133,7 @@ public static function extendTypeWithBadTargetedClass(string $className, ExtendT
132133
}
133134

134135
/**
135-
* @param Array_|Iterable_|Mixed_ $type
136+
* @param Array_|Iterable_|Object_|Mixed_ $type
136137
* @param ReflectionMethod|ReflectionProperty $reflector
137138
*/
138139
public static function createForMissingPhpDoc(PhpDocumentorType $type, $reflector, ?string $argumentName = null): self
@@ -142,6 +143,8 @@ public static function createForMissingPhpDoc(PhpDocumentorType $type, $reflecto
142143
$typeStr = 'array';
143144
} elseif ($type instanceof Iterable_) {
144145
$typeStr = 'iterable';
146+
} elseif ($type instanceof Object_) {
147+
$typeStr = \sprintf('object ("%s")', $type->getFqsen());
145148
} elseif ($type instanceof Mixed_) {
146149
$typeStr = 'mixed';
147150
}

src/Mappers/Parameters/TypeHandler.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use phpDocumentor\Reflection\Type;
1515
use phpDocumentor\Reflection\TypeResolver as PhpDocumentorTypeResolver;
1616
use phpDocumentor\Reflection\Types\Array_;
17+
use phpDocumentor\Reflection\Types\Collection;
1718
use phpDocumentor\Reflection\Types\Compound;
1819
use phpDocumentor\Reflection\Types\Iterable_;
1920
use phpDocumentor\Reflection\Types\Mixed_;
@@ -338,7 +339,17 @@ private function mapType(
338339
}
339340
$innerType = $type instanceof Nullable ? $type->getActualType() : $type;
340341

341-
if ($innerType instanceof Array_ || $innerType instanceof Iterable_ || $innerType instanceof Mixed_) {
342+
if (
343+
$innerType instanceof Array_
344+
|| $innerType instanceof Iterable_
345+
|| $innerType instanceof Mixed_
346+
// Try to match generic phpdoc-provided iterables with non-generic return-type-provided iterables
347+
// Example: (return type `\ArrayObject`, phpdoc `\ArrayObject<string, TestObject>`)
348+
|| ($innerType instanceof Object_
349+
&& $docBlockType instanceof Collection
350+
&& (string)$innerType->getFqsen() === (string)$docBlockType->getFqsen()
351+
)
352+
) {
342353
// We need to use the docBlockType
343354
if ($docBlockType === null) {
344355
throw CannotMapTypeException::createForMissingPhpDoc($innerType, $reflector, $argumentName);

src/Mappers/Root/BaseTypeMapper.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use GraphQL\Upload\UploadType;
1818
use phpDocumentor\Reflection\DocBlock;
1919
use phpDocumentor\Reflection\Type;
20+
use phpDocumentor\Reflection\Types\AbstractList;
2021
use phpDocumentor\Reflection\Types\Array_;
2122
use phpDocumentor\Reflection\Types\Boolean;
2223
use phpDocumentor\Reflection\Types\Float_;
@@ -70,7 +71,7 @@ public function toGraphQLOutputType(Type $type, ?OutputType $subType, $reflector
7071
return $mappedType;
7172
}
7273

73-
if ($type instanceof Array_) {
74+
if ($type instanceof AbstractList) {
7475
$innerType = $this->topRootTypeMapper->toGraphQLOutputType($type->getValueType(), $subType, $reflector, $docBlockObj);
7576
/*if ($innerType === null) {
7677
return $this->next->toGraphQLOutputType($type, $subType, $reflector, $docBlockObj);

src/Mappers/Root/IteratorTypeMapper.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ public function toGraphQLInputType(Type $type, ?InputType $subType, string $argu
9696
{
9797
if (! $type instanceof Compound) {
9898
//try {
99-
return $this->next->toGraphQLInputType($type, $subType, $argumentName, $reflector, $docBlockObj);
99+
return $this->next->toGraphQLInputType($type, $subType, $argumentName, $reflector, $docBlockObj);
100100

101101
/*} catch (CannotMapTypeException $e) {
102102
$this->throwIterableMissingTypeHintException($e, $type);

tests/AggregateControllerQueryProviderTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public function has($id)
4343
$aggregateQueryProvider = new AggregateControllerQueryProvider([ 'controller' ], $this->getFieldsBuilder(), $container);
4444

4545
$queries = $aggregateQueryProvider->getQueries();
46-
$this->assertCount(7, $queries);
46+
$this->assertCount(9, $queries);
4747

4848
$mutations = $aggregateQueryProvider->getMutations();
4949
$this->assertCount(1, $mutations);

tests/FieldsBuilderTest.php

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ public function testQueryProvider(): void
8080

8181
$queries = $queryProvider->getQueries($controller);
8282

83-
$this->assertCount(7, $queries);
83+
$this->assertCount(9, $queries);
8484
$usersQuery = $queries['test'];
8585
$this->assertSame('test', $usersQuery->name);
8686

@@ -201,7 +201,7 @@ public function testQueryProviderWithFixedReturnType(): void
201201

202202
$queries = $queryProvider->getQueries($controller);
203203

204-
$this->assertCount(7, $queries);
204+
$this->assertCount(9, $queries);
205205
$fixedQuery = $queries['testFixReturnType'];
206206

207207
$this->assertInstanceOf(IDType::class, $fixedQuery->getType());
@@ -215,7 +215,7 @@ public function testQueryProviderWithComplexFixedReturnType(): void
215215

216216
$queries = $queryProvider->getQueries($controller);
217217

218-
$this->assertCount(7, $queries);
218+
$this->assertCount(9, $queries);
219219
$fixedQuery = $queries['testFixComplexReturnType'];
220220

221221
$this->assertInstanceOf(NonNull::class, $fixedQuery->getType());
@@ -406,7 +406,7 @@ public function testQueryProviderWithIterableClass(): void
406406

407407
$queries = $queryProvider->getQueries($controller);
408408

409-
$this->assertCount(7, $queries);
409+
$this->assertCount(9, $queries);
410410
$iterableQuery = $queries['arrayObject'];
411411

412412
$this->assertSame('arrayObject', $iterableQuery->name);
@@ -417,13 +417,32 @@ public function testQueryProviderWithIterableClass(): void
417417
$this->assertSame('TestObject', $iterableQuery->getType()->getWrappedType()->getWrappedType()->getWrappedType()->name);
418418
}
419419

420+
public function testQueryProviderWithIterableGenericClass(): void
421+
{
422+
$controller = new TestController();
423+
424+
$queryProvider = $this->buildFieldsBuilder();
425+
426+
$queries = $queryProvider->getQueries($controller);
427+
428+
$this->assertCount(9, $queries);
429+
$iterableQuery = $queries['arrayObjectGeneric'];
430+
431+
$this->assertSame('arrayObjectGeneric', $iterableQuery->name);
432+
$this->assertInstanceOf(NonNull::class, $iterableQuery->getType());
433+
$this->assertInstanceOf(ListOfType::class, $iterableQuery->getType()->getWrappedType());
434+
$this->assertInstanceOf(NonNull::class, $iterableQuery->getType()->getWrappedType()->getWrappedType());
435+
$this->assertInstanceOf(ObjectType::class, $iterableQuery->getType()->getWrappedType()->getWrappedType()->getWrappedType());
436+
$this->assertSame('TestObject', $iterableQuery->getType()->getWrappedType()->getWrappedType()->getWrappedType()->name);
437+
}
438+
420439
public function testQueryProviderWithIterable(): void
421440
{
422441
$queryProvider = $this->buildFieldsBuilder();
423442

424443
$queries = $queryProvider->getQueries(new TestController());
425444

426-
$this->assertCount(7, $queries);
445+
$this->assertCount(9, $queries);
427446
$iterableQuery = $queries['iterable'];
428447

429448
$this->assertSame('iterable', $iterableQuery->name);
@@ -434,6 +453,23 @@ public function testQueryProviderWithIterable(): void
434453
$this->assertSame('TestObject', $iterableQuery->getType()->getWrappedType()->getWrappedType()->getWrappedType()->name);
435454
}
436455

456+
public function testQueryProviderWithIterableGeneric(): void
457+
{
458+
$queryProvider = $this->buildFieldsBuilder();
459+
460+
$queries = $queryProvider->getQueries(new TestController());
461+
462+
$this->assertCount(9, $queries);
463+
$iterableQuery = $queries['iterableGeneric'];
464+
465+
$this->assertSame('iterableGeneric', $iterableQuery->name);
466+
$this->assertInstanceOf(NonNull::class, $iterableQuery->getType());
467+
$this->assertInstanceOf(ListOfType::class, $iterableQuery->getType()->getWrappedType());
468+
$this->assertInstanceOf(NonNull::class, $iterableQuery->getType()->getWrappedType()->getWrappedType());
469+
$this->assertInstanceOf(ObjectType::class, $iterableQuery->getType()->getWrappedType()->getWrappedType()->getWrappedType());
470+
$this->assertSame('TestObject', $iterableQuery->getType()->getWrappedType()->getWrappedType()->getWrappedType()->name);
471+
}
472+
437473
public function testNoReturnTypeError(): void
438474
{
439475
$queryProvider = $this->buildFieldsBuilder();
@@ -450,7 +486,7 @@ public function testQueryProviderWithUnion(): void
450486

451487
$queries = $queryProvider->getQueries($controller);
452488

453-
$this->assertCount(7, $queries);
489+
$this->assertCount(9, $queries);
454490
$unionQuery = $queries['union'];
455491

456492
$this->assertInstanceOf(NonNull::class, $unionQuery->getType());
@@ -618,7 +654,7 @@ public function testMissingArgument(): void
618654

619655
$queries = $queryProvider->getQueries($controller);
620656

621-
$this->assertCount(7, $queries);
657+
$this->assertCount(9, $queries);
622658
$usersQuery = $queries['test'];
623659
$context = [];
624660

tests/Fixtures/TestController.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,15 @@ public function testArrayObject(): ArrayObject
9494
return new ArrayObject([]);
9595
}
9696

97+
/**
98+
* @Query(name="arrayObjectGeneric")
99+
* @return ArrayObject<TestObject>
100+
*/
101+
public function testArrayObjectGeneric(): ArrayObject
102+
{
103+
return new ArrayObject([]);
104+
}
105+
97106
/**
98107
* @Query(name="iterable")
99108
* @return iterable|TestObject[]
@@ -103,6 +112,15 @@ public function testIterable(): iterable
103112
return array();
104113
}
105114

115+
/**
116+
* @Query(name="iterableGeneric")
117+
* @return iterable<TestObject>
118+
*/
119+
public function testIterableGeneric(): iterable
120+
{
121+
return array();
122+
}
123+
106124
/**
107125
* @Query(name="union")
108126
* @return TestObject|TestObject2

tests/GlobControllerQueryProviderTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public function has($id)
3939
$globControllerQueryProvider = new GlobControllerQueryProvider('TheCodingMachine\\GraphQLite\\Fixtures', $this->getFieldsBuilder(), $container, $this->getAnnotationReader(), new Psr16Cache(new NullAdapter()), null, false, false);
4040

4141
$queries = $globControllerQueryProvider->getQueries();
42-
$this->assertCount(7, $queries);
42+
$this->assertCount(9, $queries);
4343

4444
$mutations = $globControllerQueryProvider->getMutations();
4545
$this->assertCount(1, $mutations);

tests/Mappers/Root/BaseTypeMapperTest.php

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
namespace TheCodingMachine\GraphQLite\Mappers\Root;
44

5+
use GraphQL\Type\Definition\BooleanType;
6+
use GraphQL\Type\Definition\IntType;
7+
use GraphQL\Type\Definition\ListOfType;
8+
use GraphQL\Type\Definition\NonNull;
9+
use GraphQL\Type\Definition\WrappingType;
510
use phpDocumentor\Reflection\DocBlock;
611
use phpDocumentor\Reflection\Fqsen;
712
use phpDocumentor\Reflection\Types\Array_;
@@ -10,12 +15,12 @@
1015
use phpDocumentor\Reflection\Types\Resource_;
1116
use ReflectionMethod;
1217
use TheCodingMachine\GraphQLite\AbstractQueryProviderTest;
13-
use TheCodingMachine\GraphQLite\GraphQLRuntimeException;
18+
use TheCodingMachine\GraphQLite\Fixtures\TestObject;
1419
use TheCodingMachine\GraphQLite\Mappers\CannotMapTypeException;
20+
use TheCodingMachine\GraphQLite\Types\MutableObjectType;
1521

1622
class BaseTypeMapperTest extends AbstractQueryProviderTest
1723
{
18-
1924
public function testNullableToGraphQLInputType(): void
2025
{
2126
$baseTypeMapper = new BaseTypeMapper(new FinalRootTypeMapper($this->getTypeMapper()), $this->getTypeMapper(), $this->getRootTypeMapper());
@@ -51,4 +56,48 @@ public function testUnmappableInputArray(): void
5156
$this->expectExceptionMessage("don't know how to handle type resource");
5257
$mappedType = $baseTypeMapper->toGraphQLInputType(new Array_(new Resource_()), null, 'foo', new ReflectionMethod(BaseTypeMapper::class, '__construct'), new DocBlock());
5358
}
59+
60+
/**
61+
* @param string $phpdocType
62+
* @param class-string $expectedItemType
63+
*
64+
* @return void
65+
*
66+
* @dataProvider genericIterablesProvider
67+
*/
68+
public function testOutputGenericIterables(string $phpdocType, string $expectedItemType, ?string $expectedWrappedItemType = null): void
69+
{
70+
$typeMapper = $this->getRootTypeMapper();
71+
72+
$result = $typeMapper->toGraphQLOutputType($this->resolveType($phpdocType), null, new ReflectionMethod(__CLASS__, 'testOutputGenericIterables'), new DocBlock());
73+
74+
$this->assertInstanceOf(NonNull::class, $result);
75+
$this->assertInstanceOf(ListOfType::class, $result->getWrappedType());
76+
$itemType = $result->getWrappedType()->getWrappedType();
77+
$this->assertInstanceOf($expectedItemType, $itemType);
78+
if (null !== $expectedWrappedItemType) {
79+
$this->assertInstanceOf(WrappingType::class, $itemType);
80+
$this->assertInstanceOf($expectedWrappedItemType, $itemType->getWrappedType());
81+
}
82+
}
83+
84+
public function genericIterablesProvider(): iterable
85+
{
86+
yield '\ArrayIterator with nullable int item' => ['\ArrayIterator<?int>', IntType::class];
87+
yield '\ArrayIterator with int item' => ['\ArrayIterator<int>', NonNull::class, IntType::class];
88+
89+
// key information cannot be presented in GQL types for now
90+
yield 'iterable with provided int key and test object item' => [
91+
\sprintf('iterable<%s>', TestObject::class),
92+
NonNull::class,
93+
MutableObjectType::class,
94+
];
95+
yield '\Iterator with provided string key and int item' => ['\Iterator<string, int>', NonNull::class, IntType::class];
96+
yield '\IteratorAggregate with provided int key and bool item' => ['\IteratorAggregate<int, bool>', NonNull::class, BooleanType::class];
97+
yield '\Traversable with provided string key and test object item' => [
98+
\sprintf('\Traversable<string, %s>', TestObject::class),
99+
NonNull::class,
100+
MutableObjectType::class,
101+
];
102+
}
54103
}

0 commit comments

Comments
 (0)