Completed
Push — master ( 0eb5d9...ce204c )
by David
14s
created

FieldsBuilder::getFieldsByAnnotations()   C

Complexity

Conditions 12
Paths 31

Size

Total Lines 63
Code Lines 37

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 37
dl 0
loc 63
rs 6.9666
c 0
b 0
f 0
cc 12
nc 31
nop 3

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\NonNull;
9
use GraphQL\Type\Definition\OutputType;
10
use GraphQL\Upload\UploadType;
11
use phpDocumentor\Reflection\Fqsen;
12
use phpDocumentor\Reflection\Types\Nullable;
13
use phpDocumentor\Reflection\Types\Self_;
14
use Psr\Http\Message\UploadedFileInterface;
15
use ReflectionMethod;
16
use TheCodingMachine\GraphQL\Controllers\Hydrators\HydratorInterface;
17
use TheCodingMachine\GraphQL\Controllers\Reflection\CachedDocBlockFactory;
18
use TheCodingMachine\GraphQL\Controllers\Types\CustomTypesRegistry;
19
use TheCodingMachine\GraphQL\Controllers\Types\TypeResolver;
20
use TheCodingMachine\GraphQL\Controllers\Types\UnionType;
21
use Iterator;
22
use IteratorAggregate;
23
use phpDocumentor\Reflection\DocBlock;
24
use phpDocumentor\Reflection\DocBlock\Tags\Return_;
25
use phpDocumentor\Reflection\Type;
26
use phpDocumentor\Reflection\Types\Array_;
27
use phpDocumentor\Reflection\Types\Boolean;
28
use phpDocumentor\Reflection\Types\Compound;
29
use phpDocumentor\Reflection\Types\Float_;
30
use phpDocumentor\Reflection\Types\Iterable_;
31
use phpDocumentor\Reflection\Types\Mixed_;
32
use phpDocumentor\Reflection\Types\Null_;
33
use phpDocumentor\Reflection\Types\Object_;
34
use phpDocumentor\Reflection\Types\String_;
35
use phpDocumentor\Reflection\Types\Integer;
36
use ReflectionClass;
37
use TheCodingMachine\GraphQL\Controllers\Annotations\SourceField;
38
use TheCodingMachine\GraphQL\Controllers\Annotations\Logged;
39
use TheCodingMachine\GraphQL\Controllers\Annotations\Mutation;
40
use TheCodingMachine\GraphQL\Controllers\Annotations\Query;
41
use TheCodingMachine\GraphQL\Controllers\Mappers\CannotMapTypeException;
42
use TheCodingMachine\GraphQL\Controllers\Mappers\RecursiveTypeMapperInterface;
43
use TheCodingMachine\GraphQL\Controllers\Reflection\CommentParser;
0 ignored issues
show
Bug introduced by
The type TheCodingMachine\GraphQL...eflection\CommentParser was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
44
use TheCodingMachine\GraphQL\Controllers\Security\AuthenticationServiceInterface;
45
use TheCodingMachine\GraphQL\Controllers\Security\AuthorizationServiceInterface;
46
use TheCodingMachine\GraphQL\Controllers\Types\DateTimeType;
47
use GraphQL\Type\Definition\Type as GraphQLType;
48
49
/**
50
 * A class in charge if returning list of fields for queries / mutations / entities / input types
51
 */
