Completed
Push — master ( 065c69...391b65 )
by David
08:13 queued 05:43
created

ControllerQueryProvider::mapType()   C

Complexity

Conditions 7
Paths 10

Size

Total Lines 29
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 29
rs 6.7272
c 0
b 0
f 0
cc 7
eloc 17
nc 10
nop 4
1
<?php
2
3
4
namespace TheCodingMachine\GraphQL\Controllers;
5
6
use phpDocumentor\Reflection\Type;
7
use phpDocumentor\Reflection\Types\Array_;
8
use phpDocumentor\Reflection\Types\Boolean;
9
use phpDocumentor\Reflection\Types\Float_;
10
use phpDocumentor\Reflection\Types\Mixed_;
11
use phpDocumentor\Reflection\Types\Object_;
12
use phpDocumentor\Reflection\Types\String_;
13
use Roave\BetterReflection\Reflection\ReflectionClass;
14
use Roave\BetterReflection\Reflection\ReflectionMethod;
15
use Doctrine\Common\Annotations\Reader;
16
use phpDocumentor\Reflection\Types\Integer;
17
use TheCodingMachine\GraphQL\Controllers\Annotations\Logged;
18
use TheCodingMachine\GraphQL\Controllers\Annotations\Mutation;
19
use TheCodingMachine\GraphQL\Controllers\Annotations\Query;
20
use TheCodingMachine\GraphQL\Controllers\Annotations\Right;
21
use TheCodingMachine\GraphQL\Controllers\Reflection\CommentParser;
22
use TheCodingMachine\GraphQL\Controllers\Security\AuthenticationServiceInterface;
23
use TheCodingMachine\GraphQL\Controllers\Security\AuthorizationServiceInterface;
24
use Youshido\GraphQL\Field\Field;
25
use Youshido\GraphQL\Type\ListType\ListType;
26
use Youshido\GraphQL\Type\NonNullType;
27
use Youshido\GraphQL\Type\Scalar\BooleanType;
28
use Youshido\GraphQL\Type\Scalar\DateTimeType;
29
use Youshido\GraphQL\Type\Scalar\FloatType;
30
use Youshido\GraphQL\Type\Scalar\IntType;
31
use Youshido\GraphQL\Type\Scalar\StringType;
32
use Youshido\GraphQL\Type\TypeInterface;
33
use Youshido\GraphQL\Type\Union\UnionType;
34
35
/**
36
 * A query provider that looks for queries in a "controller"
37
 */
