Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support generics typed iterables for output types #468

Merged
5 changes: 4 additions & 1 deletion src/Mappers/CannotMapTypeException.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use phpDocumentor\Reflection\Types\Array_;
use phpDocumentor\Reflection\Types\Iterable_;
use phpDocumentor\Reflection\Types\Mixed_;
use phpDocumentor\Reflection\Types\Object_;
use ReflectionMethod;
use ReflectionProperty;
use TheCodingMachine\GraphQLite\Annotations\ExtendType;
Expand Down Expand Up @@ -132,7 +133,7 @@ public static function extendTypeWithBadTargetedClass(string $className, ExtendT
}

/**
* @param Array_|Iterable_|Mixed_ $type
* @param Array_|Iterable_|Object_|Mixed_ $type
* @param ReflectionMethod|ReflectionProperty $reflector
*/
public static function createForMissingPhpDoc(PhpDocumentorType $type, $reflector, ?string $argumentName = null): self
Expand All @@ -142,6 +143,8 @@ public static function createForMissingPhpDoc(PhpDocumentorType $type, $reflecto
$typeStr = 'array';
} elseif ($type instanceof Iterable_) {
$typeStr = 'iterable';
} elseif ($type instanceof Object_) {
$typeStr = \sprintf('object ("%s")', $type->getFqsen());
} elseif ($type instanceof Mixed_) {
$typeStr = 'mixed';
}
Expand Down
13 changes: 12 additions & 1 deletion src/Mappers/Parameters/TypeHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use phpDocumentor\Reflection\Type;
use phpDocumentor\Reflection\TypeResolver as PhpDocumentorTypeResolver;
use phpDocumentor\Reflection\Types\Array_;
use phpDocumentor\Reflection\Types\Collection;
use phpDocumentor\Reflection\Types\Compound;
use phpDocumentor\Reflection\Types\Iterable_;
use phpDocumentor\Reflection\Types\Mixed_;
Expand Down Expand Up @@ -338,7 +339,17 @@ private function mapType(
}
$innerType = $type instanceof Nullable ? $type->getActualType() : $type;

