Skip to content

Commit 49cdab2

Browse files
authored
Merge pull request #14 from moufmouf/fixed_return_types
Adding the ability to specify the GraphQL return type manually
2 parents 391b65a + a3318e3 commit 49cdab2

14 files changed

+378
-24
lines changed

README.md

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@
1010
GraphQL controllers
1111
===================
1212

13-
**Work in progress, no stable release yet**
14-
1513
A utility library on top of `Youshido/graphql` library.
1614

1715
This library allows you to write your GraphQL queries in simple to write controllers:
@@ -92,12 +90,43 @@ public function users(int $limit, int $offset): array
9290
}
9391
```
9492

95-
Type-hinting against objects
96-
----------------------------
93+
Type-hinting against objects (automatic)
94+
----------------------------------------
9795

9896
When you specify an object type-hint, graphql-controllers will delegate the object creation to an hydrator.
9997
You must pass this hydrator in parameter when building the `ControllerQueryProvider`.
10098

99+
Type-hinting against objects (manually)
100+
---------------------------------------
101+
102+
As an alternative, you can also manually specify the GraphqlType of your return type manually.
103+
104+
```php
105+
/**
106+
* @Query(returnType=UserListType::class)
107+
*/
108+
public function users(int $limit, int $offset)
109+
{
110+
// Whatever the return type of the method, it will be managed as a GraphQL UserListType
111+
// UserListType must implement Youshido's TypeInterface
112+
}
113+
```
114+
115+
You can also specify the name of an entry in the container that resolves to the GraphQL type to be used.
116+
117+
```php
118+
/**
119+
* @Query(returnType="userListType")
120+
*/
121+
public function users(int $limit, int $offset)
122+
{
123+
// The return type is fetched from the container. Expected name is "userListType"
124+
}
125+
```
126+
127+
Note: for container discovery to work, you must pass the container when constructing the `ControllerQueryProvider` object.
128+
129+
101130
Troubleshooting
102131
---------------
103132

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
4+
namespace TheCodingMachine\GraphQL\Controllers\Annotations;
5+
6+
7+
abstract class AbstractRequest
8+
{
9+
/**
10+
* @var string|null
11+
*/
12+
private $returnType;
13+
14+
/**
15+
* @param mixed[] $attributes
16+
*/
17+
public function __construct(array $attributes = [])
18+
{
19+
$this->returnType = $attributes['returnType'] ?? null;
20+
}
21+
22+
/**
23+
* Returns the GraphQL return type of the request (as a string).
24+
* The string can represent the FQCN of the type or an entry in the container resolving to the GraphQL type.
25+
*
26+
* @return string|null
27+
*/
28+
public function getReturnType(): ?string
29+
{
30+
return $this->returnType;
31+
}
32+
}

src/Annotations/Mutation.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55

66
/**
77
* @Annotation
8+
* @Attributes({
9+
* @Attribute("returnType", type = "string"),
10+
* })
811
*/
9-
class Mutation
12+
class Mutation extends AbstractRequest
1013
{
1114
}

src/Annotations/Query.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55

66
/**
77
* @Annotation
8+
* @Attributes({
9+
* @Attribute("returnType", type = "string"),
10+
* })
811
*/
9-
class Query
12+
class Query extends AbstractRequest
1013
{
1114
}

src/ControllerQueryProvider.php

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,19 @@
1010
use phpDocumentor\Reflection\Types\Mixed_;
1111
use phpDocumentor\Reflection\Types\Object_;
1212
use phpDocumentor\Reflection\Types\String_;
13+
use Psr\Container\ContainerInterface;
1314
use Roave\BetterReflection\Reflection\ReflectionClass;
1415
use Roave\BetterReflection\Reflection\ReflectionMethod;
1516
use Doctrine\Common\Annotations\Reader;
1617
use phpDocumentor\Reflection\Types\Integer;
18+
use TheCodingMachine\GraphQL\Controllers\Annotations\AbstractRequest;
1719
use TheCodingMachine\GraphQL\Controllers\Annotations\Logged;
1820
use TheCodingMachine\GraphQL\Controllers\Annotations\Mutation;
1921
use TheCodingMachine\GraphQL\Controllers\Annotations\Query;
2022
use TheCodingMachine\GraphQL\Controllers\Annotations\Right;
2123
use TheCodingMachine\GraphQL\Controllers\Reflection\CommentParser;
24+
use TheCodingMachine\GraphQL\Controllers\Registry\EmptyContainer;
25+
use TheCodingMachine\GraphQL\Controllers\Registry\Registry;
2226
use TheCodingMachine\GraphQL\Controllers\Security\AuthenticationServiceInterface;
2327
use TheCodingMachine\GraphQL\Controllers\Security\AuthorizationServiceInterface;
2428
use Youshido\GraphQL\Field\Field;
@@ -30,7 +34,6 @@
3034
use Youshido\GraphQL\Type\Scalar\IntType;
3135
use Youshido\GraphQL\Type\Scalar\StringType;
3236
use Youshido\GraphQL\Type\TypeInterface;
33-
use Youshido\GraphQL\Type\Union\UnionType;
3437

3538
/**
3639
* A query provider that looks for queries in a "controller"
@@ -61,18 +64,23 @@ class ControllerQueryProvider implements QueryProviderInterface
6164
* @var AuthorizationServiceInterface
6265
*/
6366
private $authorizationService;
67+
/**
68+
* @var ContainerInterface
69+
*/
70+
private $registry;
6471

6572
/**
6673
* @param object $controller
6774
*/
68-
public function __construct($controller, Reader $annotationReader, TypeMapperInterface $typeMapper, HydratorInterface $hydrator, AuthenticationServiceInterface $authenticationService, AuthorizationServiceInterface $authorizationService)
75+
public function __construct($controller, Reader $annotationReader, TypeMapperInterface $typeMapper, HydratorInterface $hydrator, AuthenticationServiceInterface $authenticationService, AuthorizationServiceInterface $authorizationService, ?ContainerInterface $container = null)
6976
{
7077
$this->controller = $controller;
7178
$this->annotationReader = $annotationReader;
7279
$this->typeMapper = $typeMapper;
7380
$this->hydrator = $hydrator;
7481
$this->authenticationService = $authenticationService;
7582
$this->authorizationService = $authorizationService;
83+
$this->registry = new Registry($container ?: new EmptyContainer());
7684
}
7785

7886
/**
@@ -106,6 +114,7 @@ private function getFieldsByAnnotations(string $annotationName): array
106114
$standardPhpMethod = new \ReflectionMethod(get_class($this->controller), $refMethod->getName());
107115
// First, let's check the "Query" annotation
108116
$queryAnnotation = $this->annotationReader->getMethodAnnotation($standardPhpMethod, $annotationName);
117+
/* @var $queryAnnotation AbstractRequest */
109118

110119
if ($queryAnnotation !== null) {
111120
$docBlock = new CommentParser($refMethod->getDocComment());
@@ -119,11 +128,16 @@ private function getFieldsByAnnotations(string $annotationName): array
119128

120129
$phpdocType = $typeResolver->resolve((string) $refMethod->getReturnType());
121130

122-
try {
123-
$type = $this->mapType($phpdocType, $refMethod->getDocBlockReturnTypes(), $standardPhpMethod->getReturnType()->allowsNull(), false);
124-
} catch (TypeMappingException $e) {
125-
throw TypeMappingException::wrapWithReturnInfo($e, $refMethod);
131+
if ($queryAnnotation->getReturnType()) {
132+
$type = $this->registry->get($queryAnnotation->getReturnType());
133+
} else {
134+
try {
135+
$type = $this->mapType($phpdocType, $refMethod->getDocBlockReturnTypes(), $standardPhpMethod->getReturnType()->allowsNull(), false);
136+
} catch (TypeMappingException $e) {
137+
throw TypeMappingException::wrapWithReturnInfo($e, $refMethod);
138+
}
126139
}
140+
127141
$queryList[] = new QueryField($methodName, $type, $args, [$this->controller, $methodName], $this->hydrator, $docBlock->getComment());
128142
}
129143
}

src/Registry/EmptyContainer.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
4+
namespace TheCodingMachine\GraphQL\Controllers\Registry;
5+
6+
use Psr\Container\ContainerInterface;
7+
8+
/**
9+
* An always empty container (to use as a stub for the Registry).
10+
*/
11+
class EmptyContainer implements ContainerInterface
12+
{
13+
public function get($id)
14+
{
15+
throw NotFoundException::notFound($id);
16+
}
17+
18+
public function has($id)
19+
{
20+
return false;
21+
}
22+
}

src/Registry/NotFoundException.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
4+
namespace TheCodingMachine\GraphQL\Controllers\Registry;
5+
6+
use Psr\Container\NotFoundExceptionInterface;
7+
8+
class NotFoundException extends \RuntimeException implements NotFoundExceptionInterface
9+
{
10+
public static function notFound(string $id): NotFoundException
11+
{
12+
return new self('Could not find entry with ID / type with class "'.$id.'"');
13+
}
14+
}

src/Registry/Registry.php

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
<?php
2+
3+
4+
namespace TheCodingMachine\GraphQL\Controllers\Registry;
5+
6+
use Psr\Container\ContainerExceptionInterface;
7+
use Psr\Container\ContainerInterface;
8+
use Psr\Container\NotFoundExceptionInterface;
9+
use TheCodingMachine\GraphQL\Controllers\Security\AuthorizationServiceInterface;
10+
use Youshido\GraphQL\Type\Object\AbstractObjectType;
11+
12+
/**
13+
* The role of the registry is to provide access to all GraphQL types.
14+
* If the type is not found, it can be queried from the container, or if not in the container, it can be created from the Registry itself.
15+
*/
16+
class Registry implements ContainerInterface
17+
{
18+
19+
/**
20+
* @var ContainerInterface
21+
*/
22+
private $container;
23+
24+
/**
25+
* @var AbstractObjectType[]
26+
*/
27+
private $values = [];
28+
/**
29+
* @var null|AuthorizationServiceInterface
30+
*/
31+
private $authorizationService;
32+
33+
/**
34+
* @param ContainerInterface $container The proxied container.
35+
* @param AuthorizationServiceInterface|null $authorizationService
36+
*/
37+
public function __construct(ContainerInterface $container, AuthorizationServiceInterface $authorizationService = null)
38+
{
39+
$this->container = $container;
40+
$this->authorizationService = $authorizationService;
41+
}
42+
43+
/**
44+
* Finds an entry of the container by its identifier and returns it.
45+
*
46+
* @param string $id Identifier of the entry to look for.
47+
*
48+
* @throws NotFoundExceptionInterface No entry was found for **this** identifier.
49+
* @throws ContainerExceptionInterface Error while retrieving the entry.
50+
*
51+
* @return mixed Entry.
52+
*/
53+
public function get($id)
54+
{
55+
if (isset($this->values[$id])) {
56+
return $this->values[$id];
57+
}
58+
if ($this->container->has($id)) {
59+
return $this->container->get($id);
60+
}
61+
62+
if (is_a($id, AbstractObjectType::class, true)) {
63+
$this->values[$id] = new $id($this);
64+
return $this->values[$id];
65+
}
66+
67+
throw NotFoundException::notFound($id);
68+
}
69+
70+
/**
71+
* Returns true if the container can return an entry for the given identifier.
72+
* Returns false otherwise.
73+
*
74+
* `has($id)` returning true does not mean that `get($id)` will not throw an exception.
75+
* It does however mean that `get($id)` will not throw a `NotFoundExceptionInterface`.
76+
*
77+
* @param string $id Identifier of the entry to look for.
78+
*
79+
* @return bool
80+
*/
81+
public function has($id)
82+
{
83+
if (isset($this->values[$id])) {
84+
return true;
85+
}
86+
if ($this->container->has($id)) {
87+
return true;
88+
}
89+
90+
if (is_a($id, AbstractObjectType::class, true)) {
91+
return true;
92+
}
93+
94+
return false;
95+
}
96+
97+
/**
98+
* Returns the authorization service.
99+
*
100+
* @return AuthorizationServiceInterface|null
101+
*/
102+
public function getAuthorizationService(): ?AuthorizationServiceInterface
103+
{
104+
return $this->authorizationService;
105+
}
106+
}

tests/AggregateControllerQueryProviderTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public function has($id)
4444
$aggregateQueryProvider = new AggregateControllerQueryProvider([ 'controller' ], $container, $reader, $this->getTypeMapper(), $this->getHydrator(), new VoidAuthenticationService(), new VoidAuthorizationService());
4545

4646
$queries = $aggregateQueryProvider->getQueries();
47-
$this->assertCount(1, $queries);
47+
$this->assertCount(2, $queries);
4848

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

0 commit comments

Comments
 (0)