Completed
Push — master ( aa5d8f...4c27e1 )
by David
14s
created

ControllerQueryProvider   F

Complexity

Total Complexity 44

Size/Duplication

Total Lines 277
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 26

Importance

Changes 0
Metric Value
wmc 44
lcom 1
cbo 26
dl 0
loc 277
rs 3.9115
c 0
b 0
f 0

10 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 10 2
A getQueries() 0 4 1
A getMutations() 0 4 1
B getFieldsByAnnotations() 0 42 6
B isAuthorized() 0 17 5
C mapType() 0 29 7
C toGraphQlType() 0 30 11
A typesWithoutNullable() 0 6 1
A isNullable() 0 9 3
C mapParameters() 0 36 7

How to fix   Complexity   

Complex Class

Complex classes like ControllerQueryProvider often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ControllerQueryProvider, and based on these observations, apply Extract Interface, too.

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 Psr\Container\ContainerInterface;
14
use Roave\BetterReflection\Reflection\ReflectionClass;
15
use Roave\BetterReflection\Reflection\ReflectionMethod;
16
use Doctrine\Common\Annotations\Reader;
17
use phpDocumentor\Reflection\Types\Integer;
18
use TheCodingMachine\GraphQL\Controllers\Annotations\AbstractRequest;
19
use TheCodingMachine\GraphQL\Controllers\Annotations\Logged;
20
use TheCodingMachine\GraphQL\Controllers\Annotations\Mutation;
21
use TheCodingMachine\GraphQL\Controllers\Annotations\Query;
22
use TheCodingMachine\GraphQL\Controllers\Annotations\Right;
23
use TheCodingMachine\GraphQL\Controllers\Reflection\CommentParser;
24
use TheCodingMachine\GraphQL\Controllers\Registry\EmptyContainer;
25
use TheCodingMachine\GraphQL\Controllers\Registry\Registry;
26
use TheCodingMachine\GraphQL\Controllers\Security\AuthenticationServiceInterface;
27
use TheCodingMachine\GraphQL\Controllers\Security\AuthorizationServiceInterface;
28
use Youshido\GraphQL\Field\Field;
29
use Youshido\GraphQL\Type\ListType\ListType;
30
use Youshido\GraphQL\Type\NonNullType;
31
use Youshido\GraphQL\Type\Scalar\BooleanType;
32
use Youshido\GraphQL\Type\Scalar\DateTimeType;
33
use Youshido\GraphQL\Type\Scalar\FloatType;
34
use Youshido\GraphQL\Type\Scalar\IntType;
35
use Youshido\GraphQL\Type\Scalar\StringType;
36
use Youshido\GraphQL\Type\TypeInterface;
37
38
/**
39
 * A query provider that looks for queries in a "controller"
40
 */
