1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
|
4
|
|
|
namespace TheCodingMachine\GraphQL\Controllers; |
5
|
|
|
|
6
|
|
|
use phpDocumentor\Reflection\Type; |
7
|
|
|
use phpDocumentor\Reflection\Types\Array_; |
8
|
|
|
use phpDocumentor\Reflection\Types\Mixed; |
9
|
|
|
use phpDocumentor\Reflection\Types\Object_; |
10
|
|
|
use phpDocumentor\Reflection\Types\String_; |
11
|
|
|
use Roave\BetterReflection\Reflection\ReflectionClass; |
12
|
|
|
use Roave\BetterReflection\Reflection\ReflectionMethod; |
13
|
|
|
use Doctrine\Common\Annotations\Reader; |
14
|
|
|
use phpDocumentor\Reflection\Types\Integer; |
15
|
|
|
use TheCodingMachine\GraphQL\Controllers\Annotations\Logged; |
16
|
|
|
use TheCodingMachine\GraphQL\Controllers\Annotations\Mutation; |
17
|
|
|
use TheCodingMachine\GraphQL\Controllers\Annotations\Query; |
18
|
|
|
use TheCodingMachine\GraphQL\Controllers\Annotations\Right; |
19
|
|
|
use TheCodingMachine\GraphQL\Controllers\Security\AuthenticationServiceInterface; |
20
|
|
|
use TheCodingMachine\GraphQL\Controllers\Security\AuthorizationServiceInterface; |
21
|
|
|
use Youshido\GraphQL\Field\Field; |
22
|
|
|
use Youshido\GraphQL\Type\ListType\ListType; |
23
|
|
|
use Youshido\GraphQL\Type\NonNullType; |
24
|
|
|
use Youshido\GraphQL\Type\Scalar\IntType; |
25
|
|
|
use Youshido\GraphQL\Type\Scalar\StringType; |
26
|
|
|
use Youshido\GraphQL\Type\TypeInterface; |
27
|
|
|
use Youshido\GraphQL\Type\Union\UnionType; |
28
|
|
|
|
29
|
|
|
/** |
30
|
|
|
* A query provider that looks for queries in a "controller" |
31
|
|
|
*/ |
32
|
|
|
class ControllerQueryProvider implements QueryProviderInterface |
33
|
|
|
{ |
34
|
|
|
/** |
35
|
|
|
* @var object |
36
|
|
|
*/ |
37
|
|
|
private $controller; |
38
|
|
|
/** |
39
|
|
|
* @var Reader |
40
|
|
|
*/ |
41
|
|
|
private $annotationReader; |
42
|
|
|
/** |
43
|
|
|
* @var TypeMapperInterface |
44
|
|
|
*/ |
45
|
|
|
private $typeMapper; |
46
|
|
|
/** |
47
|
|
|
* @var HydratorInterface |
48
|
|
|
*/ |
49
|
|
|
private $hydrator; |
50
|
|
|
/** |
51
|
|
|
* @var AuthenticationServiceInterface |
52
|
|
|
*/ |
53
|
|
|
private $authenticationService; |
54
|
|
|
/** |
55
|
|
|
* @var AuthorizationServiceInterface |
56
|
|
|
*/ |
57
|
|
|
private $authorizationService; |
58
|
|
|
|
59
|
|
|
/** |
60
|
|
|
* @param object $controller |
61
|
|
|
*/ |
62
|
|
View Code Duplication |
public function __construct($controller, Reader $annotationReader, TypeMapperInterface $typeMapper, HydratorInterface $hydrator, AuthenticationServiceInterface $authenticationService, AuthorizationServiceInterface $authorizationService) |
|
|
|
|
63
|
|
|
{ |
64
|
|
|
$this->controller = $controller; |
65
|
|
|
$this->annotationReader = $annotationReader; |
66
|
|
|
$this->typeMapper = $typeMapper; |
67
|
|
|
$this->hydrator = $hydrator; |
68
|
|
|
$this->authenticationService = $authenticationService; |
69
|
|
|
$this->authorizationService = $authorizationService; |
70
|
|
|
} |
71
|
|
|
|
72
|
|
|
/** |
73
|
|
|
* @return Field[] |
74
|
|
|
*/ |
75
|
|
|
public function getQueries(): array |
76
|
|
|
{ |
77
|
|
|
return $this->getFieldsByAnnotations(Query::class); |
78
|
|
|
} |
79
|
|
|
|
80
|
|
|
/** |
81
|
|
|
* @return Field[] |
82
|
|
|
*/ |
83
|
|
|
public function getMutations(): array |
84
|
|
|
{ |
85
|
|
|
return $this->getFieldsByAnnotations(Mutation::class); |
86
|
|
|
} |
87
|
|
|
|
88
|
|
|
/** |
89
|
|
|
* @return Field[] |
90
|
|
|
*/ |
91
|
|
|
private function getFieldsByAnnotations(string $annotationName): array |
92
|
|
|
{ |
93
|
|
|
$refClass = ReflectionClass::createFromInstance($this->controller); |
94
|
|
|
|
95
|
|
|
$queryList = []; |
96
|
|
|
|
97
|
|
|
foreach ($refClass->getMethods() as $refMethod) { |
98
|
|
|
$standardPhpMethod = new \ReflectionMethod(get_class($this->controller), $refMethod->getName()); |
99
|
|
|
// First, let's check the "Query" annotation |
100
|
|
|
$queryAnnotation = $this->annotationReader->getMethodAnnotation($standardPhpMethod, $annotationName); |
101
|
|
|
|
102
|
|
|
if ($queryAnnotation !== null) { |
103
|
|
|
if (!$this->isAuthorized($standardPhpMethod)) { |
104
|
|
|
continue; |
105
|
|
|
} |
106
|
|
|
|
107
|
|
|
$methodName = $refMethod->getName(); |
108
|
|
|
|
109
|
|
|
$args = $this->mapParameters($refMethod, $standardPhpMethod); |
110
|
|
|
|
111
|
|
|
$type = $this->mapType($refMethod->getReturnType()->getTypeObject(), $refMethod->getDocBlockReturnTypes(), $standardPhpMethod->getReturnType()->allowsNull()); |
112
|
|
|
|
113
|
|
|
$queryList[] = new QueryField($methodName, $type, $args, [$this->controller, $methodName], $this->hydrator); |
114
|
|
|
} |
115
|
|
|
} |
116
|
|
|
|
117
|
|
|
return $queryList; |
118
|
|
|
} |
119
|
|
|
|
120
|
|
|
/** |
121
|
|
|
* Checks the @Logged and @Right annotations. |
122
|
|
|
* |
123
|
|
|
* @param \ReflectionMethod $reflectionMethod |
124
|
|
|
* @return bool |
125
|
|
|
*/ |
126
|
|
|
private function isAuthorized(\ReflectionMethod $reflectionMethod) : bool |
127
|
|
|
{ |
128
|
|
|
$loggedAnnotation = $this->annotationReader->getMethodAnnotation($reflectionMethod, Logged::class); |
129
|
|
|
|
130
|
|
|
if ($loggedAnnotation !== null && !$this->authenticationService->isLogged()) { |
131
|
|
|
return false; |
132
|
|
|
} |
133
|
|
|
|
134
|
|
|
$rightAnnotation = $this->annotationReader->getMethodAnnotation($reflectionMethod, Right::class); |
135
|
|
|
/** @var $rightAnnotation Right */ |
136
|
|
|
|
137
|
|
|
if ($rightAnnotation !== null && !$this->authorizationService->isAllowed($rightAnnotation->getName())) { |
138
|
|
|
return false; |
139
|
|
|
} |
140
|
|
|
|
141
|
|
|
return true; |
142
|
|
|
} |
143
|
|
|
|
144
|
|
|
/** |
145
|
|
|
* Note: there is a bug in $refMethod->allowsNull that forces us to use $standardRefMethod->allowsNull instead. |
146
|
|
|
* |
147
|
|
|
* @param ReflectionMethod $refMethod |
148
|
|
|
* @param \ReflectionMethod $standardRefMethod |
149
|
|
|
* @return array |
150
|
|
|
*/ |
151
|
|
|
private function mapParameters(ReflectionMethod $refMethod, \ReflectionMethod $standardRefMethod) |
152
|
|
|
{ |
153
|
|
|
$args = []; |
154
|
|
|
foreach ($standardRefMethod->getParameters() as $standardParameter) { |
155
|
|
|
$allowsNull = $standardParameter->allowsNull(); |
156
|
|
|
$parameter = $refMethod->getParameter($standardParameter->getName()); |
|
|
|
|
157
|
|
|
$args[$parameter->getName()] = $this->mapType($parameter->getTypeHint(), $parameter->getDocBlockTypes(), $allowsNull); |
158
|
|
|
} |
159
|
|
|
|
160
|
|
|
return $args; |
161
|
|
|
} |
162
|
|
|
|
163
|
|
|
/** |
164
|
|
|
* @param Type $type |
165
|
|
|
* @param Type[] $docBlockTypes |
166
|
|
|
* @return TypeInterface |
167
|
|
|
*/ |
168
|
|
|
private function mapType(Type $type, array $docBlockTypes, bool $isNullable): TypeInterface |
169
|
|
|
{ |
170
|
|
|
$graphQlType = null; |
|
|
|
|
171
|
|
|
|
172
|
|
|
if ($type instanceof Array_ || $type instanceof Mixed) { |
|
|
|
|
173
|
|
|
if (!$isNullable) { |
174
|
|
|
// Let's check a "null" value in the docblock |
175
|
|
|
$isNullable = $this->isNullable($docBlockTypes); |
176
|
|
|
} |
177
|
|
|
$filteredDocBlockTypes = $this->typesWithoutNullable($docBlockTypes); |
178
|
|
|
if (empty($filteredDocBlockTypes)) { |
179
|
|
|
// TODO: improve error message |
180
|
|
|
throw new GraphQLException("Don't know how to handle type ".((string) $type)); |
181
|
|
|
} elseif (count($filteredDocBlockTypes) === 1) { |
182
|
|
|
$graphQlType = $this->toGraphQlType($filteredDocBlockTypes[0]); |
183
|
|
|
} else { |
184
|
|
|
throw new GraphQLException('Union types are not supported (yet)'); |
185
|
|
|
//$graphQlTypes = array_map([$this, 'toGraphQlType'], $filteredDocBlockTypes); |
|
|
|
|
186
|
|
|
//$$graphQlType = new UnionType($graphQlTypes); |
|
|
|
|
187
|
|
|
} |
188
|
|
|
} else { |
189
|
|
|
$graphQlType = $this->toGraphQlType($type); |
190
|
|
|
} |
191
|
|
|
|
192
|
|
|
if (!$isNullable) { |
193
|
|
|
$graphQlType = new NonNullType($graphQlType); |
|
|
|
|
194
|
|
|
} |
195
|
|
|
|
196
|
|
|
return $graphQlType; |
197
|
|
|
} |
198
|
|
|
|
199
|
|
|
/** |
200
|
|
|
* Casts a Type to a GraphQL type. |
201
|
|
|
* Does not deal with nullable. |
202
|
|
|
* |
203
|
|
|
* @param Type $type |
204
|
|
|
* @return TypeInterface |
205
|
|
|
*/ |
206
|
|
|
private function toGraphQlType(Type $type): TypeInterface |
207
|
|
|
{ |
208
|
|
|
if ($type instanceof Integer) { |
209
|
|
|
return new IntType(); |
210
|
|
|
} elseif ($type instanceof String_) { |
211
|
|
|
return new StringType(); |
212
|
|
|
} elseif ($type instanceof Object_) { |
213
|
|
|
return $this->typeMapper->mapClassToType(ltrim($type->getFqsen(), '\\')); |
214
|
|
|
} elseif ($type instanceof Array_) { |
215
|
|
|
return new ListType(new NonNullType($this->toGraphQlType($type->getValueType()))); |
|
|
|
|
216
|
|
|
} else { |
217
|
|
|
throw new GraphQLException("Don't know how to handle type ".((string) $type)); |
218
|
|
|
} |
219
|
|
|
} |
220
|
|
|
|
221
|
|
|
/** |
222
|
|
|
* Removes "null" from the list of types. |
223
|
|
|
* |
224
|
|
|
* @param Type[] $docBlockTypeHints |
225
|
|
|
* @return array |
226
|
|
|
*/ |
227
|
|
|
private function typesWithoutNullable(array $docBlockTypeHints): array |
228
|
|
|
{ |
229
|
|
|
return array_filter($docBlockTypeHints, function ($item) { |
230
|
|
|
return !$item instanceof Null_; |
|
|
|
|
231
|
|
|
}); |
232
|
|
|
} |
233
|
|
|
|
234
|
|
|
/** |
235
|
|
|
* @param Type[] $docBlockTypeHints |
236
|
|
|
* @return bool |
237
|
|
|
*/ |
238
|
|
|
private function isNullable(array $docBlockTypeHints): bool |
239
|
|
|
{ |
240
|
|
|
foreach ($docBlockTypeHints as $docBlockTypeHint) { |
241
|
|
|
if ($docBlockTypeHint instanceof Null_) { |
|
|
|
|
242
|
|
|
return true; |
243
|
|
|
} |
244
|
|
|
} |
245
|
|
|
return false; |
246
|
|
|
} |
247
|
|
|
} |
248
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.