52
class FieldsBuilder
53
{
54
    /**
55
     * @var AnnotationReader
56
     */
57
    private $annotationReader;
58
    /**
59
     * @var RecursiveTypeMapperInterface
60
     */
61
    private $typeMapper;
62
    /**
63
     * @var HydratorInterface
64
     */
65
    private $hydrator;
66
    /**
67
     * @var AuthenticationServiceInterface
68
     */
69
    private $authenticationService;
70
    /**
71
     * @var AuthorizationServiceInterface
72
     */
73
    private $authorizationService;
74
    /**
75
     * @var CachedDocBlockFactory
76
     */
77
    private $cachedDocBlockFactory;
78
    /**
79
     * @var TypeResolver
80
     */
81
    private $typeResolver;
82
83
    /**
84
     * @param AnnotationReader $annotationReader
85
     * @param RecursiveTypeMapperInterface $typeMapper
86
     * @param HydratorInterface $hydrator
87
     * @param AuthenticationServiceInterface $authenticationService
88
     * @param AuthorizationServiceInterface $authorizationService
89
     */
90
    public function __construct(AnnotationReader $annotationReader, RecursiveTypeMapperInterface $typeMapper,
91
                                HydratorInterface $hydrator, AuthenticationServiceInterface $authenticationService,
92
                                AuthorizationServiceInterface $authorizationService, TypeResolver $typeResolver,
93
                                CachedDocBlockFactory $cachedDocBlockFactory)
94
    {
95
        $this->annotationReader = $annotationReader;
96
        $this->typeMapper = $typeMapper;
97
        $this->hydrator = $hydrator;
98
        $this->authenticationService = $authenticationService;
99
        $this->authorizationService = $authorizationService;
100
        $this->typeResolver = $typeResolver;
101
        $this->cachedDocBlockFactory = $cachedDocBlockFactory;
102
    }
103
104
    // TODO: Add RecursiveTypeMapper in the list of parameters for getQueries and REMOVE the ControllerQueryProviderFactory.
105
106
    /**
107
     * @param object $controller
108
     * @return QueryField[]
109
     * @throws \ReflectionException
110
     */
111
    public function getQueries($controller): array
112
    {
113
        return $this->getFieldsByAnnotations($controller,Query::class, false);
114
    }
115
116
    /**
117
     * @param object $controller
118
     * @return QueryField[]
119
     * @throws \ReflectionException
120
     */
121
    public function getMutations($controller): array
122
    {
123
        return $this->getFieldsByAnnotations($controller,Mutation::class, false);
124
    }
125
126
    /**
127
     * @return array<string, QueryField> QueryField indexed by name.
128
     */
129
    public function getFields($controller): array
130
    {
131
        $fieldAnnotations = $this->getFieldsByAnnotations($controller, Annotations\Field::class, true);
132
        $sourceFields = $this->getSourceFields($controller);
133
134
        $fields = [];
135
        foreach ($fieldAnnotations as $field) {
136
            $fields[$field->name] = $field;
137
        }
138
        foreach ($sourceFields as $field) {
139
            $fields[$field->name] = $field;
140
        }
141
142
        return $fields;
143
    }
144
145
    /**
146
     * @param ReflectionMethod $refMethod A method annotated with a Factory annotation.
147
     * @return array<string, array<int, mixed>> Returns an array of fields as accepted by the InputObjectType constructor.
148
     */
149
    public function getInputFields(ReflectionMethod $refMethod): array
150
    {
151
        $docBlockObj = $this->cachedDocBlockFactory->getDocBlock($refMethod);
152
        //$docBlockComment = $docBlockObj->getSummary()."\n".$docBlockObj->getDescription()->render();
153
154
        $parameters = $refMethod->getParameters();
155
156
        $args = $this->mapParameters($parameters, $docBlockObj);
157
158
        return $args;
159
    }
160
161
    /**
162
     * @param object $controller
163
     * @param string $annotationName
164
     * @param bool $injectSource Whether to inject the source object or not as the first argument. True for @Field, false for @Query and @Mutation
165
     * @return QueryField[]
166
     * @throws CannotMapTypeException
167
     * @throws \ReflectionException
168
     */
169
    private function getFieldsByAnnotations($controller, string $annotationName, bool $injectSource): array
170
    {
171
        $refClass = new \ReflectionClass($controller);
172
173
        $queryList = [];
174
175
        $oldDeclaringClass = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $oldDeclaringClass is dead and can be removed.
Loading history...
176
        $context = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $context is dead and can be removed.
Loading history...
177
178
        foreach ($refClass->getMethods() as $refMethod) {
179
            // First, let's check the "Query" or "Mutation" or "Field" annotation
180
            $queryAnnotation = $this->annotationReader->getRequestAnnotation($refMethod, $annotationName);
181
182
            if ($queryAnnotation !== null) {
183
                $unauthorized = false;
184
                if (!$this->isAuthorized($refMethod)) {
185
                    $failWith = $this->annotationReader->getFailWithAnnotation($refMethod);
186
                    if ($failWith === null) {
187
                        continue;
188
                    }
189
                    $unauthorized = true;
190
                }
191
192
                $docBlockObj = $this->cachedDocBlockFactory->getDocBlock($refMethod);
193
                $docBlockComment = $docBlockObj->getSummary()."\n".$docBlockObj->getDescription()->render();
194
195
                $methodName = $refMethod->getName();
196
                $name = $queryAnnotation->getName() ?: $methodName;
197
198
                $parameters = $refMethod->getParameters();
199
                if ($injectSource === true) {
200
                    $first_parameter = array_shift($parameters);
0 ignored issues
show
Unused Code introduced by
The assignment to $first_parameter is dead and can be removed.
Loading history...
201
                    // TODO: check that $first_parameter type is correct.
202
                }
203
204
                $args = $this->mapParameters($parameters, $docBlockObj);
205
206
                if ($queryAnnotation->getOutputType()) {
207
                    $type = $this->typeResolver->mapNameToType($queryAnnotation->getOutputType());
208
                    if (!$type instanceof OutputType) {
209
                        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()));
210
                    }
211
                } else {
212
                    $type = $this->mapReturnType($refMethod, $docBlockObj);
213
                }
214
215
                if (!$unauthorized) {
216
                    $callable = [$controller, $methodName];
217
                } else {
218
                    $failWithValue = $failWith->getValue();
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $failWith does not seem to be defined for all execution paths leading up to this point.
Loading history...
219
                    $callable = function() use ($failWithValue) {
220
                        return $failWithValue;
221
                    };
222
                    if ($failWithValue === null && $type instanceof NonNull) {
223
                        $type = $type->getWrappedType();
224
                    }
225
                }
226
227
                $queryList[] = new QueryField($name, $type, $args, $callable, null, $this->hydrator, $docBlockComment, $injectSource);
0 ignored issues
show
Bug introduced by
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 ignore-type  annotation

227
                $queryList[] = new QueryField($name, /** @scrutinizer ignore-type */ $type, $args, $callable, null, $this->hydrator, $docBlockComment, $injectSource);
Loading history...
228
            }
229
        }
