FieldsBuilder   F
last analyzed

Complexity

Total Complexity 124

Size/Duplication

Total Lines 708
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 124
eloc 319
dl 0
loc 708
rs 2
c 0
b 0
f 0

22 Methods

Rating   Name   Duplication   Size   Complexity  
A isAuthorized() 0 16 5
A getTypeInArray() 0 9 2
A resolveSelf() 0 6 2
B mapIteratorDocBlockType() 0 52 11
A __construct() 0 13 1
A getDocBlocReturnType() 0 12 3
A isNullable() 0 14 5
A getSelfFields() 0 10 2
A getInputFields() 0 10 1
A mapReturnType() 0 22 5
B mapType() 0 31 9
B mapDocBlockType() 0 48 10
C getFieldsByAnnotations() 0 71 14
B mapParameters() 0 48 11
A getMutations() 0 3 1
C getSourceFields() 0 82 17
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
C toGraphQlType() 0 34 13

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\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;
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...
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
The assignment to $oldDeclaringClass is dead and can be removed.
Loading history...
199
        $context = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $context is dead and can be removed.
Loading history...
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
Unused Code introduced by
The assignment to $first_parameter is dead and can be removed.
Loading history...
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
The variable $failWith does not seem to be defined for all execution paths leading up to this point.
Loading history...
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
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

251
                    $queryList[] = new QueryField($name, /** @scrutinizer ignore-type */ $type, $args, null, $callable[1], $this->hydrator, $docBlockComment, $injectSource);
Loading history...
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
Unused Code introduced by
The assignment to $oldDeclaringClass is dead and can be removed.
Loading history...
333
        $context = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $context is dead and can be removed.
Loading history...
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
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

376
                $queryList[] = new QueryField($sourceField->getName(), /** @scrutinizer ignore-type */ $type, $args, null, $methodName, $this->hydrator, $docBlockComment, false);
Loading history...
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
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

385
                $queryList[] = new QueryField($sourceField->getName(), /** @scrutinizer ignore-type */ $type, $args, $callable, null, $this->hydrator, $docBlockComment, false);
Loading history...
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
Bug introduced by
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 ignore-type  annotation

673
                        return $this->typeMapper->mapClassToInterfaceOrType($className, /** @scrutinizer ignore-type */ $subType);
Loading history...
Bug Best Practice introduced by
The expression return $this->typeMapper...e($className, $subType) returns the type GraphQL\Type\Definition\OutputType which is incompatible with the type-hinted return GraphQL\Type\Definition\Type.
Loading history...
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