Passed
Push — master ( 7b307a...5aeb74 )
by David
02:11
created

ControllerQueryProvider::getQueries()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
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\Mixed;
9
use phpDocumentor\Reflection\Types\Object_;
10
use phpDocumentor\Reflection\Types\String_;
11
use Roave\BetterReflection\Reflection\ReflectionClass;
12
use Roave\BetterReflection\Reflection\ReflectionMethod;
13
use Doctrine\Common\Annotations\Reader;
14
use phpDocumentor\Reflection\Types\Integer;
15
use TheCodingMachine\GraphQL\Controllers\Annotations\Logged;
16
use TheCodingMachine\GraphQL\Controllers\Annotations\Mutation;
17
use TheCodingMachine\GraphQL\Controllers\Annotations\Query;
18
use TheCodingMachine\GraphQL\Controllers\Annotations\Right;
19
use TheCodingMachine\GraphQL\Controllers\Security\AuthenticationServiceInterface;
20
use TheCodingMachine\GraphQL\Controllers\Security\AuthorizationServiceInterface;
21
use Youshido\GraphQL\Field\Field;
22
use Youshido\GraphQL\Type\ListType\ListType;
23
use Youshido\GraphQL\Type\NonNullType;
24
use Youshido\GraphQL\Type\Scalar\IntType;
25
use Youshido\GraphQL\Type\Scalar\StringType;
26
use Youshido\GraphQL\Type\TypeInterface;
27
use Youshido\GraphQL\Type\Union\UnionType;
28
29
/**
30
 * A query provider that looks for queries in a "controller"
31
 */