230
231
        return $queryList;
232
    }
233
234
    /**
235
     * @return GraphQLType&OutputType
236
     */
237
    private function mapReturnType(ReflectionMethod $refMethod, DocBlock $docBlockObj): GraphQLType
238
    {
239
        $returnType = $refMethod->getReturnType();
240
        if ($returnType !== null) {
241
            $typeResolver = new \phpDocumentor\Reflection\TypeResolver();
242
            $phpdocType = $typeResolver->resolve((string) $returnType);
243
            $phpdocType = $this->resolveSelf($phpdocType, $refMethod->getDeclaringClass());
244
        } else {
245
            $phpdocType = new Mixed_();
246
        }
247
248
        $docBlockReturnType = $this->getDocBlocReturnType($docBlockObj, $refMethod);
249
250
        try {
251
            /** @var GraphQLType&OutputType $type */
252
            $type = $this->mapType($phpdocType, $docBlockReturnType, $returnType ? $returnType->allowsNull() : false, false);
253
        } catch (TypeMappingException $e) {
254
            throw TypeMappingException::wrapWithReturnInfo($e, $refMethod);
255
        } catch (CannotMapTypeException $e) {
256
            throw CannotMapTypeException::wrapWithReturnInfo($e, $refMethod);
257
        }
258
        return $type;
259
    }
260
261
    private function getDocBlocReturnType(DocBlock $docBlock, \ReflectionMethod $refMethod): ?Type
262
    {
263
        /** @var Return_[] $returnTypeTags */
264
        $returnTypeTags = $docBlock->getTagsByName('return');
265
        if (count($returnTypeTags) > 1) {
266
            throw InvalidDocBlockException::tooManyReturnTags($refMethod);
267
        }
268
        $docBlockReturnType = null;
269
        if (isset($returnTypeTags[0])) {
270
            $docBlockReturnType = $returnTypeTags[0]->getType();
271
        }
272
        return $docBlockReturnType;
273
    }
274
275
    /**
276
     * @param object $controller
277
     * @return QueryField[]
278
     * @throws CannotMapTypeException
279
     * @throws \ReflectionException
280
     */
281
    private function getSourceFields($controller): array
