Completed
Push — master ( 3977d8...7ec8b3 )
by David
15s queued 11s
created

FieldsBuilder::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

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

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

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

646
                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...
647
            }
648
        } elseif ($type instanceof Array_) {
649
            return GraphQLType::listOf(GraphQLType::nonNull($this->toGraphQlType($type->getValueType(), $subType, $mapToInputType)));
650
        } else {
651
            throw TypeMappingException::createFromType($type);
652
        }
653
    }
654
655
    /**
656
     * Removes "null" from the type (if it is compound). Return an array of types (not a Compound type).
657
     *
658
     * @param Type $docBlockTypeHint
659
     * @return array
660
     */
661
    private function typesWithoutNullable(Type $docBlockTypeHint): array
662
    {
663
        if ($docBlockTypeHint instanceof Compound) {
664
            $docBlockTypeHints = \iterator_to_array($docBlockTypeHint);
665
        } else {
666
            $docBlockTypeHints = [$docBlockTypeHint];
667
        }
668
        return array_filter($docBlockTypeHints, function ($item) {
669
            return !$item instanceof Null_;
670
        });
671
    }
672
673
    /**
674
     * Drops "Nullable" types and return the core type.
675
     *
676
     * @param Type $typeHint
677
     * @return Type
678
     */
679
    private function dropNullableType(Type $typeHint): Type
680
    {
681
        if ($typeHint instanceof Nullable) {
682
            return $typeHint->getActualType();
683
        }
684
        return $typeHint;
685
    }
686
687
    /**
688
     * Resolves a list type.
689
     *
690
     * @param Type $typeHint
691
     * @return Type|null
692
     */
693
    private function getTypeInArray(Type $typeHint): ?Type
694
    {
695
        $typeHint = $this->dropNullableType($typeHint);
696
697
        if (!$typeHint instanceof Array_) {
698
            return null;
699
        }
700
701
        return $this->dropNullableType($typeHint->getValueType());
702
    }
703
704
    /**
705
     * @param Type $docBlockTypeHint
706
     * @return bool
707
     */
708
    private function isNullable(Type $docBlockTypeHint): bool
709
    {
710
        if ($docBlockTypeHint instanceof Null_) {
711
            return true;
712
        }
713
        if ($docBlockTypeHint instanceof Compound) {
714
            foreach ($docBlockTypeHint as $type) {
715
                if ($type instanceof Null_) {
716
                    return true;
717
                }
718
            }
719
        }
720
721
        return false;
722
    }
723
724
    /**
725
     * Resolves "self" types into the class type.
726
     *
727
     * @param Type $type
728
     * @return Type
729
     */
730
    private function resolveSelf(Type $type, ReflectionClass $reflectionClass): Type
731
    {
732
        if ($type instanceof Self_) {
733
            return new Object_(new Fqsen('\\'.$reflectionClass->getName()));
734
        }
735
        return $type;
736
    }
737
}
738