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