41
class ControllerQueryProvider implements QueryProviderInterface
42
{
43
    /**
44
     * @var object
45
     */
46
    private $controller;
47
    /**
48
     * @var Reader
49
     */
50
    private $annotationReader;
51
    /**
52
     * @var TypeMapperInterface
53
     */
54
    private $typeMapper;
55
    /**
56
     * @var HydratorInterface
57
     */
58
    private $hydrator;
59
    /**
60
     * @var AuthenticationServiceInterface
61
     */
62
    private $authenticationService;
63
    /**
64
     * @var AuthorizationServiceInterface
65
     */
66
    private $authorizationService;
67
    /**
68
     * @var ContainerInterface
69
     */
70
    private $registry;
71
72
    /**
73
     * @param object $controller
74
     */
75
    public function __construct($controller, Reader $annotationReader, TypeMapperInterface $typeMapper, HydratorInterface $hydrator, AuthenticationServiceInterface $authenticationService, AuthorizationServiceInterface $authorizationService, ?ContainerInterface $container = null)
76
    {
77
        $this->controller = $controller;
78
        $this->annotationReader = $annotationReader;
79
        $this->typeMapper = $typeMapper;
80
        $this->hydrator = $hydrator;
81
        $this->authenticationService = $authenticationService;
82
        $this->authorizationService = $authorizationService;
83
        $this->registry = new Registry($container ?: new EmptyContainer());
84
    }
85
86
    /**
87
     * @return Field[]
88
     */
89
    public function getQueries(): array
90
    {
91
        return $this->getFieldsByAnnotations(Query::class);
92
    }
93
94
    /**
95
     * @return Field[]
96
     */
97
    public function getMutations(): array
98
    {
99
        return $this->getFieldsByAnnotations(Mutation::class);
100
    }
101
102
    /**
103
     * @return Field[]
104
     */
105
    private function getFieldsByAnnotations(string $annotationName): array
106
    {
107
        $refClass = ReflectionClass::createFromInstance($this->controller);
108
109
        $queryList = [];
110
111
        $typeResolver = new \phpDocumentor\Reflection\TypeResolver();
112
113
        foreach ($refClass->getMethods() as $refMethod) {
114
            $standardPhpMethod = new \ReflectionMethod(get_class($this->controller), $refMethod->getName());
115
            // First, let's check the "Query" annotation
116
            $queryAnnotation = $this->annotationReader->getMethodAnnotation($standardPhpMethod, $annotationName);
117
            /* @var $queryAnnotation AbstractRequest */
118
119
            if ($queryAnnotation !== null) {
120
                $docBlock = new CommentParser($refMethod->getDocComment());
121
                if (!$this->isAuthorized($standardPhpMethod)) {
122
                    continue;
123
                }
124
125
                $methodName = $refMethod->getName();
126
127
                $args = $this->mapParameters($refMethod, $standardPhpMethod);
128
129
                $phpdocType = $typeResolver->resolve((string) $refMethod->getReturnType());
130
131
                if ($queryAnnotation->getReturnType()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $queryAnnotation->getReturnType() of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
132
                    $type = $this->registry->get($queryAnnotation->getReturnType());
133
                } else {
134
                    try {
135
                        $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 129 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...
136
                    } catch (TypeMappingException $e) {
137
                        throw TypeMappingException::wrapWithReturnInfo($e, $refMethod);
138
                    }
139
                }
140
141
                $queryList[] = new QueryField($methodName, $type, $args, [$this->controller, $methodName], $this->hydrator, $docBlock->getComment());
142
            }
143
        }
144
145
        return $queryList;
146
    }
147
148
    /**
149
     * Checks the @Logged and @Right annotations.
150
     *
151
     * @param \ReflectionMethod $reflectionMethod
152
     * @return bool
153
     */
154
    private function isAuthorized(\ReflectionMethod $reflectionMethod) : bool
155
    {
156
        $loggedAnnotation = $this->annotationReader->getMethodAnnotation($reflectionMethod, Logged::class);
157
158
        if ($loggedAnnotation !== null && !$this->authenticationService->isLogged()) {
159
            return false;
160
        }
161
162
        $rightAnnotation = $this->annotationReader->getMethodAnnotation($reflectionMethod, Right::class);
163
        /** @var $rightAnnotation Right */
164
165
        if ($rightAnnotation !== null && !$this->authorizationService->isAllowed($rightAnnotation->getName())) {
166
            return false;
167
        }
168
169
        return true;
170
    }
171
172
    /**
173
     * Note: there is a bug in $refMethod->allowsNull that forces us to use $standardRefMethod->allowsNull instead.
174
     *
175
     * @param ReflectionMethod $refMethod
176
     * @param \ReflectionMethod $standardRefMethod
177
     * @return array
178
     * @throws MissingTypeHintException
179
     */
180
    private function mapParameters(ReflectionMethod $refMethod, \ReflectionMethod $standardRefMethod)
181
    {
182
        $args = [];
183
184
        $typeResolver = new \phpDocumentor\Reflection\TypeResolver();
185
186
        foreach ($standardRefMethod->getParameters() as $standardParameter) {
187
            $allowsNull = $standardParameter->allowsNull();
188
            $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...
189
190
            $type = (string) $parameter->getType();
191
            if ($type === '') {
192
                throw MissingTypeHintException::missingTypeHint($parameter);
0 ignored issues
show
Bug introduced by
It seems like $parameter defined by $refMethod->getParameter...rdParameter->getName()) on line 188 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...
193
            }
194
            $phpdocType = $typeResolver->resolve($type);
195
196
            try {
197
                $arr = [
198
                    'type' => $this->mapType($phpdocType, $parameter->getDocBlockTypes(), $allowsNull || $parameter->isDefaultValueAvailable(), true),
0 ignored issues
show
Bug introduced by
It seems like $phpdocType defined by $typeResolver->resolve($type) on line 194 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...
199
                ];
200
            } catch (TypeMappingException $e) {
201
                throw TypeMappingException::wrapWithParamInfo($e, $parameter);
0 ignored issues
show
Bug introduced by
It seems like $parameter defined by $refMethod->getParameter...rdParameter->getName()) on line 188 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...
202
            }
203
204
            if ($standardParameter->allowsNull()) {
205
                $arr['default'] = null;
206
            }
207
            if ($standardParameter->isDefaultValueAvailable()) {
208
                $arr['default'] = $standardParameter->getDefaultValue();
209
            }
210
211
            $args[$parameter->getName()] = $arr;
212
        }
