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\Boolean; |
9
|
|
|
use phpDocumentor\Reflection\Types\Float_; |
10
|
|
|
use phpDocumentor\Reflection\Types\Mixed_; |
11
|
|
|
use phpDocumentor\Reflection\Types\Object_; |
12
|
|
|
use phpDocumentor\Reflection\Types\String_; |
13
|
|
|
use Psr\Container\ContainerInterface; |
14
|
|
|
use Roave\BetterReflection\Reflection\ReflectionClass; |
15
|
|
|
use Roave\BetterReflection\Reflection\ReflectionMethod; |
16
|
|
|
use Doctrine\Common\Annotations\Reader; |
17
|
|
|
use phpDocumentor\Reflection\Types\Integer; |
18
|
|
|
use TheCodingMachine\GraphQL\Controllers\Annotations\AbstractRequest; |
19
|
|
|
use TheCodingMachine\GraphQL\Controllers\Annotations\Logged; |
20
|
|
|
use TheCodingMachine\GraphQL\Controllers\Annotations\Mutation; |
21
|
|
|
use TheCodingMachine\GraphQL\Controllers\Annotations\Query; |
22
|
|
|
use TheCodingMachine\GraphQL\Controllers\Annotations\Right; |
23
|
|
|
use TheCodingMachine\GraphQL\Controllers\Reflection\CommentParser; |
24
|
|
|
use TheCodingMachine\GraphQL\Controllers\Registry\EmptyContainer; |
25
|
|
|
use TheCodingMachine\GraphQL\Controllers\Registry\Registry; |
26
|
|
|
use TheCodingMachine\GraphQL\Controllers\Registry\RegistryInterface; |
27
|
|
|
use TheCodingMachine\GraphQL\Controllers\Security\AuthenticationServiceInterface; |
28
|
|
|
use TheCodingMachine\GraphQL\Controllers\Security\AuthorizationServiceInterface; |
29
|
|
|
use Youshido\GraphQL\Field\Field; |
30
|
|
|
use Youshido\GraphQL\Type\ListType\ListType; |
31
|
|
|
use Youshido\GraphQL\Type\NonNullType; |
32
|
|
|
use Youshido\GraphQL\Type\Scalar\BooleanType; |
33
|
|
|
use Youshido\GraphQL\Type\Scalar\DateTimeType; |
34
|
|
|
use Youshido\GraphQL\Type\Scalar\FloatType; |
35
|
|
|
use Youshido\GraphQL\Type\Scalar\IntType; |
36
|
|
|
use Youshido\GraphQL\Type\Scalar\StringType; |
37
|
|
|
use Youshido\GraphQL\Type\TypeInterface; |
38
|
|
|
|
39
|
|
|
/** |
40
|
|
|
* A query provider that looks for queries in a "controller" |
41
|
|
|
*/ |
42
|
|
|
class ControllerQueryProvider implements QueryProviderInterface |
43
|
|
|
{ |
44
|
|
|
/** |
45
|
|
|
* @var object |
46
|
|
|
*/ |
47
|
|
|
private $controller; |
48
|
|
|
/** |
49
|
|
|
* @var Reader |
50
|
|
|
*/ |
51
|
|
|
private $annotationReader; |
52
|
|
|
/** |
53
|
|
|
* @var TypeMapperInterface |
54
|
|
|
*/ |
55
|
|
|
private $typeMapper; |
56
|
|
|
/** |
57
|
|
|
* @var HydratorInterface |
58
|
|
|
*/ |
59
|
|
|
private $hydrator; |
60
|
|
|
/** |
61
|
|
|
* @var AuthenticationServiceInterface |
62
|
|
|
*/ |
63
|
|
|
private $authenticationService; |
64
|
|
|
/** |
65
|
|
|
* @var AuthorizationServiceInterface |
66
|
|
|
*/ |
67
|
|
|
private $authorizationService; |
68
|
|
|
/** |
69
|
|
|
* @var RegistryInterface |
70
|
|
|
*/ |
71
|
|
|
private $registry; |
72
|
|
|
|
73
|
|
|
/** |
74
|
|
|
* @param object $controller |
75
|
|
|
*/ |
76
|
|
|
public function __construct($controller, RegistryInterface $registry) |
77
|
|
|
{ |
78
|
|
|
$this->controller = $controller; |
79
|
|
|
$this->annotationReader = $registry->getAnnotationReader(); |
80
|
|
|
$this->typeMapper = $registry->getTypeMapper(); |
81
|
|
|
$this->hydrator = $registry->getHydrator(); |
82
|
|
|
$this->authenticationService = $registry->getAuthenticationService(); |
83
|
|
|
$this->authorizationService = $registry->getAuthorizationService(); |
84
|
|
|
$this->registry = $registry; |
85
|
|
|
} |
86
|
|
|
|
87
|
|
|
/** |
88
|
|
|
* @return Field[] |
89
|
|
|
*/ |
90
|
|
|
public function getQueries(): array |
91
|
|
|
{ |
92
|
|
|
return $this->getFieldsByAnnotations(Query::class, false); |
93
|
|
|
} |
94
|
|
|
|
95
|
|
|
/** |
96
|
|
|
* @return Field[] |
97
|
|
|
*/ |
98
|
|
|
public function getMutations(): array |
99
|
|
|
{ |
100
|
|
|
return $this->getFieldsByAnnotations(Mutation::class, false); |
101
|
|
|
} |
102
|
|
|
|
103
|
|
|
/** |
104
|
|
|
* @return Field[] |
105
|
|
|
*/ |
106
|
|
|
public function getFields(): array |
107
|
|
|
{ |
108
|
|
|
return $this->getFieldsByAnnotations(Annotations\Field::class, true); |
109
|
|
|
} |
110
|
|
|
|
111
|
|
|
/** |
112
|
|
|
* @param string $annotationName |
113
|
|
|
* @param bool $injectSource Whether to inject the source object or not as the first argument. True for @Field, false for @Query and @Mutation |
114
|
|
|
* @return Field[] |
115
|
|
|
* @throws \ReflectionException |
116
|
|
|
*/ |
117
|
|
|
private function getFieldsByAnnotations(string $annotationName, bool $injectSource): array |
118
|
|
|
{ |
119
|
|
|
$refClass = ReflectionClass::createFromInstance($this->controller); |
120
|
|
|
|
121
|
|
|
$queryList = []; |
122
|
|
|
|
123
|
|
|
$typeResolver = new \phpDocumentor\Reflection\TypeResolver(); |
124
|
|
|
|
125
|
|
|
foreach ($refClass->getMethods() as $refMethod) { |
126
|
|
|
$standardPhpMethod = new \ReflectionMethod(get_class($this->controller), $refMethod->getName()); |
127
|
|
|
// First, let's check the "Query" or "Mutation" or "Field" annotation |
128
|
|
|
$queryAnnotation = $this->annotationReader->getMethodAnnotation($standardPhpMethod, $annotationName); |
129
|
|
|
/* @var $queryAnnotation AbstractRequest */ |
130
|
|
|
|
131
|
|
|
if ($queryAnnotation !== null) { |
132
|
|
|
$docBlock = new CommentParser($refMethod->getDocComment()); |
133
|
|
|
if (!$this->isAuthorized($standardPhpMethod)) { |
134
|
|
|
continue; |
135
|
|
|
} |
136
|
|
|
|
137
|
|
|
$methodName = $refMethod->getName(); |
138
|
|
|
|
139
|
|
|
$args = $this->mapParameters($refMethod, $standardPhpMethod); |
140
|
|
|
|
141
|
|
|
$phpdocType = $typeResolver->resolve((string) $refMethod->getReturnType()); |
142
|
|
|
|
143
|
|
|
if ($queryAnnotation->getReturnType()) { |
144
|
|
|
$type = $this->registry->get($queryAnnotation->getReturnType()); |
145
|
|
|
} else { |
146
|
|
|
try { |
147
|
|
|
$type = $this->mapType($phpdocType, $refMethod->getDocBlockReturnTypes(), $standardPhpMethod->getReturnType()->allowsNull(), false); |
148
|
|
|
} catch (TypeMappingException $e) { |
149
|
|
|
throw TypeMappingException::wrapWithReturnInfo($e, $refMethod); |
150
|
|
|
} |
151
|
|
|
} |
152
|
|
|
|
153
|
|
|
//$sourceType = null; |
|
|
|
|
154
|
|
|
if ($injectSource === true) { |
155
|
|
|
/*$sourceArr = */\array_shift($args); |
156
|
|
|
// Security check: if the first parameter of the correct type? |
157
|
|
|
//$sourceType = $sourceArr['type']; |
|
|
|
|
158
|
|
|
/* @var $sourceType TypeInterface */ |
159
|
|
|
// TODO |
160
|
|
|
} |
161
|
|
|
$queryList[] = new QueryField($methodName, $type, $args, [$this->controller, $methodName], $this->hydrator, $docBlock->getComment(), $injectSource); |
162
|
|
|
} |
163
|
|
|
} |
164
|
|
|
|
165
|
|
|
return $queryList; |
|
|
|
|
166
|
|
|
} |
167
|
|
|
|
168
|
|
|
/** |
169
|
|
|
* Checks the @Logged and @Right annotations. |
170
|
|
|
* |
171
|
|
|
* @param \ReflectionMethod $reflectionMethod |
172
|
|
|
* @return bool |
173
|
|
|
*/ |
174
|
|
|
private function isAuthorized(\ReflectionMethod $reflectionMethod) : bool |
175
|
|
|
{ |
176
|
|
|
$loggedAnnotation = $this->annotationReader->getMethodAnnotation($reflectionMethod, Logged::class); |
177
|
|
|
|
178
|
|
|
if ($loggedAnnotation !== null && !$this->authenticationService->isLogged()) { |
179
|
|
|
return false; |
180
|
|
|
} |
181
|
|
|
|
182
|
|
|
$rightAnnotation = $this->annotationReader->getMethodAnnotation($reflectionMethod, Right::class); |
183
|
|
|
/** @var $rightAnnotation Right */ |
184
|
|
|
|
185
|
|
|
if ($rightAnnotation !== null && !$this->authorizationService->isAllowed($rightAnnotation->getName())) { |
186
|
|
|
return false; |
187
|
|
|
} |
188
|
|
|
|
189
|
|
|
return true; |
190
|
|
|
} |
191
|
|
|
|
192
|
|
|
/** |
193
|
|
|
* Note: there is a bug in $refMethod->allowsNull that forces us to use $standardRefMethod->allowsNull instead. |
194
|
|
|
* |
195
|
|
|
* @param ReflectionMethod $refMethod |
196
|
|
|
* @param \ReflectionMethod $standardRefMethod |
197
|
|
|
* @return array[] An array of ['type'=>TypeInterface, 'default'=>val] |
198
|
|
|
* @throws MissingTypeHintException |
199
|
|
|
*/ |
200
|
|
|
private function mapParameters(ReflectionMethod $refMethod, \ReflectionMethod $standardRefMethod): array |
201
|
|
|
{ |
202
|
|
|
$args = []; |
203
|
|
|
|
204
|
|
|
$typeResolver = new \phpDocumentor\Reflection\TypeResolver(); |
205
|
|
|
|
206
|
|
|
foreach ($standardRefMethod->getParameters() as $standardParameter) { |
207
|
|
|
$allowsNull = $standardParameter->allowsNull(); |
208
|
|
|
$parameter = $refMethod->getParameter($standardParameter->getName()); |
209
|
|
|
|
210
|
|
|
$type = (string) $parameter->getType(); |
211
|
|
|
if ($type === '') { |
212
|
|
|
throw MissingTypeHintException::missingTypeHint($parameter); |
213
|
|
|
} |
214
|
|
|
$phpdocType = $typeResolver->resolve($type); |
215
|
|
|
|
216
|
|
|
try { |
217
|
|
|
$arr = [ |
218
|
|
|
'type' => $this->mapType($phpdocType, $parameter->getDocBlockTypes(), $allowsNull || $parameter->isDefaultValueAvailable(), true), |
219
|
|
|
]; |
220
|
|
|
} catch (TypeMappingException $e) { |
221
|
|
|
throw TypeMappingException::wrapWithParamInfo($e, $parameter); |
222
|
|
|
} |
223
|
|
|
|
224
|
|
|
if ($standardParameter->allowsNull()) { |
225
|
|
|
$arr['default'] = null; |
226
|
|
|
} |
227
|
|
|
if ($standardParameter->isDefaultValueAvailable()) { |
228
|
|
|
$arr['default'] = $standardParameter->getDefaultValue(); |
229
|
|
|
} |
230
|
|
|
|
231
|
|
|
$args[$parameter->getName()] = $arr; |
232
|
|
|
} |
233
|
|
|
|
234
|
|
|
return $args; |
235
|
|
|
} |
236
|
|
|
|
237
|
|
|
/** |
238
|
|
|
* @param Type $type |
239
|
|
|
* @param Type[] $docBlockTypes |
240
|
|
|
* @return TypeInterface |
241
|
|
|
*/ |
242
|
|
|
private function mapType(Type $type, array $docBlockTypes, bool $isNullable, bool $mapToInputType): TypeInterface |
243
|
|
|
{ |
244
|
|
|
$graphQlType = null; |
245
|
|
|
|
246
|
|
|
if ($type instanceof Array_ || $type instanceof Mixed_) { |
247
|
|
|
if (!$isNullable) { |
248
|
|
|
// Let's check a "null" value in the docblock |
249
|
|
|
$isNullable = $this->isNullable($docBlockTypes); |
250
|
|
|
} |
251
|
|
|
$filteredDocBlockTypes = $this->typesWithoutNullable($docBlockTypes); |
252
|
|
|
if (empty($filteredDocBlockTypes)) { |
253
|
|
|
throw TypeMappingException::createFromType($type); |
254
|
|
|
} elseif (count($filteredDocBlockTypes) === 1) { |
255
|
|
|
$graphQlType = $this->toGraphQlType($filteredDocBlockTypes[0], $mapToInputType); |
256
|
|
|
} else { |
257
|
|
|
throw new GraphQLException('Union types are not supported (yet)'); |
258
|
|
|
//$graphQlTypes = array_map([$this, 'toGraphQlType'], $filteredDocBlockTypes); |
|
|
|
|
259
|
|
|
//$$graphQlType = new UnionType($graphQlTypes); |
|
|
|
|
260
|
|
|
} |
261
|
|
|
} else { |
262
|
|
|
$graphQlType = $this->toGraphQlType($type, $mapToInputType); |
263
|
|
|
} |
264
|
|
|
|
265
|
|
|
if (!$isNullable) { |
266
|
|
|
$graphQlType = new NonNullType($graphQlType); |
267
|
|
|
} |
268
|
|
|
|
269
|
|
|
return $graphQlType; |
270
|
|
|
} |
271
|
|
|
|
272
|
|
|
/** |
273
|
|
|
* Casts a Type to a GraphQL type. |
274
|
|
|
* Does not deal with nullable. |
275
|
|
|
* |
276
|
|
|
* @param Type $type |
277
|
|
|
* @param bool $mapToInputType |
278
|
|
|
* @return TypeInterface |
279
|
|
|
*/ |
280
|
|
|
private function toGraphQlType(Type $type, bool $mapToInputType): TypeInterface |
281
|
|
|
{ |
282
|
|
|
if ($type instanceof Integer) { |
283
|
|
|
return new IntType(); |
284
|
|
|
} elseif ($type instanceof String_) { |
285
|
|
|
return new StringType(); |
286
|
|
|
} elseif ($type instanceof Boolean) { |
287
|
|
|
return new BooleanType(); |
288
|
|
|
} elseif ($type instanceof Float_) { |
289
|
|
|
return new FloatType(); |
290
|
|
|
} elseif ($type instanceof Object_) { |
291
|
|
|
$fqcn = (string) $type->getFqsen(); |
292
|
|
|
if ($fqcn === '\\DateTimeImmutable' || $fqcn === '\\DateTimeInterface') { |
293
|
|
|
return new DateTimeType(); |
294
|
|
|
} elseif ($fqcn === '\\DateTime') { |
295
|
|
|
throw new GraphQLException('Type-hinting a parameter against DateTime is not allowed. Please use the DateTimeImmutable type instead.'); |
296
|
|
|
} |
297
|
|
|
|
298
|
|
|
$className = ltrim($type->getFqsen(), '\\'); |
299
|
|
|
if ($mapToInputType) { |
300
|
|
|
return $this->typeMapper->mapClassToInputType($className); |
|
|
|
|
301
|
|
|
} else { |
302
|
|
|
return $this->typeMapper->mapClassToType($className); |
303
|
|
|
} |
304
|
|
|
} elseif ($type instanceof Array_) { |
305
|
|
|
return new ListType(new NonNullType($this->toGraphQlType($type->getValueType(), $mapToInputType))); |
306
|
|
|
} else { |
307
|
|
|
throw TypeMappingException::createFromType($type); |
308
|
|
|
} |
309
|
|
|
} |
310
|
|
|
|
311
|
|
|
/** |
312
|
|
|
* Removes "null" from the list of types. |
313
|
|
|
* |
314
|
|
|
* @param Type[] $docBlockTypeHints |
315
|
|
|
* @return array |
316
|
|
|
*/ |
317
|
|
|
private function typesWithoutNullable(array $docBlockTypeHints): array |
318
|
|
|
{ |
319
|
|
|
return array_filter($docBlockTypeHints, function ($item) { |
320
|
|
|
return !$item instanceof Null_; |
|
|
|
|
321
|
|
|
}); |
322
|
|
|
} |
323
|
|
|
|
324
|
|
|
/** |
325
|
|
|
* @param Type[] $docBlockTypeHints |
326
|
|
|
* @return bool |
327
|
|
|
*/ |
328
|
|
|
private function isNullable(array $docBlockTypeHints): bool |
329
|
|
|
{ |
330
|
|
|
foreach ($docBlockTypeHints as $docBlockTypeHint) { |
331
|
|
|
if ($docBlockTypeHint instanceof Null_) { |
332
|
|
|
return true; |
333
|
|
|
} |
334
|
|
|
} |
335
|
|
|
return false; |
336
|
|
|
} |
337
|
|
|
} |
338
|
|
|
|
Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.
The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.
This check looks for comments that seem to be mostly valid code and reports them.