282
    {
283
        $refClass = new \ReflectionClass($controller);
284
285
        /** @var SourceField[] $sourceFields */
286
        $sourceFields = $this->annotationReader->getSourceFields($refClass);
287
288
        if ($controller instanceof FromSourceFieldsInterface) {
289
            $sourceFields = array_merge($sourceFields, $controller->getSourceFields());
290
        }
291
292
        if (empty($sourceFields)) {
293
            return [];
294
        }
295
296
        $typeField = $this->annotationReader->getTypeAnnotation($refClass);
297
298
        if ($typeField === null) {
299
            throw MissingAnnotationException::missingTypeExceptionToUseSourceField();
300
        }
301
302
        $objectClass = $typeField->getClass();
303
        $objectRefClass = new \ReflectionClass($objectClass);
304
305
        $oldDeclaringClass = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $oldDeclaringClass is dead and can be removed.
Loading history...
306
        $context = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $context is dead and can be removed.
Loading history...
307
        $queryList = [];
308
309
        foreach ($sourceFields as $sourceField) {
310
            // Ignore the field if we must be logged.
311
            $right = $sourceField->getRight();
312
            $unauthorized = false;
313
            if (($sourceField->isLogged() && !$this->authenticationService->isLogged())
314
                || ($right !== null && !$this->authorizationService->isAllowed($right->getName()))) {
315
                if (!$sourceField->canFailWith()) {
316
                    continue;
317
                } else {
318
                    $unauthorized = true;
319
                }
320
            }
321
322
            try {
323
                $refMethod = $this->getMethodFromPropertyName($objectRefClass, $sourceField->getName());
324
            } catch (FieldNotFoundException $e) {
325
                throw FieldNotFoundException::wrapWithCallerInfo($e, $refClass->getName());
326
            }
327
328
            $methodName = $refMethod->getName();
329
330
331
            $docBlockObj = $this->cachedDocBlockFactory->getDocBlock($refMethod);
332
            $docBlockComment = $docBlockObj->getSummary()."\n".$docBlockObj->getDescription()->render();
333
334
335
            $args = $this->mapParameters($refMethod->getParameters(), $docBlockObj);
336
337
            if ($sourceField->isId()) {
338
                $type = GraphQLType::id();
339
                if (!$refMethod->getReturnType()->allowsNull()) {
340
                    $type = GraphQLType::nonNull($type);
341
                }
342
            } elseif ($sourceField->getOutputType()) {
343
                $type = $this->typeResolver->mapNameToType($sourceField->getOutputType());
344
            } else {
345
                $type = $this->mapReturnType($refMethod, $docBlockObj);
346
            }
347
348
            if (!$unauthorized) {
349
                $queryList[] = new QueryField($sourceField->getName(), $type, $args, null, $methodName, $this->hydrator, $docBlockComment, false);
0 ignored issues
show
Bug introduced by
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 ignore-type  annotation

349
                $queryList[] = new QueryField($sourceField->getName(), /** @scrutinizer ignore-type */ $type, $args, null, $methodName, $this->hydrator, $docBlockComment, false);
Loading history...
350
            } else {
351
                $failWithValue = $sourceField->getFailWith();
352
                $callable = function() use ($failWithValue) {
353
                    return $failWithValue;
354
                };
355
                if ($failWithValue === null && $type instanceof NonNull) {
356
                    $type = $type->getWrappedType();
357
                }
358
                $queryList[] = new QueryField($sourceField->getName(), $type, $args, $callable, null, $this->hydrator, $docBlockComment, false);
0 ignored issues
show
Bug introduced by
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 ignore-type  annotation

358
                $queryList[] = new QueryField($sourceField->getName(), /** @scrutinizer ignore-type */ $type, $args, $callable, null, $this->hydrator, $docBlockComment, false);
Loading history...
359
            }
360
361
        }
362
        return $queryList;
363
    }
364
365
    private function getMethodFromPropertyName(\ReflectionClass $reflectionClass, string $propertyName): \ReflectionMethod
366
    {
367
        if ($reflectionClass->hasMethod($propertyName)) {
368
            $methodName = $propertyName;
369
        } else {
370
            $upperCasePropertyName = \ucfirst($propertyName);
371
            if ($reflectionClass->hasMethod('get'.$upperCasePropertyName)) {
372
                $methodName = 'get'.$upperCasePropertyName;
373
            } elseif ($reflectionClass->hasMethod('is'.$upperCasePropertyName)) {
374
                $methodName = 'is'.$upperCasePropertyName;
375
            } else {
376
                throw FieldNotFoundException::missingField($reflectionClass->getName(), $propertyName);
377
            }
378
        }
379
380
        return $reflectionClass->getMethod($methodName);
381
    }