213
214
        return $args;
215
    }
216
217
    /**
218
     * @param Type $type
219
     * @param Type[] $docBlockTypes
220
     * @return TypeInterface
221
     */
222
    private function mapType(Type $type, array $docBlockTypes, bool $isNullable, bool $mapToInputType): TypeInterface
223
    {
224
        $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...
225
226
        if ($type instanceof Array_ || $type instanceof Mixed_) {
227
            if (!$isNullable) {
228
                // Let's check a "null" value in the docblock
229
                $isNullable = $this->isNullable($docBlockTypes);
230
            }
231
            $filteredDocBlockTypes = $this->typesWithoutNullable($docBlockTypes);
232
            if (empty($filteredDocBlockTypes)) {
233
                throw TypeMappingException::createFromType($type);
234
            } elseif (count($filteredDocBlockTypes) === 1) {
235
                $graphQlType = $this->toGraphQlType($filteredDocBlockTypes[0], $mapToInputType);
236
            } else {
237
                throw new GraphQLException('Union types are not supported (yet)');
238
                //$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...
239
                //$$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...
240
            }
241
        } else {
242
            $graphQlType = $this->toGraphQlType($type, $mapToInputType);
243
        }
244
245
        if (!$isNullable) {
246
            $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...
247
        }
248
249
        return $graphQlType;
250
    }
251
252
    /**
253
     * Casts a Type to a GraphQL type.
254
     * Does not deal with nullable.
255
     *
256
     * @param Type $type
257
     * @param bool $mapToInputType
258
     * @return TypeInterface
259
     */
260
    private function toGraphQlType(Type $type, bool $mapToInputType): TypeInterface
261
    {
262
        if ($type instanceof Integer) {
263
            return new IntType();
264
        } elseif ($type instanceof String_) {
265
            return new StringType();
266
        } elseif ($type instanceof Boolean) {
267
            return new BooleanType();
268
        } elseif ($type instanceof Float_) {
269
            return new FloatType();
270
        } elseif ($type instanceof Object_) {
271
            $fqcn = (string) $type->getFqsen();
272
            if ($fqcn === '\\DateTimeImmutable' || $fqcn === '\\DateTimeInterface') {
273
                return new DateTimeType();
274
            } elseif ($fqcn === '\\DateTime') {
275
                throw new GraphQLException('Type-hinting a parameter against DateTime is not allowed. Please use the DateTimeImmutable type instead.');
276
            }
277
278
            $className = ltrim($type->getFqsen(), '\\');
279
            if ($mapToInputType) {
280
                return $this->typeMapper->mapClassToInputType($className);
281
            } else {
282
                return $this->typeMapper->mapClassToType($className);
283
            }
284
        } elseif ($type instanceof Array_) {
285
            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...
286
        } else {
287
            throw TypeMappingException::createFromType($type);
288
        }
289
    }
290
291
    /**
292
     * Removes "null" from the list of types.
293
     *
294
     * @param Type[] $docBlockTypeHints
295
     * @return array
296
     */
297
    private function typesWithoutNullable(array $docBlockTypeHints): array
298
    {
299
        return array_filter($docBlockTypeHints, function ($item) {
300
            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...
301
        });
302
    }
303
304
    /**
305
     * @param Type[] $docBlockTypeHints
306
     * @return bool
307
     */
308
    private function isNullable(array $docBlockTypeHints): bool
309
    {
310
        foreach ($docBlockTypeHints as $docBlockTypeHint) {
311
            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...
312
                return true;
313
            }
314
        }
315
        return false;
316
    }
317
}
318