if ($innerType instanceof Array_ || $innerType instanceof Iterable_ || $innerType instanceof Mixed_) {
if (
$innerType instanceof Array_
|| $innerType instanceof Iterable_
|| $innerType instanceof Mixed_
// Try to match generic phpdoc-provided iterables with non-generic return-type-provided iterables
// Example: (return type `\ArrayObject`, phpdoc `\ArrayObject<string, TestObject>`)
|| ($innerType instanceof Object_
&& $docBlockType instanceof Collection
&& (string)$innerType->getFqsen() === (string)$docBlockType->getFqsen()
)
) {
// We need to use the docBlockType
if ($docBlockType === null) {
throw CannotMapTypeException::createForMissingPhpDoc($innerType, $reflector, $argumentName);
andrew-demb marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
3 changes: 2 additions & 1 deletion src/Mappers/Root/BaseTypeMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use GraphQL\Upload\UploadType;
use phpDocumentor\Reflection\DocBlock;
use phpDocumentor\Reflection\Type;
use phpDocumentor\Reflection\Types\AbstractList;
use phpDocumentor\Reflection\Types\Array_;
use phpDocumentor\Reflection\Types\Boolean;
use phpDocumentor\Reflection\Types\Float_;
Expand Down Expand Up @@ -70,7 +71,7 @@ public function toGraphQLOutputType(Type $type, ?OutputType $subType, $reflector
return $mappedType;
}

if ($type instanceof Array_) {
if ($type instanceof AbstractList) {
$innerType = $this->topRootTypeMapper->toGraphQLOutputType($type->getValueType(), $subType, $reflector, $docBlockObj);
/*if ($innerType === null) {
return $this->next->toGraphQLOutputType($type, $subType, $reflector, $docBlockObj);
Expand Down
2 changes: 1 addition & 1 deletion src/Mappers/Root/IteratorTypeMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ public function toGraphQLInputType(Type $type, ?InputType $subType, string $argu
{
if (! $type instanceof Compound) {
//try {
return $this->next->toGraphQLInputType($type, $subType, $argumentName, $reflector, $docBlockObj);
return $this->next->toGraphQLInputType($type, $subType, $argumentName, $reflector, $docBlockObj);

/*} catch (CannotMapTypeException $e) {
$this->throwIterableMissingTypeHintException($e, $type);
Expand Down
2 changes: 1 addition & 1 deletion tests/AggregateControllerQueryProviderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public function has($id)
$aggregateQueryProvider = new AggregateControllerQueryProvider([ 'controller' ], $this->getFieldsBuilder(), $container);

$queries = $aggregateQueryProvider->getQueries();
$this->assertCount(7, $queries);
$this->assertCount(9, $queries);

$mutations = $aggregateQueryProvider->getMutations();
$this->assertCount(1, $mutations);
Expand Down
50 changes: 43 additions & 7 deletions tests/FieldsBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ public function testQueryProvider(): void

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

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

Expand Down Expand Up @@ -201,7 +201,7 @@ public function testQueryProviderWithFixedReturnType(): void

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

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

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

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

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

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

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

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

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

public function testQueryProviderWithIterableGenericClass(): void
{
$controller = new TestController();

$queryProvider = $this->buildFieldsBuilder();

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

$this->assertCount(9, $queries);
$iterableQuery = $queries['arrayObjectGeneric'];

$this->assertSame('arrayObjectGeneric', $iterableQuery->name);
$this->assertInstanceOf(NonNull::class, $iterableQuery->getType());
$this->assertInstanceOf(ListOfType::class, $iterableQuery->getType()->getWrappedType());
$this->assertInstanceOf(NonNull::class, $iterableQuery->getType()->getWrappedType()->getWrappedType());
$this->assertInstanceOf(ObjectType::class, $iterableQuery->getType()->getWrappedType()->getWrappedType()->getWrappedType());
$this->assertSame('TestObject', $iterableQuery->getType()->getWrappedType()->getWrappedType()->getWrappedType()->name);
}

public function testQueryProviderWithIterable(): void
{
$queryProvider = $this->buildFieldsBuilder();

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

$this->assertCount(7, $queries);
$this->assertCount(9, $queries);
$iterableQuery = $queries['iterable'];

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

public function testQueryProviderWithIterableGeneric(): void
{
$queryProvider = $this->buildFieldsBuilder();

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

$this->assertCount(9, $queries);
$iterableQuery = $queries['iterableGeneric'];

$this->assertSame('iterableGeneric', $iterableQuery->name);
$this->assertInstanceOf(NonNull::class, $iterableQuery->getType());
$this->assertInstanceOf(ListOfType::class, $iterableQuery->getType()->getWrappedType());
$this->assertInstanceOf(NonNull::class, $iterableQuery->getType()->getWrappedType()->getWrappedType());
$this->assertInstanceOf(ObjectType::class, $iterableQuery->getType()->getWrappedType()->getWrappedType()->getWrappedType());
$this->assertSame('TestObject', $iterableQuery->getType()->getWrappedType()->getWrappedType()->getWrappedType()->name);
}

public function testNoReturnTypeError(): void
{
$queryProvider = $this->buildFieldsBuilder();
Expand All @@ -450,7 +486,7 @@ public function testQueryProviderWithUnion(): void

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

$this->assertCount(7, $queries);
$this->assertCount(9, $queries);
$unionQuery = $queries['union'];

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

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

$this->assertCount(7, $queries);
$this->assertCount(9, $queries);
$usersQuery = $queries['test'];
$context = [];

Expand Down
18 changes: 18 additions & 0 deletions tests/Fixtures/TestController.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,15 @@ public function testArrayObject(): ArrayObject
return new ArrayObject([]);
}

/**
* @Query(name="arrayObjectGeneric")
* @return ArrayObject<TestObject>
*/
public function testArrayObjectGeneric(): ArrayObject
{
return new ArrayObject([]);
}

/**
* @Query(name="iterable")
* @return iterable|TestObject[]
Expand All @@ -103,6 +112,15 @@ public function testIterable(): iterable
return array();
}

/**
* @Query(name="iterableGeneric")
* @return iterable<TestObject>
*/
public function testIterableGeneric(): iterable
{
return array();
}

/**
* @Query(name="union")
* @return TestObject|TestObject2
Expand Down
2 changes: 1 addition & 1 deletion tests/GlobControllerQueryProviderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public function has($id)
$globControllerQueryProvider = new GlobControllerQueryProvider('TheCodingMachine\\GraphQLite\\Fixtures', $this->getFieldsBuilder(), $container, $this->getAnnotationReader(), new Psr16Cache(new NullAdapter()), null, false, false);

$queries = $globControllerQueryProvider->getQueries();
$this->assertCount(7, $queries);
$this->assertCount(9, $queries);

$mutations = $globControllerQueryProvider->getMutations();
$this->assertCount(1, $mutations);
Expand Down
53 changes: 51 additions & 2 deletions tests/Mappers/Root/BaseTypeMapperTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

namespace TheCodingMachine\GraphQLite\Mappers\Root;

use GraphQL\Type\Definition\BooleanType;
use GraphQL\Type\Definition\IntType;
use GraphQL\Type\Definition\ListOfType;
use GraphQL\Type\Definition\NonNull;
use GraphQL\Type\Definition\WrappingType;
use phpDocumentor\Reflection\DocBlock;
use phpDocumentor\Reflection\Fqsen;
use phpDocumentor\Reflection\Types\Array_;
Expand All @@ -10,12 +15,12 @@
use phpDocumentor\Reflection\Types\Resource_;
use ReflectionMethod;
use TheCodingMachine\GraphQLite\AbstractQueryProviderTest;
use TheCodingMachine\GraphQLite\GraphQLRuntimeException;
use TheCodingMachine\GraphQLite\Fixtures\TestObject;
use TheCodingMachine\GraphQLite\Mappers\CannotMapTypeException;
use TheCodingMachine\GraphQLite\Types\MutableObjectType;

class BaseTypeMapperTest extends AbstractQueryProviderTest
{

public function testNullableToGraphQLInputType(): void
{
$baseTypeMapper = new BaseTypeMapper(new FinalRootTypeMapper($this->getTypeMapper()), $this->getTypeMapper(), $this->getRootTypeMapper());
Expand Down Expand Up @@ -51,4 +56,48 @@ public function testUnmappableInputArray(): void
$this->expectExceptionMessage("don't know how to handle type resource");
$mappedType = $baseTypeMapper->toGraphQLInputType(new Array_(new Resource_()), null, 'foo', new ReflectionMethod(BaseTypeMapper::class, '__construct'), new DocBlock());
}

/**
* @param string $phpdocType
* @param class-string $expectedItemType
*
* @return void
*
* @dataProvider genericIterablesProvider
*/
public function testOutputGenericIterables(string $phpdocType, string $expectedItemType, ?string $expectedWrappedItemType = null): void
{
$typeMapper = $this->getRootTypeMapper();

$result = $typeMapper->toGraphQLOutputType($this->resolveType($phpdocType), null, new ReflectionMethod(__CLASS__, 'testOutputGenericIterables'), new DocBlock());

$this->assertInstanceOf(NonNull::class, $result);
$this->assertInstanceOf(ListOfType::class, $result->getWrappedType());
$itemType = $result->getWrappedType()->getWrappedType();
$this->assertInstanceOf($expectedItemType, $itemType);
if (null !== $expectedWrappedItemType) {
$this->assertInstanceOf(WrappingType::class, $itemType);
$this->assertInstanceOf($expectedWrappedItemType, $itemType->getWrappedType());
}
}

public function genericIterablesProvider(): iterable
{
yield '\ArrayIterator with nullable int item' => ['\ArrayIterator<?int>', IntType::class];
yield '\ArrayIterator with int item' => ['\ArrayIterator<int>', NonNull::class, IntType::class];

// key information cannot be presented in GQL types for now
yield 'iterable with provided int key and test object item' => [
\sprintf('iterable<%s>', TestObject::class),
NonNull::class,
MutableObjectType::class,
];
yield '\Iterator with provided string key and int item' => ['\Iterator<string, int>', NonNull::class, IntType::class];
yield '\IteratorAggregate with provided int key and bool item' => ['\IteratorAggregate<int, bool>', NonNull::class, BooleanType::class];
yield '\Traversable with provided string key and test object item' => [
\sprintf('\Traversable<string, %s>', TestObject::class),
NonNull::class,
MutableObjectType::class,
];
}
}