382
383
    /**
384
     * Checks the @Logged and @Right annotations.
385
     *
386
     * @param \ReflectionMethod $reflectionMethod
387
     * @return bool
388
     */
389
    private function isAuthorized(\ReflectionMethod $reflectionMethod) : bool
390
    {
391
        $loggedAnnotation = $this->annotationReader->getLoggedAnnotation($reflectionMethod);
392
393
        if ($loggedAnnotation !== null && !$this->authenticationService->isLogged()) {
394
            return false;
395
        }
396
397
398
        $rightAnnotation = $this->annotationReader->getRightAnnotation($reflectionMethod);
399
400
        if ($rightAnnotation !== null && !$this->authorizationService->isAllowed($rightAnnotation->getName())) {
401
            return false;
402
        }
403
404
        return true;
405
    }
406
407
    /**
408
     * Note: there is a bug in $refMethod->allowsNull that forces us to use $standardRefMethod->allowsNull instead.
409
     *
410
     * @param \ReflectionParameter[] $refParameters
411
     * @return array[] An array of ['type'=>Type, 'defaultValue'=>val]
412
     * @throws MissingTypeHintException
413
     */
414
    private function mapParameters(array $refParameters, DocBlock $docBlock): array
415
    {
416
        $args = [];
417
418
        $typeResolver = new \phpDocumentor\Reflection\TypeResolver();
419
420
        foreach ($refParameters as $parameter) {
421
            $parameterType = $parameter->getType();
422
            $allowsNull = $parameterType === null ? true : $parameterType->allowsNull();
423
424
            $type = (string) $parameterType;
425
            if ($type === '') {
426
                throw MissingTypeHintException::missingTypeHint($parameter);
427
            }
428
            $phpdocType = $typeResolver->resolve($type);
429
            $phpdocType = $this->resolveSelf($phpdocType, $parameter->getDeclaringClass());
430
431
            /** @var DocBlock\Tags\Param[] $paramTags */
432
            $paramTags = $docBlock->getTagsByName('param');
433
            $docBlockType = null;
434
            foreach ($paramTags as $paramTag) {
435
                if ($paramTag->getVariableName() === $parameter->getName()) {
436
                    $docBlockType = $paramTag->getType();
437
                    break;
438
                }
439
            }
440
441
            try {
442
                $arr = [
443
                    'type' => $this->mapType($phpdocType, $docBlockType, $allowsNull || $parameter->isDefaultValueAvailable(), true),
444
                ];
445
            } catch (TypeMappingException $e) {
446
                throw TypeMappingException::wrapWithParamInfo($e, $parameter);
447
            } catch (CannotMapTypeException $e) {
448
                throw CannotMapTypeException::wrapWithParamInfo($e, $parameter);
449
            }
450
451
            if ($parameter->allowsNull()) {
452
                $arr['defaultValue'] = null;
453
            }
454
            if ($parameter->isDefaultValueAvailable()) {
455
                $arr['defaultValue'] = $parameter->getDefaultValue();
456
            }
457
458
            $args[$parameter->getName()] = $arr;
459
        }
460
461
        return $args;
462
    }
463
464
    /**
465
     * @param Type $type
466
     * @param Type|null $docBlockType
467
     * @return GraphQLType
468
     */
469
    private function mapType(Type $type, ?Type $docBlockType, bool $isNullable, bool $mapToInputType): GraphQLType
470
    {
471
        $graphQlType = null;
472
473
        if ($type instanceof Array_ || $type instanceof Iterable_ || $type instanceof Mixed_) {
474
            $graphQlType = $this->mapDocBlockType($type, $docBlockType, $isNullable, $mapToInputType);
475
        } else {
476
            try {
477
                $graphQlType = $this->toGraphQlType($type, $mapToInputType);
478
                if (!$isNullable) {
479
                    $graphQlType = GraphQLType::nonNull($graphQlType);
480
                }
481
            } catch (TypeMappingException | CannotMapTypeException $e) {
482
                // Is the type iterable? If yes, let's analyze the docblock
483
                // TODO: it would be better not to go through an exception for this.
484
                if ($type instanceof Object_) {
485
                    $fqcn = (string) $type->getFqsen();
486
                    $refClass = new ReflectionClass($fqcn);
487
                    // Note : $refClass->isIterable() is only accessible in PHP 7.2
488
                    if ($refClass->implementsInterface(Iterator::class) || $refClass->implementsInterface(IteratorAggregate::class)) {
489
                        $graphQlType = $this->mapDocBlockType($type, $docBlockType, $isNullable, $mapToInputType);
490
                    } else {
491
                        throw $e;
492
                    }
493
                } else {
494
                    throw $e;
495
                }
496
            }
497
        }
498
499
500
        return $graphQlType;
501
    }
