Completed
Push — master ( d70f56...7b6b4e )
by David
22s
created

FieldsBuilder::getSelfFields()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 5
dl 0
loc 10
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 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
     * Track Field annotation in a self targeted type
150
     *
151
     * @return array<string, QueryField> QueryField indexed by name.
152
     */
153
    public function getSelfFields(string $className): array
154
    {
155
        $fieldAnnotations = $this->getFieldsByAnnotations(null, Annotations\Field::class, false, $className);
156
157
        $fields = [];
158
        foreach ($fieldAnnotations as $field) {
159
            $fields[$field->name] = $field;
160
        }
161
162
        return $fields;
163
    }
164
165
    /**
166
     * @param ReflectionMethod $refMethod A method annotated with a Factory annotation.
167
     * @return array<string, array<int, mixed>> Returns an array of fields as accepted by the InputObjectType constructor.
168
     */
169
    public function getInputFields(ReflectionMethod $refMethod): array
170
    {
171
        $docBlockObj = $this->cachedDocBlockFactory->getDocBlock($refMethod);
172
        //$docBlockComment = $docBlockObj->getSummary()."\n".$docBlockObj->getDescription()->render();
173
174
        $parameters = $refMethod->getParameters();
175
176
        $args = $this->mapParameters($parameters, $docBlockObj);
177
178
        return $args;
179
    }
180
181
    /**
182
     * @param object $controller
183
     * @param string $annotationName
184
     * @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
185
     * @return QueryField[]
186
     * @throws CannotMapTypeExceptionInterface
187
     * @throws \ReflectionException
188
     */
189
    private function getFieldsByAnnotations($controller, string $annotationName, bool $injectSource, ?string $sourceClassName = null): array
190
    {
191
        if ($sourceClassName !== null) {
192
            $refClass = new \ReflectionClass($sourceClassName);
193
        } else {
194
            $refClass = new \ReflectionClass($controller);
195
        }
196
197
        $queryList = [];
198
199
        $oldDeclaringClass = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $oldDeclaringClass is dead and can be removed.
Loading history...
200
        $context = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $context is dead and can be removed.
Loading history...
201
202
        foreach ($refClass->getMethods() as $refMethod) {
203
            // First, let's check the "Query" or "Mutation" or "Field" annotation
204
            $queryAnnotation = $this->annotationReader->getRequestAnnotation($refMethod, $annotationName);
205
206
            if ($queryAnnotation !== null) {
207
                $unauthorized = false;
208
                if (!$this->isAuthorized($refMethod)) {
209
                    $failWith = $this->annotationReader->getFailWithAnnotation($refMethod);
210
                    if ($failWith === null) {
211
                        continue;
212
                    }
213
                    $unauthorized = true;
214
                }
215
216
                $docBlockObj = $this->cachedDocBlockFactory->getDocBlock($refMethod);
217
                $docBlockComment = $docBlockObj->getSummary()."\n".$docBlockObj->getDescription()->render();
218
219
                $methodName = $refMethod->getName();
220
                $name = $queryAnnotation->getName() ?: $methodName;
221
222
                $parameters = $refMethod->getParameters();
223
                if ($injectSource === true) {
224
                    $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...
225
                    // TODO: check that $first_parameter type is correct.
226
                }
227
228
                $args = $this->mapParameters($parameters, $docBlockObj);
229
230
                if ($queryAnnotation->getOutputType()) {
231
                    $type = $this->typeResolver->mapNameToType($queryAnnotation->getOutputType());
232
                    if (!$type instanceof OutputType) {
233
                        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()));
234
                    }
235
                } else {
236
                    $type = $this->mapReturnType($refMethod, $docBlockObj);
237
                }
238
239
                if (!$unauthorized) {
240
                    $callable = [$controller, $methodName];
241
                } else {
242
                    $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...
243
                    $callable = function() use ($failWithValue) {
244
                        return $failWithValue;
245
                    };
246
                    if ($failWithValue === null && $type instanceof NonNull) {
247
                        $type = $type->getWrappedType();
248
                    }
249
                }
250
251
                if ($sourceClassName !== null) {
252
                    $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

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

377
                $queryList[] = new QueryField($sourceField->getName(), /** @scrutinizer ignore-type */ $type, $args, null, $methodName, $this->hydrator, $docBlockComment, false);
Loading history...
378
            } else {
379
                $failWithValue = $sourceField->getFailWith();
380
                $callable = function() use ($failWithValue) {
381
                    return $failWithValue;
382
                };
383
                if ($failWithValue === null && $type instanceof NonNull) {
384
                    $type = $type->getWrappedType();
385
                }
386
                $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

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

671
                return $this->typeMapper->mapClassToInterfaceOrType($className, /** @scrutinizer ignore-type */ $subType);
Loading history...
672
            }
673
        } elseif ($type instanceof Array_) {
674
            return GraphQLType::listOf(GraphQLType::nonNull($this->toGraphQlType($type->getValueType(), $subType, $mapToInputType)));
675
        } else {
676
            throw TypeMappingException::createFromType($type);
677
        }
678
    }
679
680
    /**
681
     * Removes "null" from the type (if it is compound). Return an array of types (not a Compound type).
682
     *
683
     * @param Type $docBlockTypeHint
684
     * @return array
685
     */
686
    private function typesWithoutNullable(Type $docBlockTypeHint): array
687
    {
688
        if ($docBlockTypeHint instanceof Compound) {
689
            $docBlockTypeHints = \iterator_to_array($docBlockTypeHint);
690
        } else {
691
            $docBlockTypeHints = [$docBlockTypeHint];
692
        }
693
        return array_filter($docBlockTypeHints, function ($item) {
694
            return !$item instanceof Null_;
695
        });
696
    }
697
698
    /**
699
     * Drops "Nullable" types and return the core type.
700
     *
701
     * @param Type $typeHint
702
     * @return Type
703
     */
704
    private function dropNullableType(Type $typeHint): Type
705
    {
706
        if ($typeHint instanceof Nullable) {
707
            return $typeHint->getActualType();
708
        }
709
        return $typeHint;
710
    }
711
712
    /**
713
     * Resolves a list type.
714
     *
715
     * @param Type $typeHint
716
     * @return Type|null
717
     */
718
    private function getTypeInArray(Type $typeHint): ?Type
719
    {
720
        $typeHint = $this->dropNullableType($typeHint);
721
722
        if (!$typeHint instanceof Array_) {
723
            return null;
724
        }
725
726
        return $this->dropNullableType($typeHint->getValueType());
727
    }
728
729
    /**
730
     * @param Type $docBlockTypeHint
731
     * @return bool
732
     */
733
    private function isNullable(Type $docBlockTypeHint): bool
734
    {
735
        if ($docBlockTypeHint instanceof Null_) {
736
            return true;
737
        }
738
        if ($docBlockTypeHint instanceof Compound) {
739
            foreach ($docBlockTypeHint as $type) {
740
                if ($type instanceof Null_) {
741
                    return true;
742
                }
743
            }
744
        }
745
746
        return false;
747
    }
748
749
    /**
750
     * Resolves "self" types into the class type.
751
     *
752
     * @param Type $type
753
     * @return Type
754
     */
755
    private function resolveSelf(Type $type, ReflectionClass $reflectionClass): Type
756
    {
757
        if ($type instanceof Self_) {
758
            return new Object_(new Fqsen('\\'.$reflectionClass->getName()));
759
        }
760
        return $type;
761
    }
762
}
763