32
class ControllerQueryProvider implements QueryProviderInterface
33
{
34
    /**
35
     * @var object
36
     */
37
    private $controller;
38
    /**
39
     * @var Reader
40
     */
41
    private $annotationReader;
42
    /**
43
     * @var TypeMapperInterface
44
     */
45
    private $typeMapper;
46
    /**
47
     * @var HydratorInterface
48
     */
49
    private $hydrator;
50
    /**
51
     * @var AuthenticationServiceInterface
52
     */
53
    private $authenticationService;
54
    /**
55
     * @var AuthorizationServiceInterface
56
     */
57
    private $authorizationService;
58
59
    /**
60
     * @param object $controller
61
     */
62 View Code Duplication
    public function __construct($controller, Reader $annotationReader, TypeMapperInterface $typeMapper, HydratorInterface $hydrator, AuthenticationServiceInterface $authenticationService, AuthorizationServiceInterface $authorizationService)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
63
    {
64
        $this->controller = $controller;
65
        $this->annotationReader = $annotationReader;
66
        $this->typeMapper = $typeMapper;
67
        $this->hydrator = $hydrator;
68
        $this->authenticationService = $authenticationService;
69
        $this->authorizationService = $authorizationService;
70
    }
71
72
    /**
73
     * @return Field[]
74
     */
75
    public function getQueries(): array
76
    {
77
        return $this->getFieldsByAnnotations(Query::class);
78
    }
79
80
    /**
81
     * @return Field[]
82
     */
83
    public function getMutations(): array
84
    {
85
        return $this->getFieldsByAnnotations(Mutation::class);
86
    }
87
88
    /**
89
     * @return Field[]
90
     */
91
    private function getFieldsByAnnotations(string $annotationName): array
92
    {
93
        $refClass = ReflectionClass::createFromInstance($this->controller);
94
95
        $queryList = [];
96
97
        foreach ($refClass->getMethods() as $refMethod) {
98
            $standardPhpMethod = new \ReflectionMethod(get_class($this->controller), $refMethod->getName());
99
            // First, let's check the "Query" annotation
100
            $queryAnnotation = $this->annotationReader->getMethodAnnotation($standardPhpMethod, $annotationName);
101
102
            if ($queryAnnotation !== null) {
103
                if (!$this->isAuthorized($standardPhpMethod)) {
104
                    continue;
105
                }
106
107
                $methodName = $refMethod->getName();
108
109
                $args = $this->mapParameters($refMethod, $standardPhpMethod);
110
111
                $type = $this->mapType($refMethod->getReturnType()->getTypeObject(), $refMethod->getDocBlockReturnTypes(), $standardPhpMethod->getReturnType()->allowsNull());
112
113
                $queryList[] = new QueryField($methodName, $type, $args, [$this->controller, $methodName], $this->hydrator);
114
            }
115
        }
116
117
        return $queryList;
118
    }
119
120
    /**
121
     * Checks the @Logged and @Right annotations.
122
     *
123
     * @param \ReflectionMethod $reflectionMethod
124
     * @return bool
125
     */
126
    private function isAuthorized(\ReflectionMethod $reflectionMethod) : bool
127
    {
128
        $loggedAnnotation = $this->annotationReader->getMethodAnnotation($reflectionMethod, Logged::class);
129
130
        if ($loggedAnnotation !== null && !$this->authenticationService->isLogged()) {
131
            return false;
132
        }
133
134
        $rightAnnotation = $this->annotationReader->getMethodAnnotation($reflectionMethod, Right::class);
135
        /** @var $rightAnnotation Right */
136
137
        if ($rightAnnotation !== null && !$this->authorizationService->isAllowed($rightAnnotation->getName())) {
138
            return false;
139
        }
140
141
        return true;
142
    }
143
144
    /**
145
     * Note: there is a bug in $refMethod->allowsNull that forces us to use $standardRefMethod->allowsNull instead.
146
     *
147
     * @param ReflectionMethod $refMethod
148
     * @param \ReflectionMethod $standardRefMethod
149
     * @return array
150
     */
151
    private function mapParameters(ReflectionMethod $refMethod, \ReflectionMethod $standardRefMethod)
152
    {
153
        $args = [];
154
        foreach ($standardRefMethod->getParameters() as $standardParameter) {
155
            $allowsNull = $standardParameter->allowsNull();
156
            $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...
157
            $args[$parameter->getName()] = $this->mapType($parameter->getTypeHint(), $parameter->getDocBlockTypes(), $allowsNull);
158
        }
159
160
        return $args;
161
    }
162
163
    /**
164
     * @param Type $type
165
     * @param Type[] $docBlockTypes
166
     * @return TypeInterface
167
     */
168
    private function mapType(Type $type, array $docBlockTypes, bool $isNullable): TypeInterface
169
    {
170
        $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...
171
172
        if ($type instanceof Array_ || $type instanceof Mixed) {
0 ignored issues
show
Bug introduced by
The class phpDocumentor\Reflection\Types\Mixed 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...
173
            if (!$isNullable) {
174
                // Let's check a "null" value in the docblock
175
                $isNullable = $this->isNullable($docBlockTypes);
176
            }
177
            $filteredDocBlockTypes = $this->typesWithoutNullable($docBlockTypes);
178
            if (empty($filteredDocBlockTypes)) {
179
                // TODO: improve error message
180
                throw new GraphQLException("Don't know how to handle type ".((string) $type));
181
            } elseif (count($filteredDocBlockTypes) === 1) {
182
                $graphQlType = $this->toGraphQlType($filteredDocBlockTypes[0]);
183
            } else {
184
                throw new GraphQLException('Union types are not supported (yet)');
185
                //$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...
186
                //$$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...
187
            }
188
        } else {
189
            $graphQlType = $this->toGraphQlType($type);
190
        }
191
192
        if (!$isNullable) {
193
            $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...
194
        }
195
196
        return $graphQlType;
197
    }
198
199
    /**
200
     * Casts a Type to a GraphQL type.
201
     * Does not deal with nullable.
202
     *
203
     * @param Type $type
204
     * @return TypeInterface
205
     */
206
    private function toGraphQlType(Type $type): TypeInterface
207
    {
208
        if ($type instanceof Integer) {
209
            return new IntType();
210
        } elseif ($type instanceof String_) {
211
            return new StringType();
212
        } elseif ($type instanceof Object_) {
213
            return $this->typeMapper->mapClassToType(ltrim($type->getFqsen(), '\\'));
214
        } elseif ($type instanceof Array_) {
215
            return new ListType(new NonNullType($this->toGraphQlType($type->getValueType())));
0 ignored issues
show
Documentation introduced by
$this->toGraphQlType($type->getValueType()) 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...
216
        } else {
217
            throw new GraphQLException("Don't know how to handle type ".((string) $type));
218
        }
219
    }
220
221
    /**
222
     * Removes "null" from the list of types.
223
     *
224
     * @param Type[] $docBlockTypeHints
225
     * @return array
226
     */
227
    private function typesWithoutNullable(array $docBlockTypeHints): array
228
    {
229
        return array_filter($docBlockTypeHints, function ($item) {
230
            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...
231
        });
232
    }
233
234
    /**
235
     * @param Type[] $docBlockTypeHints
236
     * @return bool
237
     */
238
    private function isNullable(array $docBlockTypeHints): bool
239
    {
240
        foreach ($docBlockTypeHints as $docBlockTypeHint) {
241
            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...
242
                return true;
243
            }
244
        }
245
        return false;
246
    }
247
}
248