502
503
    private function mapDocBlockType(Type $type, ?Type $docBlockType, bool $isNullable, bool $mapToInputType): GraphQLType
504
    {
505
        if ($docBlockType === null) {
506
            throw TypeMappingException::createFromType($type);
507
        }
508
        if (!$isNullable) {
509
            // Let's check a "null" value in the docblock
510
            $isNullable = $this->isNullable($docBlockType);
511
        }
512
513
        $filteredDocBlockTypes = $this->typesWithoutNullable($docBlockType);
514
        if (empty($filteredDocBlockTypes)) {
515
            throw TypeMappingException::createFromType($type);
516
        }
517
518
        $unionTypes = [];
519
        $lastException = null;
520
        foreach ($filteredDocBlockTypes as $singleDocBlockType) {
521
            try {
522
                $unionTypes[] = $this->toGraphQlType($this->dropNullableType($singleDocBlockType), $mapToInputType);
523
            } catch (TypeMappingException | CannotMapTypeException $e) {
524
                // We have several types. It is ok not to be able to match one.
525
                $lastException = $e;
526
            }
527
        }
528
529
        if (empty($unionTypes) && $lastException !== null) {
530
            throw $lastException;
531
        }
532
533
        if (count($unionTypes) === 1) {
534
            $graphQlType = $unionTypes[0];
535
        } else {
536
            $graphQlType = new UnionType($unionTypes, $this->typeMapper);
537
        }
538
539
        /* elseif (count($filteredDocBlockTypes) === 1) {
540
            $graphQlType = $this->toGraphQlType($filteredDocBlockTypes[0], $mapToInputType);
541
        } else {
542
            throw new GraphQLException('Union types are not supported (yet)');
543
            //$graphQlTypes = array_map([$this, 'toGraphQlType'], $filteredDocBlockTypes);
544
            //$$graphQlType = new UnionType($graphQlTypes);
545
        }*/
546
547
        if (!$isNullable) {
548
            $graphQlType = GraphQLType::nonNull($graphQlType);
549
        }
550
        return $graphQlType;
551
    }
552
553
    /**
554
     * Casts a Type to a GraphQL type.
555
     * Does not deal with nullable.
556
     *
557
     * @param Type $type
558
     * @param bool $mapToInputType
559
     * @return (InputType&GraphQLType)|(OutputType&GraphQLType)
560
     */
561
    private function toGraphQlType(Type $type, bool $mapToInputType): GraphQLType
