Completed
Push — master ( ab2a5d...d6b376 )
by David
17s
created

FieldsBuilder   F

Complexity

Total Complexity 97

Size/Duplication

Total Lines 575
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 97
eloc 244
dl 0
loc 575
rs 2
c 0
b 0
f 0

19 Methods

Rating   Name   Duplication   Size   Complexity  
A isAuthorized() 0 16 5
A resolveSelf() 0 6 2
A __construct() 0 12 1
A getDocBlocReturnType() 0 12 3
A isNullable() 0 14 5
A getInputFields() 0 10 1
A mapReturnType() 0 22 5
B mapType() 0 32 9
B mapDocBlockType() 0 48 10
B getFieldsByAnnotations() 0 46 8
B mapParameters() 0 48 11
A getMutations() 0 3 1
C getSourceFields() 0 69 13
A typesWithoutNullable() 0 9 2
A dropNullableType() 0 6 2
A getMethodFromPropertyName() 0 16 4
A getFields() 0 14 3
A getQueries() 0 3 1
B toGraphQlType() 0 28 11

How to fix   Complexity   

Complex Class

Complex classes like FieldsBuilder often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use FieldsBuilder, and based on these observations, apply Extract Interface, too.

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\OutputType;
9
use phpDocumentor\Reflection\Fqsen;
10
use phpDocumentor\Reflection\Types\Nullable;
11
use phpDocumentor\Reflection\Types\Self_;
12
use Psr\Container\ContainerInterface;
13
use ReflectionMethod;
14
use TheCodingMachine\GraphQL\Controllers\Hydrators\HydratorInterface;
15
use TheCodingMachine\GraphQL\Controllers\Reflection\CachedDocBlockFactory;
16
use TheCodingMachine\GraphQL\Controllers\Types\UnionType;
17
use Iterator;
18
use IteratorAggregate;
19
use phpDocumentor\Reflection\DocBlock;
20
use phpDocumentor\Reflection\DocBlock\Tags\Return_;
21
use phpDocumentor\Reflection\Type;
22
use phpDocumentor\Reflection\Types\Array_;
23
use phpDocumentor\Reflection\Types\Boolean;
24
use phpDocumentor\Reflection\Types\Compound;
25
use phpDocumentor\Reflection\Types\Float_;
26
use phpDocumentor\Reflection\Types\Iterable_;
27
use phpDocumentor\Reflection\Types\Mixed_;
28
use phpDocumentor\Reflection\Types\Null_;
29
use phpDocumentor\Reflection\Types\Object_;
30
use phpDocumentor\Reflection\Types\String_;
31
use phpDocumentor\Reflection\Types\Integer;
32
use ReflectionClass;
33
use TheCodingMachine\GraphQL\Controllers\Annotations\SourceField;
34
use TheCodingMachine\GraphQL\Controllers\Annotations\Logged;
35
use TheCodingMachine\GraphQL\Controllers\Annotations\Mutation;
36
use TheCodingMachine\GraphQL\Controllers\Annotations\Query;
37
use TheCodingMachine\GraphQL\Controllers\Mappers\CannotMapTypeException;
38
use TheCodingMachine\GraphQL\Controllers\Mappers\RecursiveTypeMapperInterface;
39
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...
40
use TheCodingMachine\GraphQL\Controllers\Security\AuthenticationServiceInterface;
41
use TheCodingMachine\GraphQL\Controllers\Security\AuthorizationServiceInterface;
42
use TheCodingMachine\GraphQL\Controllers\Types\DateTimeType;
43
use GraphQL\Type\Definition\Type as GraphQLType;
44
45
/**
46
 * A class in charge if returning list of fields for queries / mutations / entities / input types
47
 */