38
class ControllerQueryProvider implements QueryProviderInterface
39
{
40
    /**
41
     * @var object
42
     */
43
    private $controller;
44
    /**
45
     * @var Reader
46
     */
47
    private $annotationReader;
48
    /**
49
     * @var TypeMapperInterface
50
     */
51
    private $typeMapper;
52
    /**
53
     * @var HydratorInterface
54
     */
55
    private $hydrator;
56
    /**
57
     * @var AuthenticationServiceInterface
58
     */
59
    private $authenticationService;
60
    /**
61
     * @var AuthorizationServiceInterface
62
     */
63
    private $authorizationService;
64
65
    /**
66
     * @param object $controller
67
     */
68
    public function __construct($controller, Reader $annotationReader, TypeMapperInterface $typeMapper, HydratorInterface $hydrator, AuthenticationServiceInterface $authenticationService, AuthorizationServiceInterface $authorizationService)
69
    {
70
        $this->controller = $controller;
71
        $this->annotationReader = $annotationReader;
72
        $this->typeMapper = $typeMapper;
73
        $this->hydrator = $hydrator;
74
        $this->authenticationService = $authenticationService;
75
        $this->authorizationService = $authorizationService;
76
    }
77
78
    /**
79
     * @return Field[]
80
     */
81
    public function getQueries(): array
82
    {
83
        return $this->getFieldsByAnnotations(Query::class);
84
    }
85
86
    /**
87
     * @return Field[]
88
     */
89
    public function getMutations(): array
90
    {
91
        return $this->getFieldsByAnnotations(Mutation::class);
92
    }
93
94
    /**
95
     * @return Field[]
96
     */
97
    private function getFieldsByAnnotations(string $annotationName): array
98
    {
99
        $refClass = ReflectionClass::createFromInstance($this->controller);
100
101
        $queryList = [];
102
103
        $typeResolver = new \phpDocumentor\Reflection\TypeResolver();
104
105
        foreach ($refClass->getMethods() as $refMethod) {
106
            $standardPhpMethod = new \ReflectionMethod(get_class($this->controller), $refMethod->getName());
107
            // First, let's check the "Query" annotation
108
            $queryAnnotation = $this->annotationReader->getMethodAnnotation($standardPhpMethod, $annotationName);
109
110
            if ($queryAnnotation !== null) {
111
                $docBlock = new CommentParser($refMethod->getDocComment());
112
                if (!$this->isAuthorized($standardPhpMethod)) {
113
                    continue;
114
                }
115
116
                $methodName = $refMethod->getName();
117
118
                $args = $this->mapParameters($refMethod, $standardPhpMethod);
119
120
                $phpdocType = $typeResolver->resolve((string) $refMethod->getReturnType());
121
122
                try {
123
                    $type = $this->mapType($phpdocType, $refMethod->getDocBlockReturnTypes(), $standardPhpMethod->getReturnType()->allowsNull(), false);
0 ignored issues
show
Bug introduced by
It seems like $phpdocType defined by $typeResolver->resolve((...ethod->getReturnType()) on line 120 can be null; however, TheCodingMachine\GraphQL...ueryProvider::mapType() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
124
                } catch (TypeMappingException $e) {
125
                    throw TypeMappingException::wrapWithReturnInfo($e, $refMethod);
126
                }
127
                $queryList[] = new QueryField($methodName, $type, $args, [$this->controller, $methodName], $this->hydrator, $docBlock->getComment());
128
            }
129
        }
130
131
        return $queryList;
132
    }
133
134
    /**
135
     * Checks the @Logged and @Right annotations.
136
     *
137
     * @param \ReflectionMethod $reflectionMethod
138
     * @return bool
139
     */
140
    private function isAuthorized(\ReflectionMethod $reflectionMethod) : bool
141
    {
142
        $loggedAnnotation = $this->annotationReader->getMethodAnnotation($reflectionMethod, Logged::class);
143
144
        if ($loggedAnnotation !== null && !$this->authenticationService->isLogged()) {
145
            return false;
146
        }
147
148
        $rightAnnotation = $this->annotationReader->getMethodAnnotation($reflectionMethod, Right::class);
149
        /** @var $rightAnnotation Right */
150
151
        if ($rightAnnotation !== null && !$this->authorizationService->isAllowed($rightAnnotation->getName())) {
152
            return false;
153
        }
154
155
        return true;
156
    }
157
158
    /**
159
     * Note: there is a bug in $refMethod->allowsNull that forces us to use $standardRefMethod->allowsNull instead.
160
     *
161
     * @param ReflectionMethod $refMethod
162
     * @param \ReflectionMethod $standardRefMethod
163
     * @return array
164
     * @throws MissingTypeHintException
165
     */
166
    private function mapParameters(ReflectionMethod $refMethod, \ReflectionMethod $standardRefMethod)
167
    {
168
        $args = [];
169
170
        $typeResolver = new \phpDocumentor\Reflection\TypeResolver();
171
172
        foreach ($standardRefMethod->getParameters() as $standardParameter) {
173
            $allowsNull = $standardParameter->allowsNull();
174
            $parameter = $refMethod->getParameter($standardParameter->getName());
0 ignored issues
show
Bug introduced by
Consider using $standardParameter->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
175
176
            $type = (string) $parameter->getType();
177
            if ($type === '') {
178
                throw MissingTypeHintException::missingTypeHint($parameter);
0 ignored issues
show
Bug introduced by
It seems like $parameter defined by $refMethod->getParameter...rdParameter->getName()) on line 174 can be null; however, TheCodingMachine\GraphQL...tion::missingTypeHint() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
179
            }
180
            $phpdocType = $typeResolver->resolve($type);
181
182
            try {
183
                $arr = [
184
                    'type' => $this->mapType($phpdocType, $parameter->getDocBlockTypes(), $allowsNull, true),
0 ignored issues
show
Bug introduced by
It seems like $phpdocType defined by $typeResolver->resolve($type) on line 180 can be null; however, TheCodingMachine\GraphQL...ueryProvider::mapType() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
185
                ];
186
            } catch (TypeMappingException $e) {
187
                throw TypeMappingException::wrapWithParamInfo($e, $parameter);
0 ignored issues
show
Bug introduced by
It seems like $parameter defined by $refMethod->getParameter...rdParameter->getName()) on line 174 can be null; however, TheCodingMachine\GraphQL...on::wrapWithParamInfo() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
188
            }
189
190
            if ($standardParameter->allowsNull()) {
191
                $arr['default'] = null;
192
            }
193
            if ($standardParameter->isDefaultValueAvailable()) {
194
                $arr['default'] = $standardParameter->getDefaultValue();
195
            }
196
197
            $args[$parameter->getName()] = $arr;
198
        }
199
200
        return $args;
201
    }
202
203
    /**
204
     * @param Type $type
205
     * @param Type[] $docBlockTypes
206
     * @return TypeInterface
207
     */
