FieldsBuilder::__construct()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 8
dl 0
loc 13
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 8

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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