Completed
Pull Request — master (#19)
by David
02:28 queued 33s
created

ControllerQueryProvider   B

Complexity

Total Complexity 45

Size/Duplication

Total Lines 294
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 45
dl 0
loc 294
rs 8.3673
c 0
b 0
f 0

11 Methods

Rating   Name   Duplication   Size   Complexity  
A typesWithoutNullable() 0 4 1
C mapType() 0 28 7
C mapParameters() 0 35 7
A isNullable() 0 8 3
A __construct() 0 9 1
A getFields() 0 3 1
A getQueries() 0 3 1
B isAuthorized() 0 16 5
A getMutations() 0 3 1
C getFieldsByAnnotations() 0 49 7
C toGraphQlType() 0 28 11

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.

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\Registry\RegistryInterface;
27
use TheCodingMachine\GraphQL\Controllers\Security\AuthenticationServiceInterface;
28
use TheCodingMachine\GraphQL\Controllers\Security\AuthorizationServiceInterface;
29
use Youshido\GraphQL\Field\Field;
30
use Youshido\GraphQL\Type\ListType\ListType;
31
use Youshido\GraphQL\Type\NonNullType;
32
use Youshido\GraphQL\Type\Scalar\BooleanType;
33
use Youshido\GraphQL\Type\Scalar\DateTimeType;
34
use Youshido\GraphQL\Type\Scalar\FloatType;
35
use Youshido\GraphQL\Type\Scalar\IntType;
36
use Youshido\GraphQL\Type\Scalar\StringType;
37
use Youshido\GraphQL\Type\TypeInterface;
38
39
/**
40
 * A query provider that looks for queries in a "controller"
41
 */
42
class ControllerQueryProvider implements QueryProviderInterface
43
{
44
    /**
45
     * @var object
46
     */
47
    private $controller;
48
    /**
49
     * @var Reader
50
     */
51
    private $annotationReader;
52
    /**
53
     * @var TypeMapperInterface
54
     */
55
    private $typeMapper;
56
    /**
57
     * @var HydratorInterface
58
     */
59
    private $hydrator;
60
    /**
61
     * @var AuthenticationServiceInterface
62
     */
63
    private $authenticationService;
64
    /**
65
     * @var AuthorizationServiceInterface
66
     */
67
    private $authorizationService;
68
    /**
69
     * @var RegistryInterface
70
     */
71
    private $registry;
72
73
    /**
74
     * @param object $controller
75
     */
76
    public function __construct($controller, RegistryInterface $registry)
77
    {
78
        $this->controller = $controller;
79
        $this->annotationReader = $registry->getAnnotationReader();
80
        $this->typeMapper = $registry->getTypeMapper();
81
        $this->hydrator = $registry->getHydrator();
82
        $this->authenticationService = $registry->getAuthenticationService();
83
        $this->authorizationService = $registry->getAuthorizationService();
84
        $this->registry = $registry;
85
    }
86
87
    /**
88
     * @return Field[]
89
     */
90
    public function getQueries(): array
91
    {
92
        return $this->getFieldsByAnnotations(Query::class, false);
93
    }
94
95
    /**
96
     * @return Field[]
97
     */
98
    public function getMutations(): array
99
    {
100
        return $this->getFieldsByAnnotations(Mutation::class, false);
101
    }
102
103
    /**
104
     * @return Field[]
105
     */
106
    public function getFields(): array
107
    {
108
        return $this->getFieldsByAnnotations(Annotations\Field::class, true);
109
    }
110
111
    /**
112
     * @param string $annotationName
113
     * @param bool $injectSource Whether to inject the source object or not as the first argument. True for @Field, false for @Query and @Mutation
114
     * @return Field[]
115
     * @throws \ReflectionException
116
     */
117
    private function getFieldsByAnnotations(string $annotationName, bool $injectSource): array
118
    {
119
        $refClass = ReflectionClass::createFromInstance($this->controller);
120
121
        $queryList = [];
122
123
        $typeResolver = new \phpDocumentor\Reflection\TypeResolver();
124
125
        foreach ($refClass->getMethods() as $refMethod) {
126
            $standardPhpMethod = new \ReflectionMethod(get_class($this->controller), $refMethod->getName());
127
            // First, let's check the "Query" or "Mutation" or "Field" annotation
128
            $queryAnnotation = $this->annotationReader->getMethodAnnotation($standardPhpMethod, $annotationName);
129
            /* @var $queryAnnotation AbstractRequest */
130
131
            if ($queryAnnotation !== null) {
132
                $docBlock = new CommentParser($refMethod->getDocComment());
133
                if (!$this->isAuthorized($standardPhpMethod)) {
134
                    continue;
135
                }
136
137
                $methodName = $refMethod->getName();
138
139
                $args = $this->mapParameters($refMethod, $standardPhpMethod);
140
141
                $phpdocType = $typeResolver->resolve((string) $refMethod->getReturnType());
142
143
                if ($queryAnnotation->getReturnType()) {
144
                    $type = $this->registry->get($queryAnnotation->getReturnType());
145
                } else {
146
                    try {
147
                        $type = $this->mapType($phpdocType, $refMethod->getDocBlockReturnTypes(), $standardPhpMethod->getReturnType()->allowsNull(), false);
148
                    } catch (TypeMappingException $e) {
149
                        throw TypeMappingException::wrapWithReturnInfo($e, $refMethod);
150
                    }
151
                }
152
153
                //$sourceType = null;
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% 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...
154
                if ($injectSource === true) {
155
                    /*$sourceArr = */\array_shift($args);
156
                    // Security check: if the first parameter of the correct type?
157
                    //$sourceType = $sourceArr['type'];
0 ignored issues
show
Unused Code Comprehensibility introduced by
67% 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...
158
                    /* @var $sourceType TypeInterface */
159
                    // TODO
160
                }
161
                $queryList[] = new QueryField($methodName, $type, $args, [$this->controller, $methodName], $this->hydrator, $docBlock->getComment(), $injectSource);
162
            }
163
        }
164
165
        return $queryList;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $queryList returns an array which contains values of type TheCodingMachine\GraphQL\Controllers\QueryField which are incompatible with the documented value type Youshido\GraphQL\Field\Field.
Loading history...
166
    }
167
168
    /**
169
     * Checks the @Logged and @Right annotations.
170
     *
171
     * @param \ReflectionMethod $reflectionMethod
172
     * @return bool
173
     */
174
    private function isAuthorized(\ReflectionMethod $reflectionMethod) : bool
175
    {
176
        $loggedAnnotation = $this->annotationReader->getMethodAnnotation($reflectionMethod, Logged::class);
177
178
        if ($loggedAnnotation !== null && !$this->authenticationService->isLogged()) {
179
            return false;
180
        }
181
182
        $rightAnnotation = $this->annotationReader->getMethodAnnotation($reflectionMethod, Right::class);
183
        /** @var $rightAnnotation Right */
184
185
        if ($rightAnnotation !== null && !$this->authorizationService->isAllowed($rightAnnotation->getName())) {
186
            return false;
187
        }
188
189
        return true;
190
    }
191
192
    /**
193
     * Note: there is a bug in $refMethod->allowsNull that forces us to use $standardRefMethod->allowsNull instead.
194
     *
195
     * @param ReflectionMethod $refMethod
196
     * @param \ReflectionMethod $standardRefMethod
197
     * @return array[] An array of ['type'=>TypeInterface, 'default'=>val]
198
     * @throws MissingTypeHintException
199
     */
200
    private function mapParameters(ReflectionMethod $refMethod, \ReflectionMethod $standardRefMethod): array
201
    {
202
        $args = [];
203
204
        $typeResolver = new \phpDocumentor\Reflection\TypeResolver();
205
206
        foreach ($standardRefMethod->getParameters() as $standardParameter) {
207
            $allowsNull = $standardParameter->allowsNull();
208
            $parameter = $refMethod->getParameter($standardParameter->getName());
209
210
            $type = (string) $parameter->getType();
211
            if ($type === '') {
212
                throw MissingTypeHintException::missingTypeHint($parameter);
213
            }
214
            $phpdocType = $typeResolver->resolve($type);
215
216
            try {
217
                $arr = [
218
                    'type' => $this->mapType($phpdocType, $parameter->getDocBlockTypes(), $allowsNull || $parameter->isDefaultValueAvailable(), true),
219
                ];
220
            } catch (TypeMappingException $e) {
221
                throw TypeMappingException::wrapWithParamInfo($e, $parameter);
222
            }
223
224
            if ($standardParameter->allowsNull()) {
225
                $arr['default'] = null;
226
            }
227
            if ($standardParameter->isDefaultValueAvailable()) {
228
                $arr['default'] = $standardParameter->getDefaultValue();
229
            }
230
231
            $args[$parameter->getName()] = $arr;
232
        }
233
234
        return $args;
235
    }
236
237
    /**
238
     * @param Type $type
239
     * @param Type[] $docBlockTypes
240
     * @return TypeInterface
241
     */
242
    private function mapType(Type $type, array $docBlockTypes, bool $isNullable, bool $mapToInputType): TypeInterface
243
    {
244
        $graphQlType = null;
245
246
        if ($type instanceof Array_ || $type instanceof Mixed_) {
247
            if (!$isNullable) {
248
                // Let's check a "null" value in the docblock
249
                $isNullable = $this->isNullable($docBlockTypes);
250
            }
251
            $filteredDocBlockTypes = $this->typesWithoutNullable($docBlockTypes);
252
            if (empty($filteredDocBlockTypes)) {
253
                throw TypeMappingException::createFromType($type);
254
            } elseif (count($filteredDocBlockTypes) === 1) {
255
                $graphQlType = $this->toGraphQlType($filteredDocBlockTypes[0], $mapToInputType);
256
            } else {
257
                throw new GraphQLException('Union types are not supported (yet)');
258
                //$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...
259
                //$$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...
260
            }
261
        } else {
262
            $graphQlType = $this->toGraphQlType($type, $mapToInputType);
263
        }
264
265
        if (!$isNullable) {
266
            $graphQlType = new NonNullType($graphQlType);
267
        }
268
269
        return $graphQlType;
270
    }
271
272
    /**
273
     * Casts a Type to a GraphQL type.
274
     * Does not deal with nullable.
275
     *
276
     * @param Type $type
277
     * @param bool $mapToInputType
278
     * @return TypeInterface
279
     */
280
    private function toGraphQlType(Type $type, bool $mapToInputType): TypeInterface
281
    {
282
        if ($type instanceof Integer) {
283
            return new IntType();
284
        } elseif ($type instanceof String_) {
285
            return new StringType();
286
        } elseif ($type instanceof Boolean) {
287
            return new BooleanType();
288
        } elseif ($type instanceof Float_) {
289
            return new FloatType();
290
        } elseif ($type instanceof Object_) {
291
            $fqcn = (string) $type->getFqsen();
292
            if ($fqcn === '\\DateTimeImmutable' || $fqcn === '\\DateTimeInterface') {
293
                return new DateTimeType();
294
            } elseif ($fqcn === '\\DateTime') {
295
                throw new GraphQLException('Type-hinting a parameter against DateTime is not allowed. Please use the DateTimeImmutable type instead.');
296
            }
297
298
            $className = ltrim($type->getFqsen(), '\\');
299
            if ($mapToInputType) {
300
                return $this->typeMapper->mapClassToInputType($className);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->typeMapper...ToInputType($className) returns the type Youshido\GraphQL\Type\InputTypeInterface which includes types incompatible with the type-hinted return Youshido\GraphQL\Type\TypeInterface.
Loading history...
301
            } else {
302
                return $this->typeMapper->mapClassToType($className);
303
            }
304
        } elseif ($type instanceof Array_) {
305
            return new ListType(new NonNullType($this->toGraphQlType($type->getValueType(), $mapToInputType)));
306
        } else {
307
            throw TypeMappingException::createFromType($type);
308
        }
309
    }
310
311
    /**
312
     * Removes "null" from the list of types.
313
     *
314
     * @param Type[] $docBlockTypeHints
315
     * @return array
316
     */
317
    private function typesWithoutNullable(array $docBlockTypeHints): array
318
    {
319
        return array_filter($docBlockTypeHints, function ($item) {
320
            return !$item instanceof Null_;
0 ignored issues
show
Bug introduced by
The type TheCodingMachine\GraphQL\Controllers\Null_ 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...
321
        });
322
    }
323
324
    /**
325
     * @param Type[] $docBlockTypeHints
326
     * @return bool
327
     */
328
    private function isNullable(array $docBlockTypeHints): bool
329
    {
330
        foreach ($docBlockTypeHints as $docBlockTypeHint) {
331
            if ($docBlockTypeHint instanceof Null_) {
332
                return true;
333
            }
334
        }
335
        return false;
336
    }
337
}
338