208
    private function mapType(Type $type, array $docBlockTypes, bool $isNullable, bool $mapToInputType): TypeInterface
209
    {
210
        $graphQlType = null;
0 ignored issues
show
Unused Code introduced by
$graphQlType is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
211
212
        if ($type instanceof Array_ || $type instanceof Mixed_) {
213
            if (!$isNullable) {
214
                // Let's check a "null" value in the docblock
215
                $isNullable = $this->isNullable($docBlockTypes);
216
            }
217
            $filteredDocBlockTypes = $this->typesWithoutNullable($docBlockTypes);
218
            if (empty($filteredDocBlockTypes)) {
219
                throw TypeMappingException::createFromType($type);
220
            } elseif (count($filteredDocBlockTypes) === 1) {
221
                $graphQlType = $this->toGraphQlType($filteredDocBlockTypes[0], $mapToInputType);
222
            } else {
223
                throw new GraphQLException('Union types are not supported (yet)');
224
                //$graphQlTypes = array_map([$this, 'toGraphQlType'], $filteredDocBlockTypes);
0 ignored issues
show
Unused Code Comprehensibility introduced by
65% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
225
                //$$graphQlType = new UnionType($graphQlTypes);
0 ignored issues
show
Unused Code Comprehensibility introduced by
59% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
226
            }
227
        } else {
228
            $graphQlType = $this->toGraphQlType($type, $mapToInputType);
229
        }
230
231
        if (!$isNullable) {
232
            $graphQlType = new NonNullType($graphQlType);
0 ignored issues
show
Documentation introduced by
$graphQlType is of type object<Youshido\GraphQL\Type\TypeInterface>, but the function expects a object<Youshido\GraphQL\Type\AbstractType>|string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
233
        }
234
235
        return $graphQlType;
236
    }
237
238
    /**
239
     * Casts a Type to a GraphQL type.
240
     * Does not deal with nullable.
241
     *
242
     * @param Type $type
243
     * @param bool $mapToInputType
244
     * @return TypeInterface
245
     */
246
    private function toGraphQlType(Type $type, bool $mapToInputType): TypeInterface
247
    {
248
        if ($type instanceof Integer) {
249
            return new IntType();
250
        } elseif ($type instanceof String_) {
251
            return new StringType();
252
        } elseif ($type instanceof Boolean) {
253
            return new BooleanType();
254
        } elseif ($type instanceof Float_) {
255
            return new FloatType();
256
        } elseif ($type instanceof Object_) {
257
            $fqcn = (string) $type->getFqsen();
258
            if ($fqcn === '\\DateTimeImmutable' || $fqcn === '\\DateTimeInterface') {
259
                return new DateTimeType();
260
            } elseif ($fqcn === '\\DateTime') {
261
                throw new GraphQLException('Type-hinting a parameter against DateTime is not allowed. Please use the DateTimeImmutable type instead.');
262
            }
263
264
            $className = ltrim($type->getFqsen(), '\\');
265
            if ($mapToInputType) {
266
                return $this->typeMapper->mapClassToInputType($className);
267
            } else {
268
                return $this->typeMapper->mapClassToType($className);
269
            }
270
        } elseif ($type instanceof Array_) {
271
            return new ListType(new NonNullType($this->toGraphQlType($type->getValueType(), $mapToInputType)));
0 ignored issues
show
Documentation introduced by
$this->toGraphQlType($ty...ype(), $mapToInputType) is of type object<Youshido\GraphQL\Type\TypeInterface>, but the function expects a object<Youshido\GraphQL\Type\AbstractType>|string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
272
        } else {
273
            throw TypeMappingException::createFromType($type);
274
        }
275
    }
276
277
    /**
278
     * Removes "null" from the list of types.
279
     *
280
     * @param Type[] $docBlockTypeHints
281
     * @return array
282
     */
283
    private function typesWithoutNullable(array $docBlockTypeHints): array
284
    {
285
        return array_filter($docBlockTypeHints, function ($item) {
286
            return !$item instanceof Null_;
0 ignored issues
show
Bug introduced by
The class TheCodingMachine\GraphQL\Controllers\Null_ does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
287
        });
288
    }
289
290
    /**
291
     * @param Type[] $docBlockTypeHints
292
     * @return bool
293
     */
294
    private function isNullable(array $docBlockTypeHints): bool
295
    {
296
        foreach ($docBlockTypeHints as $docBlockTypeHint) {
297
            if ($docBlockTypeHint instanceof Null_) {
0 ignored issues
show
Bug introduced by
The class TheCodingMachine\GraphQL\Controllers\Null_ does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
298
                return true;
299
            }
300
        }
301
        return false;
302
    }
303
}
304