Skip to content

Commit 1fa0023

Browse files
authored
Merge pull request #48 from moufmouf/complexOutputType
Adding support for complex GraphQL types in "outputType" parameter
2 parents bd09f5a + b4a2837 commit 1fa0023

10 files changed

+162
-18
lines changed

src/FieldsBuilder.php

+10-9
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use phpDocumentor\Reflection\Types\Self_;
1717
use Psr\Http\Message\UploadedFileInterface;
1818
use ReflectionMethod;
19+
use function sprintf;
1920
use TheCodingMachine\GraphQLite\Annotations\Field;
2021
use TheCodingMachine\GraphQLite\Annotations\SourceFieldInterface;
2122
use TheCodingMachine\GraphQLite\Hydrators\HydratorInterface;
@@ -267,9 +268,10 @@ private function getFieldsByAnnotations($controller, string $annotationName, boo
267268
$args = $this->mapParameters($parameters, $docBlockObj);
268269

269270
if ($queryAnnotation->getOutputType()) {
270-
$type = $this->typeResolver->mapNameToType($queryAnnotation->getOutputType());
271-
if (!$type instanceof OutputType) {
272-
throw new \InvalidArgumentException(sprintf("In %s::%s, the 'outputType' parameter in @Type annotation should contain the name of an OutputType. The '%s' type does not implement GraphQL\\Type\\Definition\\OutputType", $refMethod->getDeclaringClass()->getName(), $refMethod->getName(), $queryAnnotation->getOutputType()));
271+
try {
272+
$type = $this->typeResolver->mapNameToOutputType($queryAnnotation->getOutputType());
273+
} catch (CannotMapTypeExceptionInterface $e) {
274+
throw CannotMapTypeException::wrapWithReturnInfo($e, $refMethod);
273275
}
274276
} else {
275277
$type = $this->mapReturnType($refMethod, $docBlockObj);
@@ -403,7 +405,11 @@ private function getQueryFieldsFromSourceFields(array $sourceFields, ReflectionC
403405
$type = GraphQLType::nonNull($type);
404406
}
405407
} elseif ($sourceField->getOutputType()) {
406-
$type = $this->typeResolver->mapNameToType($sourceField->getOutputType());
408+
try {
409+
$type = $this->typeResolver->mapNameToOutputType($sourceField->getOutputType());
410+
} catch (CannotMapTypeExceptionInterface $e) {
411+
throw CannotMapTypeException::wrapWithSourceField($e, $refClass, $sourceField);
412+
}
407413
} else {
408414
$type = $this->mapReturnType($refMethod, $docBlockObj);
409415
}
@@ -524,11 +530,6 @@ private function mapParameters(array $refParameters, DocBlock $docBlock): array
524530
return $args;
525531
}
526532

527-
/**
528-
* @param Type $type
529-
* @param Type|null $docBlockType
530-
* @return GraphQLType
531-
*/
532533
private function mapType(Type $type, ?Type $docBlockType, bool $isNullable, bool $mapToInputType): GraphQLType
533534
{
534535
$graphQlType = null;

src/Mappers/CannotMapTypeException.php

+19
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,12 @@
44
namespace TheCodingMachine\GraphQLite\Mappers;
55

66

7+
use GraphQL\Error\SyntaxError;
78
use GraphQL\Type\Definition\ObjectType;
9+
use ReflectionClass;
810
use ReflectionMethod;
11+
use function sprintf;
12+
use TheCodingMachine\GraphQLite\Annotations\SourceField;
913

1014
class CannotMapTypeException extends \Exception implements CannotMapTypeExceptionInterface
1115
{
@@ -24,6 +28,11 @@ public static function createForName(string $name): self
2428
return new self('cannot find GraphQL type "'.$name.'". Check your TypeMapper configuration.');
2529
}
2630

31+
public static function createForParseError(SyntaxError $error): self
32+
{
33+
return new self($error->getMessage(), $error->getCode(), $error);
34+
}
35+
2736
public static function wrapWithParamInfo(CannotMapTypeExceptionInterface $previous, \ReflectionParameter $parameter): self
2837
{
2938
$message = sprintf('For parameter $%s, in %s::%s, %s',
@@ -45,6 +54,16 @@ public static function wrapWithReturnInfo(CannotMapTypeExceptionInterface $previ
4554
return new self($message, 0, $previous);
4655
}
4756

57+
public static function wrapWithSourceField(CannotMapTypeExceptionInterface $previous, ReflectionClass $class, SourceField $sourceField): self
58+
{
59+
$message = sprintf('For @SourceField "%s" declared in "%s", %s',
60+
$sourceField->getName(),
61+
$class->getName(),
62+
$previous->getMessage());
63+
64+
return new self($message, 0, $previous);
65+
}
66+
4867
public static function mustBeOutputType($subTypeName): self
4968
{
5069
return new self('type "'.$subTypeName.'" must be an output type.');

src/Types/TypeResolver.php

+22-2
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,13 @@
33

44
namespace TheCodingMachine\GraphQLite\Types;
55

6+
use GraphQL\Error\Error;
7+
use GraphQL\Language\Parser;
8+
use GraphQL\Type\Definition\OutputType;
69
use GraphQL\Type\Definition\Type;
10+
use GraphQL\Type\Definition\WrappingType;
711
use GraphQL\Type\Schema;
12+
use GraphQL\Utils\AST;
813
use RuntimeException;
914
use TheCodingMachine\GraphQLite\Mappers\CannotMapTypeException;
1015
use TheCodingMachine\GraphQLite\Mappers\CannotMapTypeExceptionInterface;
@@ -36,12 +41,27 @@ public function mapNameToType(string $typeName): Type
3641
if ($this->schema === null) {
3742
throw new RuntimeException('You must register a schema first before resolving types.');
3843
}
39-
40-
$type = $this->schema->getType($typeName);
44+
45+
try {
46+
$parsedOutputType = Parser::parseType($typeName);
47+
$type = AST::typeFromAST($this->schema, $parsedOutputType);
48+
} catch (Error $e) {
49+
throw CannotMapTypeException::createForParseError($e);
50+
}
51+
4152
if ($type === null) {
4253
throw CannotMapTypeException::createForName($typeName);
4354
}
4455

4556
return $type;
4657
}
58+
59+
public function mapNameToOutputType(string $typeName): OutputType
60+
{
61+
$type = $this->mapNameToType($typeName);
62+
if (!$type instanceof OutputType || ($type instanceof WrappingType && !$type->getWrappedType() instanceof OutputType)) {
63+
throw CannotMapTypeException::mustBeOutputType($typeName);
64+
}
65+
return $type;
66+
}
4767
}

tests/AggregateControllerQueryProviderTest.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public function has($id)
4343
$aggregateQueryProvider = new AggregateControllerQueryProvider([ 'controller' ], $this->getControllerQueryProviderFactory(), $this->getTypeMapper(), $container);
4444

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

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

tests/FieldsBuilderTest.php

+50-5
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,11 @@
2424
use TheCodingMachine\GraphQLite\Fixtures\TestControllerWithInvalidReturnType;
2525
use TheCodingMachine\GraphQLite\Fixtures\TestControllerWithIterableParam;
2626
use TheCodingMachine\GraphQLite\Fixtures\TestControllerWithIterableReturnType;
27+
use TheCodingMachine\GraphQLite\Fixtures\TestFieldBadOutputType;
2728
use TheCodingMachine\GraphQLite\Fixtures\TestObject;
2829
use TheCodingMachine\GraphQLite\Fixtures\TestSelfType;
30+
use TheCodingMachine\GraphQLite\Fixtures\TestSourceFieldBadOutputType;
31+
use TheCodingMachine\GraphQLite\Fixtures\TestSourceFieldBadOutputType2;
2932
use TheCodingMachine\GraphQLite\Fixtures\TestType;
3033
use TheCodingMachine\GraphQLite\Fixtures\TestTypeId;
3134
use TheCodingMachine\GraphQLite\Fixtures\TestTypeMissingAnnotation;
@@ -36,6 +39,7 @@
3639
use TheCodingMachine\GraphQLite\Containers\EmptyContainer;
3740
use TheCodingMachine\GraphQLite\Containers\BasicAutoWiringContainer;
3841
use TheCodingMachine\GraphQLite\Mappers\CannotMapTypeException;
42+
use TheCodingMachine\GraphQLite\Mappers\CannotMapTypeExceptionInterface;
3943
use TheCodingMachine\GraphQLite\Reflection\CachedDocBlockFactory;
4044
use TheCodingMachine\GraphQLite\Security\AuthenticationServiceInterface;
4145
use TheCodingMachine\GraphQLite\Security\AuthorizationServiceInterface;
@@ -55,7 +59,7 @@ public function testQueryProvider()
5559

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

58-
$this->assertCount(6, $queries);
62+
$this->assertCount(7, $queries);
5963
$usersQuery = $queries[0];
6064
$this->assertSame('test', $usersQuery->name);
6165

@@ -146,12 +150,29 @@ public function testQueryProviderWithFixedReturnType()
146150

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

149-
$this->assertCount(6, $queries);
153+
$this->assertCount(7, $queries);
150154
$fixedQuery = $queries[1];
151155

152156
$this->assertInstanceOf(IDType::class, $fixedQuery->getType());
153157
}
154158

159+
public function testQueryProviderWithComplexFixedReturnType()
160+
{
161+
$controller = new TestController();
162+
163+
$queryProvider = $this->buildFieldsBuilder();
164+
165+
$queries = $queryProvider->getQueries($controller);
166+
167+
$this->assertCount(7, $queries);
168+
$fixedQuery = $queries[6];
169+
170+
$this->assertInstanceOf(NonNull::class, $fixedQuery->getType());
171+
$this->assertInstanceOf(ListOfType::class, $fixedQuery->getType()->getWrappedType());
172+
$this->assertInstanceOf(NonNull::class, $fixedQuery->getType()->getWrappedType()->getWrappedType());
173+
$this->assertInstanceOf(IDType::class, $fixedQuery->getType()->getWrappedType()->getWrappedType()->getWrappedType());
174+
}
175+
155176
public function testNameFromAnnotation()
156177
{
157178
$controller = new TestController();
@@ -312,7 +333,7 @@ public function testQueryProviderWithIterableClass()
312333

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

315-
$this->assertCount(6, $queries);
336+
$this->assertCount(7, $queries);
316337
$iterableQuery = $queries[3];
317338

318339
$this->assertInstanceOf(NonNull::class, $iterableQuery->getType());
@@ -328,7 +349,7 @@ public function testQueryProviderWithIterable()
328349

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

331-
$this->assertCount(6, $queries);
352+
$this->assertCount(7, $queries);
332353
$iterableQuery = $queries[4];
333354

334355
$this->assertInstanceOf(NonNull::class, $iterableQuery->getType());
@@ -353,7 +374,7 @@ public function testQueryProviderWithUnion()
353374

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

356-
$this->assertCount(6, $queries);
377+
$this->assertCount(7, $queries);
357378
$unionQuery = $queries[5];
358379

359380
$this->assertInstanceOf(NonNull::class, $unionQuery->getType());
@@ -471,4 +492,28 @@ public function testSourceFieldWithFailWith()
471492

472493
$this->assertInstanceOf(StringType::class, $fields['test']->getType());
473494
}
495+
496+
public function testSourceFieldBadOutputTypeException()
497+
{
498+
$queryProvider = $this->buildFieldsBuilder();
499+
$this->expectException(CannotMapTypeExceptionInterface::class);
500+
$this->expectExceptionMessage('For @SourceField "test" declared in "TheCodingMachine\GraphQLite\Fixtures\TestSourceFieldBadOutputType", cannot find GraphQL type "[NotExists]". Check your TypeMapper configuration.');
501+
$queryProvider->getFields(new TestSourceFieldBadOutputType(), true);
502+
}
503+
504+
public function testSourceFieldBadOutputType2Exception()
505+
{
506+
$queryProvider = $this->buildFieldsBuilder();
507+
$this->expectException(CannotMapTypeExceptionInterface::class);
508+
$this->expectExceptionMessage('For @SourceField "test" declared in "TheCodingMachine\GraphQLite\Fixtures\TestSourceFieldBadOutputType2", Syntax Error: Expected ], found <EOF>');
509+
$queryProvider->getFields(new TestSourceFieldBadOutputType2(), true);
510+
}
511+
512+
public function testBadOutputTypeException()
513+
{
514+
$queryProvider = $this->buildFieldsBuilder();
515+
$this->expectException(CannotMapTypeExceptionInterface::class);
516+
$this->expectExceptionMessage('For return type of TheCodingMachine\GraphQLite\Fixtures\TestFieldBadOutputType::test, cannot find GraphQL type "[NotExists]". Check your TypeMapper configuration.');
517+
$queryProvider->getFields(new TestFieldBadOutputType(), true);
518+
}
474519
}

tests/Fixtures/TestController.php

+8
Original file line numberDiff line numberDiff line change
@@ -107,4 +107,12 @@ public function testUnion()
107107
{
108108
return new TestObject2('foo');
109109
}
110+
111+
/**
112+
* @Query(outputType="[ID!]!")
113+
*/
114+
public function testFixComplexReturnType(): array
115+
{
116+
return ['42'];
117+
}
110118
}
+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
4+
namespace TheCodingMachine\GraphQLite\Fixtures;
5+
6+
use TheCodingMachine\GraphQLite\Annotations\Field;
7+
use TheCodingMachine\GraphQLite\Annotations\Type;
8+
9+
/**
10+
* @Type(class=TestObject::class)
11+
*/
12+
class TestFieldBadOutputType
13+
{
14+
/**
15+
* @Field(outputType="[NotExists]")
16+
*/
17+
public function test(): array
18+
{
19+
return [];
20+
}
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
4+
namespace TheCodingMachine\GraphQLite\Fixtures;
5+
6+
use TheCodingMachine\GraphQLite\Annotations\SourceField;
7+
use TheCodingMachine\GraphQLite\Annotations\Type;
8+
9+
/**
10+
* @Type(class=TestObject::class)
11+
* @SourceField(name="test", outputType="[NotExists]")
12+
*/
13+
class TestSourceFieldBadOutputType
14+
{
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
4+
namespace TheCodingMachine\GraphQLite\Fixtures;
5+
6+
use TheCodingMachine\GraphQLite\Annotations\SourceField;
7+
use TheCodingMachine\GraphQLite\Annotations\Type;
8+
9+
/**
10+
* @Type(class=TestObject::class)
11+
* @SourceField(name="test", outputType="[BadFormat")
12+
*/
13+
class TestSourceFieldBadOutputType2
14+
{
15+
}

tests/GlobControllerQueryProviderTest.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public function has($id)
3737
$globControllerQueryProvider = new GlobControllerQueryProvider('TheCodingMachine\\GraphQLite\\Fixtures', $this->getControllerQueryProviderFactory(), $this->getTypeMapper(), $container, $this->getLockFactory(), new NullCache(), null, false);
3838

3939
$queries = $globControllerQueryProvider->getQueries();
40-
$this->assertCount(6, $queries);
40+
$this->assertCount(7, $queries);
4141

4242
$mutations = $globControllerQueryProvider->getMutations();
4343
$this->assertCount(1, $mutations);

0 commit comments

Comments
 (0)