48
class FieldsBuilder
49
{
50
    /**
51
     * @var AnnotationReader
52
     */
53
    private $annotationReader;
54
    /**
55
     * @var RecursiveTypeMapperInterface
56
     */
57
    private $typeMapper;
58
    /**
59
     * @var HydratorInterface
60
     */
61
    private $hydrator;
62
    /**
63
     * @var AuthenticationServiceInterface
64
     */
65
    private $authenticationService;
66
    /**
67
     * @var AuthorizationServiceInterface
68
     */
69
    private $authorizationService;
70
    /**
71
     * @var ContainerInterface
72
     */
73
    private $registry;
74
    /**
75
     * @var CachedDocBlockFactory
76
     */
77
    private $cachedDocBlockFactory;
78
79
    /**
80
     * @param AnnotationReader $annotationReader
81
     * @param RecursiveTypeMapperInterface $typeMapper
82
     * @param HydratorInterface $hydrator
83
     * @param AuthenticationServiceInterface $authenticationService
84
     * @param AuthorizationServiceInterface $authorizationService
85
     * @param ContainerInterface $registry The registry is used to fetch custom return types by container identifier (using the returnType parameter of the Type annotation)
86
     */
87
    public function __construct(AnnotationReader $annotationReader, RecursiveTypeMapperInterface $typeMapper,
88
                                HydratorInterface $hydrator, AuthenticationServiceInterface $authenticationService,
89
                                AuthorizationServiceInterface $authorizationService, ContainerInterface $registry,
90
                                CachedDocBlockFactory $cachedDocBlockFactory)
91
    {
92
        $this->annotationReader = $annotationReader;
93
        $this->typeMapper = $typeMapper;
94
        $this->hydrator = $hydrator;
95
        $this->authenticationService = $authenticationService;
96
        $this->authorizationService = $authorizationService;
97
        $this->registry = $registry;
98
        $this->cachedDocBlockFactory = $cachedDocBlockFactory;
99
    }
100
101
    // TODO: Add RecursiveTypeMapper in the list of parameters for getQueries and REMOVE the ControllerQueryProviderFactory.
102
103
    /**
104
     * @param object $controller
105
     * @return QueryField[]
106
     * @throws \ReflectionException
107
     */
108
    public function getQueries($controller): array
109
    {
110
        return $this->getFieldsByAnnotations($controller,Query::class, false);
111
    }
112
113
    /**
114
     * @param object $controller
115
     * @return QueryField[]
116
     * @throws \ReflectionException
117
     */
118
    public function getMutations($controller): array
119
    {
120
        return $this->getFieldsByAnnotations($controller,Mutation::class, false);
121
    }
122
123
    /**
124
     * @return array<string, QueryField> QueryField indexed by name.
125
     */
126
    public function getFields($controller): array
127
    {
128
        $fieldAnnotations = $this->getFieldsByAnnotations($controller, Annotations\Field::class, true);
129
        $sourceFields = $this->getSourceFields($controller);
130
131
        $fields = [];
132
        foreach ($fieldAnnotations as $field) {
133
            $fields[$field->name] = $field;
134
        }
135
        foreach ($sourceFields as $field) {
136
            $fields[$field->name] = $field;
137
        }
138
139
        return $fields;
140
    }
141
142
    /**
143
     * @param ReflectionMethod $refMethod A method annotated with a Factory annotation.
144
     * @return array<string, array<int, mixed>> Returns an array of fields as accepted by the InputObjectType constructor.
145
     */
146
    public function getInputFields(ReflectionMethod $refMethod): array
147
    {
148
        $docBlockObj = $this->cachedDocBlockFactory->getDocBlock($refMethod);
149
        //$docBlockComment = $docBlockObj->getSummary()."\n".$docBlockObj->getDescription()->render();
150
151
        $parameters = $refMethod->getParameters();
152
153
        $args = $this->mapParameters($parameters, $docBlockObj);
154
155
        return $args;
156
    }
157
158
    /**
159
     * @param object $controller
160
     * @param string $annotationName
161
     * @param bool $injectSource Whether to inject the source object or not as the first argument. True for @Field, false for @Query and @Mutation
162
     * @return QueryField[]
163
     * @throws CannotMapTypeException
164
     * @throws \ReflectionException
165
     */
166
    private function getFieldsByAnnotations($controller, string $annotationName, bool $injectSource): array
167
    {
168
        $refClass = new \ReflectionClass($controller);
169
170
        $queryList = [];
171
172
        $oldDeclaringClass = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $oldDeclaringClass is dead and can be removed.
Loading history...
173
        $context = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $context is dead and can be removed.
Loading history...
174
175
        foreach ($refClass->getMethods() as $refMethod) {
176
            // First, let's check the "Query" or "Mutation" or "Field" annotation
177
            $queryAnnotation = $this->annotationReader->getRequestAnnotation($refMethod, $annotationName);
178
179
            if ($queryAnnotation !== null) {
180
                if (!$this->isAuthorized($refMethod)) {
181
                    continue;
182
                }
183
184
                $docBlockObj = $this->cachedDocBlockFactory->getDocBlock($refMethod);
185
                $docBlockComment = $docBlockObj->getSummary()."\n".$docBlockObj->getDescription()->render();
186
187
                $methodName = $refMethod->getName();
188
                $name = $queryAnnotation->getName() ?: $methodName;
189
190
                $parameters = $refMethod->getParameters();
191
                if ($injectSource === true) {
192
                    $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...
193
                    // TODO: check that $first_parameter type is correct.
194
                }
195
196
                $args = $this->mapParameters($parameters, $docBlockObj);
197
198
                if ($queryAnnotation->getReturnType()) {
199
                    $type = $this->registry->get($queryAnnotation->getReturnType());
200
                    if (!$type instanceof OutputType) {
201
                        throw new \InvalidArgumentException(sprintf("In %s::%s, the 'returnType' parameter in @Type annotation should contain a container identifier that points to an entry that implements GraphQL\\Type\\Definition\\OutputType. The '%s' container entry does not implement GraphQL\\Type\\Definition\\OutputType", $refMethod->getDeclaringClass()->getName(), $refMethod->getName(), $queryAnnotation->getReturnType()));
202
                    }
203
                } else {
204
                    $type = $this->mapReturnType($refMethod, $docBlockObj);
205
                }
206
207
                $queryList[] = new QueryField($name, $type, $args, [$controller, $methodName], null, $this->hydrator, $docBlockComment, $injectSource);
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

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

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