Completed
Push — master ( 787d04...7f05e4 )
by David
13s queued 10s
created

ControllerQueryProvider::getMutations()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
eloc 1
nc 1
nop 0
1
<?php
2
3
4
namespace TheCodingMachine\GraphQL\Controllers;
5
6
use function array_map;
7
use function array_merge;
8
use function get_class;
9
use function gettype;
10
use GraphQL\Type\Definition\InputType;
11
use GraphQL\Type\Definition\OutputType;
12
use phpDocumentor\Reflection\Fqsen;
13
use phpDocumentor\Reflection\Types\Nullable;
14
use phpDocumentor\Reflection\Types\Self_;
15
use phpDocumentor\Reflection\Types\Static_;
16
use Psr\Container\ContainerInterface;
17
use ReflectionMethod;
18
use TheCodingMachine\GraphQL\Controllers\Annotations\Exceptions\ClassNotFoundException;
19
use TheCodingMachine\GraphQL\Controllers\Mappers\RecursiveTypeMapper;
20
use TheCodingMachine\GraphQL\Controllers\Mappers\TypeMapperInterface;
21
use TheCodingMachine\GraphQL\Controllers\Types\UnionType;
22
use function is_object;
23
use Iterator;
24
use IteratorAggregate;
25
use phpDocumentor\Reflection\DocBlock;
26
use phpDocumentor\Reflection\DocBlock\Tags\Return_;
27
use phpDocumentor\Reflection\Type;
28
use phpDocumentor\Reflection\Types\Array_;
29
use phpDocumentor\Reflection\Types\Boolean;
30
use phpDocumentor\Reflection\Types\Compound;
31
use phpDocumentor\Reflection\Types\ContextFactory;
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;
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 query provider that looks for queries in a "controller"
54
 */