562
    {
563
        if ($type instanceof Integer) {
564
            return GraphQLType::int();
0 ignored issues
show
Bug Best Practice introduced by
The expression return GraphQL\Type\Definition\Type::int() returns the type GraphQL\Type\Definition\IntType which is incompatible with the documented return type GraphQL\Type\Definition\...QL\Type\Definition\Type.
Loading history...
565
        } elseif ($type instanceof String_) {
566
            return GraphQLType::string();
0 ignored issues
show
Bug Best Practice introduced by
The expression return GraphQL\Type\Definition\Type::string() returns the type GraphQL\Type\Definition\StringType which is incompatible with the documented return type GraphQL\Type\Definition\...QL\Type\Definition\Type.
Loading history...
567
        } elseif ($type instanceof Boolean) {
568
            return GraphQLType::boolean();
0 ignored issues
show
Bug Best Practice introduced by
The expression return GraphQL\Type\Definition\Type::boolean() returns the type GraphQL\Type\Definition\BooleanType which is incompatible with the documented return type GraphQL\Type\Definition\...QL\Type\Definition\Type.
Loading history...
569
        } elseif ($type instanceof Float_) {
570
            return GraphQLType::float();
0 ignored issues
show
Bug Best Practice introduced by
The expression return GraphQL\Type\Definition\Type::float() returns the type GraphQL\Type\Definition\FloatType which is incompatible with the documented return type GraphQL\Type\Definition\...QL\Type\Definition\Type.
Loading history...
571
        } elseif ($type instanceof Object_) {
572
            $fqcn = (string) $type->getFqsen();
573
            if ($fqcn === '\\DateTimeImmutable' || $fqcn === '\\DateTimeInterface') {
574
                return DateTimeType::getInstance();
0 ignored issues
show
Bug Best Practice introduced by
The expression return TheCodingMachine\...TimeType::getInstance() returns the type TheCodingMachine\GraphQL...lers\Types\DateTimeType which is incompatible with the documented return type GraphQL\Type\Definition\...QL\Type\Definition\Type.
Loading history...
575
            } elseif ($fqcn === '\\'.UploadedFileInterface::class) {
576
                return CustomTypesRegistry::getUploadType();
0 ignored issues
show
Bug Best Practice introduced by
The expression return TheCodingMachine\...gistry::getUploadType() returns the type GraphQL\Upload\UploadType which is incompatible with the documented return type GraphQL\Type\Definition\...QL\Type\Definition\Type.
Loading history...
577
            } elseif ($fqcn === '\\DateTime') {
578
                throw new GraphQLException('Type-hinting a parameter against DateTime is not allowed. Please use the DateTimeImmutable type instead.');
579
            }
580
581
            $className = ltrim($type->getFqsen(), '\\');
582
            if ($mapToInputType) {
583
                return $this->typeMapper->mapClassToInputType($className);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->typeMapper...ToInputType($className) returns the type GraphQL\Type\Definition\InputObjectType which is incompatible with the documented return type GraphQL\Type\Definition\...QL\Type\Definition\Type.
Loading history...
584
            } else {
585
                return $this->typeMapper->mapClassToInterfaceOrType($className);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->typeMapper...rfaceOrType($className) returns the type GraphQL\Type\Definition\OutputType which is incompatible with the type-hinted return GraphQL\Type\Definition\Type.
Loading history...
586
            }
587
        } elseif ($type instanceof Array_) {
588
            return GraphQLType::listOf(GraphQLType::nonNull($this->toGraphQlType($type->getValueType(), $mapToInputType)));
0 ignored issues
show
Bug Best Practice introduced by
The expression return GraphQL\Type\Defi...e(), $mapToInputType))) returns the type GraphQL\Type\Definition\ListOfType which is incompatible with the documented return type GraphQL\Type\Definition\...QL\Type\Definition\Type.
Loading history...
589
        } else {
590
            throw TypeMappingException::createFromType($type);
591
        }
592
    }
593
594
    /**
595
     * Removes "null" from the type (if it is compound). Return an array of types (not a Compound type).
596
     *
597
     * @param Type $docBlockTypeHint
598
     * @return array
599
     */
600
    private function typesWithoutNullable(Type $docBlockTypeHint): array
601
    {
602
        if ($docBlockTypeHint instanceof Compound) {
603
            $docBlockTypeHints = \iterator_to_array($docBlockTypeHint);
604
        } else {
605
            $docBlockTypeHints = [$docBlockTypeHint];
606
        }
607
        return array_filter($docBlockTypeHints, function ($item) {
608
            return !$item instanceof Null_;
609
        });
610
    }
611
612
    /**
613
     * Drops "Nullable" types and return the core type.
614
     *
615
     * @param Type $typeHint
616
     * @return Type
617
     */
618
    private function dropNullableType(Type $typeHint): Type
619
    {
620
        if ($typeHint instanceof Nullable) {
621
            return $typeHint->getActualType();
622
        }
623
        return $typeHint;
624
    }
625
626
    /**
627
     * @param Type $docBlockTypeHint
628
     * @return bool
629
     */
630
    private function isNullable(Type $docBlockTypeHint): bool
631
    {
632
        if ($docBlockTypeHint instanceof Null_) {
633
            return true;
634
        }
635
        if ($docBlockTypeHint instanceof Compound) {
636
            foreach ($docBlockTypeHint as $type) {
637
                if ($type instanceof Null_) {
638
                    return true;
639
                }
640
            }
641
        }
642
643
        return false;
644
    }
645
646
    /**
647
     * Resolves "self" types into the class type.
648
     *
649
     * @param Type $type
650
     * @return Type
651
     */
652
    private function resolveSelf(Type $type, ReflectionClass $reflectionClass): Type
653
    {
654
        if ($type instanceof Self_) {
655
            return new Object_(new Fqsen('\\'.$reflectionClass->getName()));
656
        }
657
        return $type;
658
    }
659
}
660