1 | <?php |
||||
2 | |||||
3 | |||||
4 | namespace TheCodingMachine\GraphQL\Controllers; |
||||
5 | |||||
6 | use function array_merge; |
||||
7 | use GraphQL\Type\Definition\InputType; |
||||
8 | use GraphQL\Type\Definition\ListOfType; |
||||
9 | use GraphQL\Type\Definition\NonNull; |
||||
10 | use GraphQL\Type\Definition\OutputType; |
||||
11 | use GraphQL\Type\Definition\WrappingType; |
||||
12 | use GraphQL\Upload\UploadType; |
||||
13 | use phpDocumentor\Reflection\Fqsen; |
||||
14 | use phpDocumentor\Reflection\Types\Nullable; |
||||
15 | use phpDocumentor\Reflection\Types\Self_; |
||||
16 | use Psr\Http\Message\UploadedFileInterface; |
||||
17 | use ReflectionMethod; |
||||
18 | use TheCodingMachine\GraphQL\Controllers\Hydrators\HydratorInterface; |
||||
19 | use TheCodingMachine\GraphQL\Controllers\Mappers\CannotMapTypeExceptionInterface; |
||||
20 | use TheCodingMachine\GraphQL\Controllers\Reflection\CachedDocBlockFactory; |
||||
21 | use TheCodingMachine\GraphQL\Controllers\Types\CustomTypesRegistry; |
||||
22 | use TheCodingMachine\GraphQL\Controllers\Types\ID; |
||||
23 | use TheCodingMachine\GraphQL\Controllers\Types\TypeResolver; |
||||
24 | use TheCodingMachine\GraphQL\Controllers\Types\UnionType; |
||||
25 | use Iterator; |
||||
26 | use IteratorAggregate; |
||||
27 | use phpDocumentor\Reflection\DocBlock; |
||||
28 | use phpDocumentor\Reflection\DocBlock\Tags\Return_; |
||||
29 | use phpDocumentor\Reflection\Type; |
||||
30 | use phpDocumentor\Reflection\Types\Array_; |
||||
31 | use phpDocumentor\Reflection\Types\Boolean; |
||||
32 | use phpDocumentor\Reflection\Types\Compound; |
||||
33 | use phpDocumentor\Reflection\Types\Float_; |
||||
34 | use phpDocumentor\Reflection\Types\Iterable_; |
||||
35 | use phpDocumentor\Reflection\Types\Mixed_; |
||||
36 | use phpDocumentor\Reflection\Types\Null_; |
||||
37 | use phpDocumentor\Reflection\Types\Object_; |
||||
38 | use phpDocumentor\Reflection\Types\String_; |
||||
39 | use phpDocumentor\Reflection\Types\Integer; |
||||
40 | use ReflectionClass; |
||||
41 | use TheCodingMachine\GraphQL\Controllers\Annotations\SourceField; |
||||
42 | use TheCodingMachine\GraphQL\Controllers\Annotations\Logged; |
||||
43 | use TheCodingMachine\GraphQL\Controllers\Annotations\Mutation; |
||||
44 | use TheCodingMachine\GraphQL\Controllers\Annotations\Query; |
||||
45 | use TheCodingMachine\GraphQL\Controllers\Mappers\CannotMapTypeException; |
||||
46 | use TheCodingMachine\GraphQL\Controllers\Mappers\RecursiveTypeMapperInterface; |
||||
47 | use TheCodingMachine\GraphQL\Controllers\Reflection\CommentParser; |
||||
48 | use TheCodingMachine\GraphQL\Controllers\Security\AuthenticationServiceInterface; |
||||
49 | use TheCodingMachine\GraphQL\Controllers\Security\AuthorizationServiceInterface; |
||||
50 | use TheCodingMachine\GraphQL\Controllers\Types\DateTimeType; |
||||
51 | use GraphQL\Type\Definition\Type as GraphQLType; |
||||
52 | |||||
53 | /** |
||||
54 | * A class in charge if returning list of fields for queries / mutations / entities / input types |
||||
55 | */ |
||||
56 | class FieldsBuilder |
||||
57 | { |
||||
58 | /** |
||||
59 | * @var AnnotationReader |
||||
60 | */ |
||||
61 | private $annotationReader; |
||||
62 | /** |
||||
63 | * @var RecursiveTypeMapperInterface |
||||
64 | */ |
||||
65 | private $typeMapper; |
||||
66 | /** |
||||
67 | * @var HydratorInterface |
||||
68 | */ |
||||
69 | private $hydrator; |
||||
70 | /** |
||||
71 | * @var AuthenticationServiceInterface |
||||
72 | */ |
||||
73 | private $authenticationService; |
||||
74 | /** |
||||
75 | * @var AuthorizationServiceInterface |
||||
76 | */ |
||||
77 | private $authorizationService; |
||||
78 | /** |
||||
79 | * @var CachedDocBlockFactory |
||||
80 | */ |
||||
81 | private $cachedDocBlockFactory; |
||||
82 | /** |
||||
83 | * @var TypeResolver |
||||
84 | */ |
||||
85 | private $typeResolver; |
||||
86 | /** |
||||
87 | * @var NamingStrategyInterface |
||||
88 | */ |
||||
89 | private $namingStrategy; |
||||
90 | |||||
91 | public function __construct(AnnotationReader $annotationReader, RecursiveTypeMapperInterface $typeMapper, |
||||
92 | HydratorInterface $hydrator, AuthenticationServiceInterface $authenticationService, |
||||
93 | AuthorizationServiceInterface $authorizationService, TypeResolver $typeResolver, |
||||
94 | CachedDocBlockFactory $cachedDocBlockFactory, NamingStrategyInterface $namingStrategy) |
||||
95 | { |
||||
96 | $this->annotationReader = $annotationReader; |
||||
97 | $this->typeMapper = $typeMapper; |
||||
98 | $this->hydrator = $hydrator; |
||||
99 | $this->authenticationService = $authenticationService; |
||||
100 | $this->authorizationService = $authorizationService; |
||||
101 | $this->typeResolver = $typeResolver; |
||||
102 | $this->cachedDocBlockFactory = $cachedDocBlockFactory; |
||||
103 | $this->namingStrategy = $namingStrategy; |
||||
104 | } |
||||
105 | |||||
106 | // TODO: Add RecursiveTypeMapper in the list of parameters for getQueries and REMOVE the ControllerQueryProviderFactory. |
||||
107 | |||||
108 | /** |
||||
109 | * @param object $controller |
||||
110 | * @return QueryField[] |
||||
111 | * @throws \ReflectionException |
||||
112 | */ |
||||
113 | public function getQueries($controller): array |
||||
114 | { |
||||
115 | return $this->getFieldsByAnnotations($controller,Query::class, false); |
||||
116 | } |
||||
117 | |||||
118 | /** |
||||
119 | * @param object $controller |
||||
120 | * @return QueryField[] |
||||
121 | * @throws \ReflectionException |
||||
122 | */ |
||||
123 | public function getMutations($controller): array |
||||
124 | { |
||||
125 | return $this->getFieldsByAnnotations($controller,Mutation::class, false); |
||||
126 | } |
||||
127 | |||||
128 | /** |
||||
129 | * @return array<string, QueryField> QueryField indexed by name. |
||||
130 | */ |
||||
131 | public function getFields($controller): array |
||||
132 | { |
||||
133 | $fieldAnnotations = $this->getFieldsByAnnotations($controller, Annotations\Field::class, true); |
||||
134 | $sourceFields = $this->getSourceFields($controller); |
||||
135 | |||||
136 | $fields = []; |
||||
137 | foreach ($fieldAnnotations as $field) { |
||||
138 | $fields[$field->name] = $field; |
||||
139 | } |
||||
140 | foreach ($sourceFields as $field) { |
||||
141 | $fields[$field->name] = $field; |
||||
142 | } |
||||
143 | |||||
144 | return $fields; |
||||
145 | } |
||||
146 | |||||
147 | /** |
||||
148 | * Track Field annotation in a self targeted type |
||||
149 | * |
||||
150 | * @return array<string, QueryField> QueryField indexed by name. |
||||
151 | */ |
||||
152 | public function getSelfFields(string $className): array |
||||
153 | { |
||||
154 | $fieldAnnotations = $this->getFieldsByAnnotations(null, Annotations\Field::class, false, $className); |
||||
155 | |||||
156 | $fields = []; |
||||
157 | foreach ($fieldAnnotations as $field) { |
||||
158 | $fields[$field->name] = $field; |
||||
159 | } |
||||
160 | |||||
161 | return $fields; |
||||
162 | } |
||||
163 | |||||
164 | /** |
||||
165 | * @param ReflectionMethod $refMethod A method annotated with a Factory annotation. |
||||
166 | * @return array<string, array<int, mixed>> Returns an array of fields as accepted by the InputObjectType constructor. |
||||
167 | */ |
||||
168 | public function getInputFields(ReflectionMethod $refMethod): array |
||||
169 | { |
||||
170 | $docBlockObj = $this->cachedDocBlockFactory->getDocBlock($refMethod); |
||||
171 | //$docBlockComment = $docBlockObj->getSummary()."\n".$docBlockObj->getDescription()->render(); |
||||
172 | |||||
173 | $parameters = $refMethod->getParameters(); |
||||
174 | |||||
175 | $args = $this->mapParameters($parameters, $docBlockObj); |
||||
176 | |||||
177 | return $args; |
||||
178 | } |
||||
179 | |||||
180 | /** |
||||
181 | * @param object $controller |
||||
182 | * @param string $annotationName |
||||
183 | * @param bool $injectSource Whether to inject the source object or not as the first argument. True for @Field (unless @Type has no class attribute), false for @Query and @Mutation |
||||
184 | * @return QueryField[] |
||||
185 | * @throws CannotMapTypeExceptionInterface |
||||
186 | * @throws \ReflectionException |
||||
187 | */ |
||||
188 | private function getFieldsByAnnotations($controller, string $annotationName, bool $injectSource, ?string $sourceClassName = null): array |
||||
189 | { |
||||
190 | if ($sourceClassName !== null) { |
||||
191 | $refClass = new \ReflectionClass($sourceClassName); |
||||
192 | } else { |
||||
193 | $refClass = new \ReflectionClass($controller); |
||||
194 | } |
||||
195 | |||||
196 | $queryList = []; |
||||
197 | |||||
198 | $oldDeclaringClass = null; |
||||
0 ignored issues
–
show
Unused Code
introduced
by
![]() |
|||||
199 | $context = null; |
||||
0 ignored issues
–
show
|
|||||
200 | |||||
201 | foreach ($refClass->getMethods() as $refMethod) { |
||||
202 | // First, let's check the "Query" or "Mutation" or "Field" annotation |
||||
203 | $queryAnnotation = $this->annotationReader->getRequestAnnotation($refMethod, $annotationName); |
||||
204 | |||||
205 | if ($queryAnnotation !== null) { |
||||
206 | $unauthorized = false; |
||||
207 | if (!$this->isAuthorized($refMethod)) { |
||||
208 | $failWith = $this->annotationReader->getFailWithAnnotation($refMethod); |
||||
209 | if ($failWith === null) { |
||||
210 | continue; |
||||
211 | } |
||||
212 | $unauthorized = true; |
||||
213 | } |
||||
214 | |||||
215 | $docBlockObj = $this->cachedDocBlockFactory->getDocBlock($refMethod); |
||||
216 | $docBlockComment = $docBlockObj->getSummary()."\n".$docBlockObj->getDescription()->render(); |
||||
217 | |||||
218 | $methodName = $refMethod->getName(); |
||||
219 | $name = $queryAnnotation->getName() ?: $this->namingStrategy->getFieldNameFromMethodName($methodName); |
||||
220 | |||||
221 | $parameters = $refMethod->getParameters(); |
||||
222 | if ($injectSource === true) { |
||||
223 | $first_parameter = array_shift($parameters); |
||||
0 ignored issues
–
show
|
|||||
224 | // TODO: check that $first_parameter type is correct. |
||||
225 | } |
||||
226 | |||||
227 | $args = $this->mapParameters($parameters, $docBlockObj); |
||||
228 | |||||
229 | if ($queryAnnotation->getOutputType()) { |
||||
230 | $type = $this->typeResolver->mapNameToType($queryAnnotation->getOutputType()); |
||||
231 | if (!$type instanceof OutputType) { |
||||
232 | 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())); |
||||
233 | } |
||||
234 | } else { |
||||
235 | $type = $this->mapReturnType($refMethod, $docBlockObj); |
||||
236 | } |
||||
237 | |||||
238 | if (!$unauthorized) { |
||||
239 | $callable = [$controller, $methodName]; |
||||
240 | } else { |
||||
241 | $failWithValue = $failWith->getValue(); |
||||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||||
242 | $callable = function() use ($failWithValue) { |
||||
243 | return $failWithValue; |
||||
244 | }; |
||||
245 | if ($failWithValue === null && $type instanceof NonNull) { |
||||
246 | $type = $type->getWrappedType(); |
||||
247 | } |
||||
248 | } |
||||
249 | |||||
250 | if ($sourceClassName !== null) { |
||||
251 | $queryList[] = new QueryField($name, $type, $args, null, $callable[1], $this->hydrator, $docBlockComment, $injectSource); |
||||
0 ignored issues
–
show
It seems like
$type can also be of type GraphQL\Type\Definition\InputObjectType and GraphQL\Type\Definition\Type ; however, parameter $type of TheCodingMachine\GraphQL...eryField::__construct() does only seem to accept GraphQL\Type\Definition\OutputType , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
252 | } else { |
||||
253 | $queryList[] = new QueryField($name, $type, $args, $callable, null, $this->hydrator, $docBlockComment, $injectSource); |
||||
254 | } |
||||
255 | } |
||||
256 | } |
||||
257 | |||||
258 | return $queryList; |
||||
259 | } |
||||
260 | |||||
261 | /** |
||||
262 | * @return GraphQLType&OutputType |
||||
263 | */ |
||||
264 | private function mapReturnType(ReflectionMethod $refMethod, DocBlock $docBlockObj): GraphQLType |
||||
265 | { |
||||
266 | $returnType = $refMethod->getReturnType(); |
||||
267 | if ($returnType !== null) { |
||||
268 | $typeResolver = new \phpDocumentor\Reflection\TypeResolver(); |
||||
269 | $phpdocType = $typeResolver->resolve((string) $returnType); |
||||
270 | $phpdocType = $this->resolveSelf($phpdocType, $refMethod->getDeclaringClass()); |
||||
271 | } else { |
||||
272 | $phpdocType = new Mixed_(); |
||||
273 | } |
||||
274 | |||||
275 | $docBlockReturnType = $this->getDocBlocReturnType($docBlockObj, $refMethod); |
||||
276 | |||||
277 | try { |
||||
278 | /** @var GraphQLType&OutputType $type */ |
||||
279 | $type = $this->mapType($phpdocType, $docBlockReturnType, $returnType ? $returnType->allowsNull() : false, false); |
||||
280 | } catch (TypeMappingException $e) { |
||||
281 | throw TypeMappingException::wrapWithReturnInfo($e, $refMethod); |
||||
282 | } catch (CannotMapTypeExceptionInterface $e) { |
||||
283 | throw CannotMapTypeException::wrapWithReturnInfo($e, $refMethod); |
||||
284 | } |
||||
285 | return $type; |
||||
286 | } |
||||
287 | |||||
288 | private function getDocBlocReturnType(DocBlock $docBlock, \ReflectionMethod $refMethod): ?Type |
||||
289 | { |
||||
290 | /** @var Return_[] $returnTypeTags */ |
||||
291 | $returnTypeTags = $docBlock->getTagsByName('return'); |
||||
292 | if (count($returnTypeTags) > 1) { |
||||
293 | throw InvalidDocBlockException::tooManyReturnTags($refMethod); |
||||
294 | } |
||||
295 | $docBlockReturnType = null; |
||||
296 | if (isset($returnTypeTags[0])) { |
||||
297 | $docBlockReturnType = $returnTypeTags[0]->getType(); |
||||
298 | } |
||||
299 | return $docBlockReturnType; |
||||
300 | } |
||||
301 | |||||
302 | /** |
||||
303 | * @param object $controller |
||||
304 | * @return QueryField[] |
||||
305 | * @throws CannotMapTypeExceptionInterface |
||||
306 | * @throws \ReflectionException |
||||
307 | */ |
||||
308 | private function getSourceFields($controller): array |
||||
309 | { |
||||
310 | $refClass = new \ReflectionClass($controller); |
||||
311 | |||||
312 | /** @var SourceField[] $sourceFields */ |
||||
313 | $sourceFields = $this->annotationReader->getSourceFields($refClass); |
||||
314 | |||||
315 | if ($controller instanceof FromSourceFieldsInterface) { |
||||
316 | $sourceFields = array_merge($sourceFields, $controller->getSourceFields()); |
||||
317 | } |
||||
318 | |||||
319 | if (empty($sourceFields)) { |
||||
320 | return []; |
||||
321 | } |
||||
322 | |||||
323 | $typeField = $this->annotationReader->getTypeAnnotation($refClass); |
||||
324 | |||||
325 | if ($typeField === null) { |
||||
326 | throw MissingAnnotationException::missingTypeExceptionToUseSourceField(); |
||||
327 | } |
||||
328 | |||||
329 | $objectClass = $typeField->getClass(); |
||||
330 | $objectRefClass = new \ReflectionClass($objectClass); |
||||
331 | |||||
332 | $oldDeclaringClass = null; |
||||
0 ignored issues
–
show
|
|||||
333 | $context = null; |
||||
0 ignored issues
–
show
|
|||||
334 | $queryList = []; |
||||
335 | |||||
336 | foreach ($sourceFields as $sourceField) { |
||||
337 | // Ignore the field if we must be logged. |
||||
338 | $right = $sourceField->getRight(); |
||||
339 | $unauthorized = false; |
||||
340 | if (($sourceField->isLogged() && !$this->authenticationService->isLogged()) |
||||
341 | || ($right !== null && !$this->authorizationService->isAllowed($right->getName()))) { |
||||
342 | if (!$sourceField->canFailWith()) { |
||||
343 | continue; |
||||
344 | } else { |
||||
345 | $unauthorized = true; |
||||
346 | } |
||||
347 | } |
||||
348 | |||||
349 | try { |
||||
350 | $refMethod = $this->getMethodFromPropertyName($objectRefClass, $sourceField->getName()); |
||||
351 | } catch (FieldNotFoundException $e) { |
||||
352 | throw FieldNotFoundException::wrapWithCallerInfo($e, $refClass->getName()); |
||||
353 | } |
||||
354 | |||||
355 | $methodName = $refMethod->getName(); |
||||
356 | |||||
357 | |||||
358 | $docBlockObj = $this->cachedDocBlockFactory->getDocBlock($refMethod); |
||||
359 | $docBlockComment = $docBlockObj->getSummary()."\n".$docBlockObj->getDescription()->render(); |
||||
360 | |||||
361 | |||||
362 | $args = $this->mapParameters($refMethod->getParameters(), $docBlockObj); |
||||
363 | |||||
364 | if ($sourceField->isId()) { |
||||
365 | $type = GraphQLType::id(); |
||||
366 | if (!$refMethod->getReturnType()->allowsNull()) { |
||||
367 | $type = GraphQLType::nonNull($type); |
||||
368 | } |
||||
369 | } elseif ($sourceField->getOutputType()) { |
||||
370 | $type = $this->typeResolver->mapNameToType($sourceField->getOutputType()); |
||||
371 | } else { |
||||
372 | $type = $this->mapReturnType($refMethod, $docBlockObj); |
||||
373 | } |
||||
374 | |||||
375 | if (!$unauthorized) { |
||||
376 | $queryList[] = new QueryField($sourceField->getName(), $type, $args, null, $methodName, $this->hydrator, $docBlockComment, false); |
||||
0 ignored issues
–
show
It seems like
$type can also be of type GraphQL\Type\Definition\Type ; however, parameter $type of TheCodingMachine\GraphQL...eryField::__construct() does only seem to accept GraphQL\Type\Definition\OutputType , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
377 | } else { |
||||
378 | $failWithValue = $sourceField->getFailWith(); |
||||
379 | $callable = function() use ($failWithValue) { |
||||
380 | return $failWithValue; |
||||
381 | }; |
||||
382 | if ($failWithValue === null && $type instanceof NonNull) { |
||||
383 | $type = $type->getWrappedType(); |
||||
384 | } |
||||
385 | $queryList[] = new QueryField($sourceField->getName(), $type, $args, $callable, null, $this->hydrator, $docBlockComment, false); |
||||
0 ignored issues
–
show
It seems like
$type can also be of type GraphQL\Type\Definition\InputObjectType and GraphQL\Type\Definition\Type ; however, parameter $type of TheCodingMachine\GraphQL...eryField::__construct() does only seem to accept GraphQL\Type\Definition\OutputType , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
386 | } |
||||
387 | |||||
388 | } |
||||
389 | return $queryList; |
||||
390 | } |
||||
391 | |||||
392 | private function getMethodFromPropertyName(\ReflectionClass $reflectionClass, string $propertyName): \ReflectionMethod |
||||
393 | { |
||||
394 | if ($reflectionClass->hasMethod($propertyName)) { |
||||
395 | $methodName = $propertyName; |
||||
396 | } else { |
||||
397 | $upperCasePropertyName = \ucfirst($propertyName); |
||||
398 | if ($reflectionClass->hasMethod('get'.$upperCasePropertyName)) { |
||||
399 | $methodName = 'get'.$upperCasePropertyName; |
||||
400 | } elseif ($reflectionClass->hasMethod('is'.$upperCasePropertyName)) { |
||||
401 | $methodName = 'is'.$upperCasePropertyName; |
||||
402 | } else { |
||||
403 | throw FieldNotFoundException::missingField($reflectionClass->getName(), $propertyName); |
||||
404 | } |
||||
405 | } |
||||
406 | |||||
407 | return $reflectionClass->getMethod($methodName); |
||||
408 | } |
||||
409 | |||||
410 | /** |
||||
411 | * Checks the @Logged and @Right annotations. |
||||
412 | * |
||||
413 | * @param \ReflectionMethod $reflectionMethod |
||||
414 | * @return bool |
||||
415 | */ |
||||
416 | private function isAuthorized(\ReflectionMethod $reflectionMethod) : bool |
||||
417 | { |
||||
418 | $loggedAnnotation = $this->annotationReader->getLoggedAnnotation($reflectionMethod); |
||||
419 | |||||
420 | if ($loggedAnnotation !== null && !$this->authenticationService->isLogged()) { |
||||
421 | return false; |
||||
422 | } |
||||
423 | |||||
424 | |||||
425 | $rightAnnotation = $this->annotationReader->getRightAnnotation($reflectionMethod); |
||||
426 | |||||
427 | if ($rightAnnotation !== null && !$this->authorizationService->isAllowed($rightAnnotation->getName())) { |
||||
428 | return false; |
||||
429 | } |
||||
430 | |||||
431 | return true; |
||||
432 | } |
||||
433 | |||||
434 | /** |
||||
435 | * Note: there is a bug in $refMethod->allowsNull that forces us to use $standardRefMethod->allowsNull instead. |
||||
436 | * |
||||
437 | * @param \ReflectionParameter[] $refParameters |
||||
438 | * @return array[] An array of ['type'=>Type, 'defaultValue'=>val] |
||||
439 | * @throws MissingTypeHintException |
||||
440 | */ |
||||
441 | private function mapParameters(array $refParameters, DocBlock $docBlock): array |
||||
442 | { |
||||
443 | $args = []; |
||||
444 | |||||
445 | $typeResolver = new \phpDocumentor\Reflection\TypeResolver(); |
||||
446 | |||||
447 | foreach ($refParameters as $parameter) { |
||||
448 | $parameterType = $parameter->getType(); |
||||
449 | $allowsNull = $parameterType === null ? true : $parameterType->allowsNull(); |
||||
450 | |||||
451 | $type = (string) $parameterType; |
||||
452 | if ($type === '') { |
||||
453 | throw MissingTypeHintException::missingTypeHint($parameter); |
||||
454 | } |
||||
455 | $phpdocType = $typeResolver->resolve($type); |
||||
456 | $phpdocType = $this->resolveSelf($phpdocType, $parameter->getDeclaringClass()); |
||||
457 | |||||
458 | /** @var DocBlock\Tags\Param[] $paramTags */ |
||||
459 | $paramTags = $docBlock->getTagsByName('param'); |
||||
460 | $docBlockType = null; |
||||
461 | foreach ($paramTags as $paramTag) { |
||||
462 | if ($paramTag->getVariableName() === $parameter->getName()) { |
||||
463 | $docBlockType = $paramTag->getType(); |
||||
464 | break; |
||||
465 | } |
||||
466 | } |
||||
467 | |||||
468 | try { |
||||
469 | $arr = [ |
||||
470 | 'type' => $this->mapType($phpdocType, $docBlockType, $allowsNull || $parameter->isDefaultValueAvailable(), true), |
||||
471 | ]; |
||||
472 | } catch (TypeMappingException $e) { |
||||
473 | throw TypeMappingException::wrapWithParamInfo($e, $parameter); |
||||
474 | } catch (CannotMapTypeExceptionInterface $e) { |
||||
475 | throw CannotMapTypeException::wrapWithParamInfo($e, $parameter); |
||||
476 | } |
||||
477 | |||||
478 | if ($parameter->allowsNull()) { |
||||
479 | $arr['defaultValue'] = null; |
||||
480 | } |
||||
481 | if ($parameter->isDefaultValueAvailable()) { |
||||
482 | $arr['defaultValue'] = $parameter->getDefaultValue(); |
||||
483 | } |
||||
484 | |||||
485 | $args[$parameter->getName()] = $arr; |
||||
486 | } |
||||
487 | |||||
488 | return $args; |
||||
489 | } |
||||
490 | |||||
491 | /** |
||||
492 | * @param Type $type |
||||
493 | * @param Type|null $docBlockType |
||||
494 | * @return GraphQLType |
||||
495 | */ |
||||
496 | private function mapType(Type $type, ?Type $docBlockType, bool $isNullable, bool $mapToInputType): GraphQLType |
||||
497 | { |
||||
498 | $graphQlType = null; |
||||
499 | |||||
500 | if ($type instanceof Array_ || $type instanceof Iterable_ || $type instanceof Mixed_) { |
||||
501 | $graphQlType = $this->mapDocBlockType($type, $docBlockType, $isNullable, $mapToInputType); |
||||
502 | } else { |
||||
503 | try { |
||||
504 | $graphQlType = $this->toGraphQlType($type, null, $mapToInputType); |
||||
505 | if (!$isNullable) { |
||||
506 | $graphQlType = GraphQLType::nonNull($graphQlType); |
||||
507 | } |
||||
508 | } catch (TypeMappingException | CannotMapTypeExceptionInterface $e) { |
||||
509 | // Is the type iterable? If yes, let's analyze the docblock |
||||
510 | // TODO: it would be better not to go through an exception for this. |
||||
511 | if ($type instanceof Object_) { |
||||
512 | $fqcn = (string) $type->getFqsen(); |
||||
513 | $refClass = new ReflectionClass($fqcn); |
||||
514 | // Note : $refClass->isIterable() is only accessible in PHP 7.2 |
||||
515 | if ($refClass->implementsInterface(Iterator::class) || $refClass->implementsInterface(IteratorAggregate::class)) { |
||||
516 | $graphQlType = $this->mapIteratorDocBlockType($type, $docBlockType, $isNullable); |
||||
517 | } else { |
||||
518 | throw $e; |
||||
519 | } |
||||
520 | } else { |
||||
521 | throw $e; |
||||
522 | } |
||||
523 | } |
||||
524 | } |
||||
525 | |||||
526 | return $graphQlType; |
||||
527 | } |
||||
528 | |||||
529 | private function mapDocBlockType(Type $type, ?Type $docBlockType, bool $isNullable, bool $mapToInputType): GraphQLType |
||||
530 | { |
||||
531 | if ($docBlockType === null) { |
||||
532 | throw TypeMappingException::createFromType($type); |
||||
533 | } |
||||
534 | if (!$isNullable) { |
||||
535 | // Let's check a "null" value in the docblock |
||||
536 | $isNullable = $this->isNullable($docBlockType); |
||||
537 | } |
||||
538 | |||||
539 | $filteredDocBlockTypes = $this->typesWithoutNullable($docBlockType); |
||||
540 | if (empty($filteredDocBlockTypes)) { |
||||
541 | throw TypeMappingException::createFromType($type); |
||||
542 | } |
||||
543 | |||||
544 | $unionTypes = []; |
||||
545 | $lastException = null; |
||||
546 | foreach ($filteredDocBlockTypes as $singleDocBlockType) { |
||||
547 | try { |
||||
548 | $unionTypes[] = $this->toGraphQlType($this->dropNullableType($singleDocBlockType), null, $mapToInputType); |
||||
549 | } catch (TypeMappingException | CannotMapTypeExceptionInterface $e) { |
||||
550 | // We have several types. It is ok not to be able to match one. |
||||
551 | $lastException = $e; |
||||
552 | } |
||||
553 | } |
||||
554 | |||||
555 | if (empty($unionTypes) && $lastException !== null) { |
||||
556 | throw $lastException; |
||||
557 | } |
||||
558 | |||||
559 | if (count($unionTypes) === 1) { |
||||
560 | $graphQlType = $unionTypes[0]; |
||||
561 | } else { |
||||
562 | $graphQlType = new UnionType($unionTypes, $this->typeMapper); |
||||
563 | } |
||||
564 | |||||
565 | /* elseif (count($filteredDocBlockTypes) === 1) { |
||||
566 | $graphQlType = $this->toGraphQlType($filteredDocBlockTypes[0], $mapToInputType); |
||||
567 | } else { |
||||
568 | throw new GraphQLException('Union types are not supported (yet)'); |
||||
569 | //$graphQlTypes = array_map([$this, 'toGraphQlType'], $filteredDocBlockTypes); |
||||
570 | //$$graphQlType = new UnionType($graphQlTypes); |
||||
571 | }*/ |
||||
572 | |||||
573 | if (!$isNullable) { |
||||
574 | $graphQlType = GraphQLType::nonNull($graphQlType); |
||||
575 | } |
||||
576 | return $graphQlType; |
||||
577 | } |
||||
578 | |||||
579 | /** |
||||
580 | * Maps a type where the main PHP type is an iterator |
||||
581 | */ |
||||
582 | private function mapIteratorDocBlockType(Type $type, ?Type $docBlockType, bool $isNullable): GraphQLType |
||||
583 | { |
||||
584 | if ($docBlockType === null) { |
||||
585 | throw TypeMappingException::createFromType($type); |
||||
586 | } |
||||
587 | if (!$isNullable) { |
||||
588 | // Let's check a "null" value in the docblock |
||||
589 | $isNullable = $this->isNullable($docBlockType); |
||||
590 | } |
||||
591 | |||||
592 | $filteredDocBlockTypes = $this->typesWithoutNullable($docBlockType); |
||||
593 | if (empty($filteredDocBlockTypes)) { |
||||
594 | throw TypeMappingException::createFromType($type); |
||||
595 | } |
||||
596 | |||||
597 | $unionTypes = []; |
||||
598 | $lastException = null; |
||||
599 | foreach ($filteredDocBlockTypes as $singleDocBlockType) { |
||||
600 | try { |
||||
601 | $singleDocBlockType = $this->getTypeInArray($singleDocBlockType); |
||||
602 | if ($singleDocBlockType !== null) { |
||||
603 | $subGraphQlType = $this->toGraphQlType($singleDocBlockType, null, false); |
||||
604 | } else { |
||||
605 | $subGraphQlType = null; |
||||
606 | } |
||||
607 | |||||
608 | $unionTypes[] = $this->toGraphQlType($type, $subGraphQlType, false); |
||||
609 | |||||
610 | // TODO: add here a scan of the $type variable and do stuff if it is iterable. |
||||
611 | // TODO: remove the iterator type if specified in the docblock (@return Iterator|User[]) |
||||
612 | // TODO: check there is at least one array (User[]) |
||||
613 | } catch (TypeMappingException | CannotMapTypeExceptionInterface $e) { |
||||
614 | // We have several types. It is ok not to be able to match one. |
||||
615 | $lastException = $e; |
||||
616 | } |
||||
617 | } |
||||
618 | |||||
619 | if (empty($unionTypes) && $lastException !== null) { |
||||
620 | // We have an issue, let's try without the subType |
||||
621 | return $this->mapDocBlockType($type, $docBlockType, $isNullable, false); |
||||
622 | } |
||||
623 | |||||
624 | if (count($unionTypes) === 1) { |
||||
625 | $graphQlType = $unionTypes[0]; |
||||
626 | } else { |
||||
627 | $graphQlType = new UnionType($unionTypes, $this->typeMapper); |
||||
628 | } |
||||
629 | |||||
630 | if (!$isNullable) { |
||||
631 | $graphQlType = GraphQLType::nonNull($graphQlType); |
||||
632 | } |
||||
633 | return $graphQlType; |
||||
634 | } |
||||
635 | |||||
636 | /** |
||||
637 | * Casts a Type to a GraphQL type. |
||||
638 | * Does not deal with nullable. |
||||
639 | * |
||||
640 | * @param Type $type |
||||
641 | * @param GraphQLType|null $subType |
||||
642 | * @param bool $mapToInputType |
||||
643 | * @return GraphQLType (InputType&GraphQLType)|(OutputType&GraphQLType) |
||||
644 | * @throws CannotMapTypeExceptionInterface |
||||
645 | */ |
||||
646 | private function toGraphQlType(Type $type, ?GraphQLType $subType, bool $mapToInputType): GraphQLType |
||||
647 | { |
||||
648 | if ($type instanceof Integer) { |
||||
649 | return GraphQLType::int(); |
||||
650 | } elseif ($type instanceof String_) { |
||||
651 | return GraphQLType::string(); |
||||
652 | } elseif ($type instanceof Boolean) { |
||||
653 | return GraphQLType::boolean(); |
||||
654 | } elseif ($type instanceof Float_) { |
||||
655 | return GraphQLType::float(); |
||||
656 | } elseif ($type instanceof Object_) { |
||||
657 | $fqcn = (string) $type->getFqsen(); |
||||
658 | switch ($fqcn) { |
||||
659 | case '\\DateTimeImmutable': |
||||
660 | case '\\DateTimeInterface': |
||||
661 | return DateTimeType::getInstance(); |
||||
662 | case '\\'.UploadedFileInterface::class: |
||||
663 | return CustomTypesRegistry::getUploadType(); |
||||
664 | case '\\DateTime': |
||||
665 | throw new GraphQLException('Type-hinting a parameter against DateTime is not allowed. Please use the DateTimeImmutable type instead.'); |
||||
666 | case '\\'.ID::class: |
||||
667 | return GraphQLType::id(); |
||||
668 | default: |
||||
669 | $className = ltrim($type->getFqsen(), '\\'); |
||||
670 | if ($mapToInputType) { |
||||
671 | return $this->typeMapper->mapClassToInputType($className); |
||||
672 | } else { |
||||
673 | return $this->typeMapper->mapClassToInterfaceOrType($className, $subType); |
||||
0 ignored issues
–
show
It seems like
$subType can also be of type GraphQL\Type\Definition\Type ; however, parameter $subType of TheCodingMachine\GraphQL...lassToInterfaceOrType() does only seem to accept GraphQL\Type\Definition\OutputType|null , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
674 | } |
||||
675 | } |
||||
676 | } elseif ($type instanceof Array_) { |
||||
677 | return GraphQLType::listOf(GraphQLType::nonNull($this->toGraphQlType($type->getValueType(), $subType, $mapToInputType))); |
||||
678 | } else { |
||||
679 | throw TypeMappingException::createFromType($type); |
||||
680 | } |
||||
681 | } |
||||
682 | |||||
683 | /** |
||||
684 | * Removes "null" from the type (if it is compound). Return an array of types (not a Compound type). |
||||
685 | * |
||||
686 | * @param Type $docBlockTypeHint |
||||
687 | * @return array |
||||
688 | */ |
||||
689 | private function typesWithoutNullable(Type $docBlockTypeHint): array |
||||
690 | { |
||||
691 | if ($docBlockTypeHint instanceof Compound) { |
||||
692 | $docBlockTypeHints = \iterator_to_array($docBlockTypeHint); |
||||
693 | } else { |
||||
694 | $docBlockTypeHints = [$docBlockTypeHint]; |
||||
695 | } |
||||
696 | return array_filter($docBlockTypeHints, function ($item) { |
||||
697 | return !$item instanceof Null_; |
||||
698 | }); |
||||
699 | } |
||||
700 | |||||
701 | /** |
||||
702 | * Drops "Nullable" types and return the core type. |
||||
703 | * |
||||
704 | * @param Type $typeHint |
||||
705 | * @return Type |
||||
706 | */ |
||||
707 | private function dropNullableType(Type $typeHint): Type |
||||
708 | { |
||||
709 | if ($typeHint instanceof Nullable) { |
||||
710 | return $typeHint->getActualType(); |
||||
711 | } |
||||
712 | return $typeHint; |
||||
713 | } |
||||
714 | |||||
715 | /** |
||||
716 | * Resolves a list type. |
||||
717 | * |
||||
718 | * @param Type $typeHint |
||||
719 | * @return Type|null |
||||
720 | */ |
||||
721 | private function getTypeInArray(Type $typeHint): ?Type |
||||
722 | { |
||||
723 | $typeHint = $this->dropNullableType($typeHint); |
||||
724 | |||||
725 | if (!$typeHint instanceof Array_) { |
||||
726 | return null; |
||||
727 | } |
||||
728 | |||||
729 | return $this->dropNullableType($typeHint->getValueType()); |
||||
730 | } |
||||
731 | |||||
732 | /** |
||||
733 | * @param Type $docBlockTypeHint |
||||
734 | * @return bool |
||||
735 | */ |
||||
736 | private function isNullable(Type $docBlockTypeHint): bool |
||||
737 | { |
||||
738 | if ($docBlockTypeHint instanceof Null_) { |
||||
739 | return true; |
||||
740 | } |
||||
741 | if ($docBlockTypeHint instanceof Compound) { |
||||
742 | foreach ($docBlockTypeHint as $type) { |
||||
743 | if ($type instanceof Null_) { |
||||
744 | return true; |
||||
745 | } |
||||
746 | } |
||||
747 | } |
||||
748 | |||||
749 | return false; |
||||
750 | } |
||||
751 | |||||
752 | /** |
||||
753 | * Resolves "self" types into the class type. |
||||
754 | * |
||||
755 | * @param Type $type |
||||
756 | * @return Type |
||||
757 | */ |
||||
758 | private function resolveSelf(Type $type, ReflectionClass $reflectionClass): Type |
||||
759 | { |
||||
760 | if ($type instanceof Self_) { |
||||
761 | return new Object_(new Fqsen('\\'.$reflectionClass->getName())); |
||||
762 | } |
||||
763 | return $type; |
||||
764 | } |
||||
765 | } |
||||
766 |