55
class ControllerQueryProvider implements QueryProviderInterface
56
{
57
    /**
58
     * @var object
59
     */
60
    private $controller;
61
    /**
62
     * @var AnnotationReader
63
     */
64
    private $annotationReader;
65
    /**
66
     * @var RecursiveTypeMapperInterface
67
     */
68
    private $typeMapper;
69
    /**
70
     * @var HydratorInterface
71
     */
72
    private $hydrator;
73
    /**
74
     * @var AuthenticationServiceInterface
75
     */
76
    private $authenticationService;
77
    /**
78
     * @var AuthorizationServiceInterface
79
     */
80
    private $authorizationService;
81
    /**
82
     * @var ContainerInterface
83
     */
84
    private $registry;
85
86
    /**
87
     * @param object $controller
88
     * @param AnnotationReader $annotationReader
89
     * @param RecursiveTypeMapperInterface $typeMapper
90
     * @param HydratorInterface $hydrator
91
     * @param AuthenticationServiceInterface $authenticationService
92
     * @param AuthorizationServiceInterface $authorizationService
93
     * @param ContainerInterface $registry The registry is used to fetch custom return types by container identifier (using the returnType parameter of the Type annotation)
94
     */
95
    public function __construct($controller, AnnotationReader $annotationReader, RecursiveTypeMapperInterface $typeMapper,
96
                                HydratorInterface $hydrator, AuthenticationServiceInterface $authenticationService,
97
                                AuthorizationServiceInterface $authorizationService, ContainerInterface $registry)
98
    {
99
        $this->controller = $controller;
100
        $this->annotationReader = $annotationReader;
101
        $this->typeMapper = $typeMapper;
102
        $this->hydrator = $hydrator;
103
        $this->authenticationService = $authenticationService;
104
        $this->authorizationService = $authorizationService;
105
        $this->registry = $registry;
106
    }
107
108
    /**
109
     * @return QueryField[]
110
     */
111
    public function getQueries(): array
112
    {
113
        return $this->getFieldsByAnnotations(Query::class, false);
114
    }
115
116
    /**
117
     * @return QueryField[]
118
     */
119
    public function getMutations(): array
120
    {
121
        return $this->getFieldsByAnnotations(Mutation::class, false);
122
    }
123
124
    /**
125
     * @return array<string, QueryField> QueryField indexed by name.
126
     */
127
    public function getFields(): array
128
    {
129
        $fieldAnnotations = $this->getFieldsByAnnotations(Annotations\Field::class, true);
130
        $sourceFields = $this->getSourceFields();
131
132
        $fields = [];
133
        foreach ($fieldAnnotations as $field) {
134
            $fields[$field->name] = $field;
135
        }
136
        foreach ($sourceFields as $field) {
137
            $fields[$field->name] = $field;
138
        }
139
140
        return $fields;
141
    }
142
143
    /**
144
     * @param string $annotationName
145
     * @param bool $injectSource Whether to inject the source object or not as the first argument. True for @Field, false for @Query and @Mutation
146
     * @return QueryField[]
147
     * @throws \ReflectionException
148
     */
149
    private function getFieldsByAnnotations(string $annotationName, bool $injectSource): array
150
    {
151
        $refClass = new \ReflectionClass($this->controller);
152
        $docBlockFactory = \phpDocumentor\Reflection\DocBlockFactory::createInstance();
153
        $contextFactory = new ContextFactory();
154
155
        //$refClass = ReflectionClass::createFromInstance($this->controller);
156
157
        $queryList = [];
158
159
        $oldDeclaringClass = null;
160
        $context = null;
161
162
        foreach ($refClass->getMethods() as $refMethod) {
163
            // First, let's check the "Query" or "Mutation" or "Field" annotation
164
            $queryAnnotation = $this->annotationReader->getRequestAnnotation($refMethod, $annotationName);
165
166
            if ($queryAnnotation !== null) {
167
                if (!$this->isAuthorized($refMethod)) {
168
                    continue;
169
                }
170
171
                $docComment = $refMethod->getDocComment() ?: '/** */';
172
173
                // context is changing based on the class the method is declared in.
174
                // we assume methods will be returned packed by classes so we do this little cache
175
                if ($oldDeclaringClass !== $refMethod->getDeclaringClass()->getName()) {
176
                    $context = $contextFactory->createFromReflector($refMethod);
177
                    $oldDeclaringClass = $refMethod->getDeclaringClass()->getName();
178
                }
179
180
                $docBlockObj = $docBlockFactory->create($docComment, $context);
0 ignored issues
show
Bug introduced by
It seems like $docComment can also be of type true; however, parameter $docblock of phpDocumentor\Reflection\DocBlockFactory::create() does only seem to accept object|string, 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

180
                $docBlockObj = $docBlockFactory->create(/** @scrutinizer ignore-type */ $docComment, $context);
Loading history...
181
182
                // TODO: change CommentParser to use $docBlockObj methods instead.
183
                $docBlock = new CommentParser($refMethod->getDocComment());
0 ignored issues
show
Bug introduced by
It seems like $refMethod->getDocComment() can also be of type boolean; however, parameter $docBlock of TheCodingMachine\GraphQL...ntParser::__construct() does only seem to accept string, 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

183
                $docBlock = new CommentParser(/** @scrutinizer ignore-type */ $refMethod->getDocComment());
Loading history...
184
185
                $methodName = $refMethod->getName();
186
                $name = $queryAnnotation->getName() ?: $methodName;
187
188
                $parameters = $refMethod->getParameters();
189
                if ($injectSource === true) {
190
                    $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...
191
                    // TODO: check that $first_parameter type is correct.
192
                }
193
194
                $args = $this->mapParameters($parameters, $docBlockObj);
195
196
                if ($queryAnnotation->getReturnType()) {
197
                    $type = $this->registry->get($queryAnnotation->getReturnType());
198
                    if (!$type instanceof OutputType) {
199
                        throw new \InvalidArgumentException(sprintf("In %s::%s, the 'returnType' parameter in @Type annotation should contain a container identifier that points to an entry that implements GraphQL\\Type\\Definition\\OutputType. The '%s' container entry does not implement GraphQL\\Type\\Definition\\OutputType", $refMethod->getDeclaringClass()->getName(), $refMethod->getName(), $queryAnnotation->getReturnType()));
200
                    }
201
                } else {
202
                    $type = $this->mapReturnType($refMethod, $docBlockObj);
203
                }
204
205
                $queryList[] = new QueryField($name, $type, $args, [$this->controller, $methodName], null, $this->hydrator, $docBlock->getComment(), $injectSource);
0 ignored issues
show
Bug introduced by
It seems like $type can also be of type GraphQL\Type\Definition\Type; however, parameter $type of TheCodingMachine\GraphQL...eryField::__construct() does only seem to accept GraphQL\Type\Definition\OutputType, maybe add an additional type check? ( Ignorable by Annotation )

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

205
                $queryList[] = new QueryField($name, /** @scrutinizer ignore-type */ $type, $args, [$this->controller, $methodName], null, $this->hydrator, $docBlock->getComment(), $injectSource);
Loading history...
206
            }
207
        }
208
209
        return $queryList;
210
    }
211
212
    /**
213
     * @return GraphQLType&OutputType
214
     */
215
    private function mapReturnType(ReflectionMethod $refMethod, DocBlock $docBlockObj): GraphQLType
216
    {
217
        $returnType = $refMethod->getReturnType();
218
        if ($returnType !== null) {
219
            $typeResolver = new \phpDocumentor\Reflection\TypeResolver();
220
            $phpdocType = $typeResolver->resolve((string) $returnType);
221
            $phpdocType = $this->resolveSelf($phpdocType, $refMethod->getDeclaringClass());
222
        } else {
223
            $phpdocType = new Mixed_();
224
        }
225
226
        $docBlockReturnType = $this->getDocBlocReturnType($docBlockObj, $refMethod);
227
228
        try {
229
            /** @var GraphQLType&OutputType $type */
230
            $type = $this->mapType($phpdocType, $docBlockReturnType, $returnType ? $returnType->allowsNull() : false, false);
231
        } catch (TypeMappingException $e) {
232
            throw TypeMappingException::wrapWithReturnInfo($e, $refMethod);
233
        } catch (CannotMapTypeException $e) {
234
            throw CannotMapTypeException::wrapWithReturnInfo($e, $refMethod);
235
        }
236
        return $type;
237
    }
238
239
    private function getDocBlocReturnType(DocBlock $docBlock, \ReflectionMethod $refMethod): ?Type
240
    {
241
        /** @var Return_[] $returnTypeTags */
242
        $returnTypeTags = $docBlock->getTagsByName('return');
243
        if (count($returnTypeTags) > 1) {
244
            throw InvalidDocBlockException::tooManyReturnTags($refMethod);
245
        }
246
        $docBlockReturnType = null;
247
        if (isset($returnTypeTags[0])) {
248
            $docBlockReturnType = $returnTypeTags[0]->getType();
249
        }
250
        return $docBlockReturnType;
251
    }
252
253
    /**
254
     * @return QueryField[]
255
     */
256
    private function getSourceFields(): array
257
    {
258
        $refClass = new \ReflectionClass($this->controller);
259
        $docBlockFactory = \phpDocumentor\Reflection\DocBlockFactory::createInstance();
260
        $contextFactory = new ContextFactory();
261
262
        /** @var SourceField[] $sourceFields */
263
        $sourceFields = $this->annotationReader->getSourceFields($refClass);
264
265
        if ($this->controller instanceof FromSourceFieldsInterface) {
266
            $sourceFields = array_merge($sourceFields, $this->controller->getSourceFields());
267
        }
268
269
        if (empty($sourceFields)) {
270
            return [];
271
        }
272
273
        $typeField = $this->annotationReader->getTypeAnnotation($refClass);
274
275
        if ($typeField === null) {
276
            throw MissingAnnotationException::missingTypeExceptionToUseSourceField();
277
        }
278
279
        $objectClass = $typeField->getClass();
280
        $objectRefClass = new \ReflectionClass($objectClass);
281
282
        $oldDeclaringClass = null;
283
        $context = null;
284
        $queryList = [];
285
286
        foreach ($sourceFields as $sourceField) {
287
            // Ignore the field if we must be logged.
288
            if ($sourceField->isLogged() && !$this->authenticationService->isLogged()) {
289
                continue;
290
            }
291
292
            $right = $sourceField->getRight();
293
            if ($right !== null && !$this->authorizationService->isAllowed($right->getName())) {
294
                continue;
295
            }
296
297
            try {
298
                $refMethod = $this->getMethodFromPropertyName($objectRefClass, $sourceField->getName());
299
            } catch (FieldNotFoundException $e) {
300
                throw FieldNotFoundException::wrapWithCallerInfo($e, $refClass->getName());
301
            }
302
303
            $docBlock = new CommentParser($refMethod->getDocComment());
0 ignored issues
show
Bug introduced by
It seems like $refMethod->getDocComment() can also be of type boolean; however, parameter $docBlock of TheCodingMachine\GraphQL...ntParser::__construct() does only seem to accept string, 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

303
            $docBlock = new CommentParser(/** @scrutinizer ignore-type */ $refMethod->getDocComment());
Loading history...
304
305
            $methodName = $refMethod->getName();
306
307
308
            // context is changing based on the class the method is declared in.
309
            // we assume methods will be returned packed by classes so we do this little cache
310
            if ($oldDeclaringClass !== $refMethod->getDeclaringClass()->getName()) {
311
                $context = $contextFactory->createFromReflector($refMethod);
312
                $oldDeclaringClass = $refMethod->getDeclaringClass()->getName();
313
            }
314
315
            $docComment = $refMethod->getDocComment() ?: '/** */';
316
            $docBlockObj = $docBlockFactory->create($docComment, $context);
0 ignored issues
show
Bug introduced by
It seems like $docComment can also be of type true; however, parameter $docblock of phpDocumentor\Reflection\DocBlockFactory::create() does only seem to accept object|string, 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

316
            $docBlockObj = $docBlockFactory->create(/** @scrutinizer ignore-type */ $docComment, $context);
Loading history...
317
318
            $args = $this->mapParameters($refMethod->getParameters(), $docBlockObj);
319
320
            if ($sourceField->isId()) {
321
                $type = GraphQLType::id();
322
                if (!$refMethod->getReturnType()->allowsNull()) {
323
                    $type = GraphQLType::nonNull($type);
324
                }
325
            } elseif ($sourceField->getReturnType()) {
326
                $type = $this->registry->get($sourceField->getReturnType());
327
            } else {
328
                $type = $this->mapReturnType($refMethod, $docBlockObj);
329
            }
330
331
            $queryList[] = new QueryField($sourceField->getName(), $type, $args, null, $methodName, $this->hydrator, $docBlock->getComment(), 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

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