Completed
Pull Request — master (#2)
by
unknown
01:57
created

getMethodFullName()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 1
crap 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace GraphQL\Doctrine\Factory;
6
7
use Doctrine\Common\Annotations\AnnotationReader;
8
use Doctrine\Common\Annotations\Reader;
9
use Doctrine\Common\Collections\Collection;
10
use Doctrine\Common\Persistence\Mapping\Driver\AnnotationDriver;
11
use Doctrine\Common\Persistence\Mapping\Driver\MappingDriverChain;
12
use Doctrine\ORM\EntityManager;
13
use Doctrine\ORM\Mapping\ClassMetadata;
14
use GraphQL\Doctrine\Annotation\Exclude;
15
use GraphQL\Doctrine\Exception;
16
use GraphQL\Doctrine\Types;
17
use GraphQL\Type\Definition\InputType;
18
use GraphQL\Type\Definition\NonNull;
19
use GraphQL\Type\Definition\Type;
20
use GraphQL\Type\Definition\WrappingType;
21
use ReflectionClass;
22
use ReflectionMethod;
23
use ReflectionParameter;
24
use ReflectionType;
25
26
/**
27
 * A factory to create a configuration for all fields of an entity
28
 */
29
abstract class AbstractFieldsConfigurationFactory
30
{
31
    /**
32
     * @var Types
33
     */
34
    private $types;
35
36
    /**
37
     * @var EntityManager
38
     */
39
    private $entityManager;
40
41
    /**
42
     * Doctrine metadata for the entity
43
     * @var ClassMetadata
44
     */
45
    private $metadata;
46
47
    /**
48
     * The identity field name, eg: "id"
49
     * @var string
50
     */
51
    private $identityField;
52
53 11
    public function __construct(Types $types, EntityManager $entityManager)
0 ignored issues
show
Bug introduced by
You have injected the EntityManager via parameter $entityManager. This is generally not recommended as it might get closed and become unusable. Instead, it is recommended to inject the ManagerRegistry and retrieve the EntityManager via getManager() each time you need it.

The EntityManager might become unusable for example if a transaction is rolled back and it gets closed. Let’s assume that somewhere in your application, or in a third-party library, there is code such as the following:

function someFunction(ManagerRegistry $registry) {
    $em = $registry->getManager();
    $em->getConnection()->beginTransaction();
    try {
        // Do something.
        $em->getConnection()->commit();
    } catch (\Exception $ex) {
        $em->getConnection()->rollback();
        $em->close();

        throw $ex;
    }
}

If that code throws an exception and the EntityManager is closed. Any other code which depends on the same instance of the EntityManager during this request will fail.

On the other hand, if you instead inject the ManagerRegistry, the getManager() method guarantees that you will always get a usable manager instance.

Loading history...
54
    {
55 11
        $this->types = $types;
56 11
        $this->entityManager = $entityManager;
57 11
    }
58
59
    /**
60
     * Returns the regexp pattern to filter method names
61
     */
62
    abstract protected function getMethodPattern(): string;
63
64
    /**
65
     * Get the entire configuration for a method
66
     * @param ReflectionMethod $method
67
     * @return array
68
     */
69
    abstract protected function methodToConfiguration(ReflectionMethod $method): ?array;
70
71
    /**
72
     * Create a configuration for all fields of Doctrine entity
73
     * @param string $className
74
     * @return array
75
     */
76 11
    public function create(string $className): array
77
    {
78 11
        $this->findIdentityField($className);
79
80 11
        $class = new ReflectionClass($className);
81 11
        $methods = $class->getMethods(ReflectionMethod::IS_PUBLIC);
82 11
        $fieldConfigurations = [];
83 11
        foreach ($methods as $method) {
84
            // Skip non-callable, non-instance or non-getter methods
85 11
            if ($method->isAbstract() || $method->isStatic()) {
86 1
                continue;
87
            }
88
89
            // Skip non-getter methods
90 11
            $name = $method->getName();
91 11
            if (!preg_match($this->getMethodPattern(), $name)) {
92 3
                continue;
93
            }
94
95
            // Skip exclusion specified by user
96 11
            if ($this->isExcluded($method)) {
97 2
                continue;
98
            }
99
100 11
            $configuration = $this->methodToConfiguration($method);
101 4
            if ($configuration) {
102 4
                $fieldConfigurations[] = $configuration;
103
            }
104
        }
105
106 4
        return $fieldConfigurations;
107
    }
108
109
    /**
110
     * Returns whether the getter is excluded
111
     * @param ReflectionMethod $method
112
     * @return bool
113
     */
114 11
    private function isExcluded(ReflectionMethod $method): bool
115
    {
116 11
        $exclude = $this->getAnnotationReader()->getMethodAnnotation($method, Exclude::class);
117
118 11
        return $exclude !== null;
119
    }
120
121
    /**
122
     * Get annotation reader
123
     * @return Reader
124
     */
125 11
    protected function getAnnotationReader(): Reader
126
    {
127 11
        $driver = $this->entityManager->getConfiguration()->getMetadataDriverImpl();
128 11
        if (is_a($driver, MappingDriverChain::class)) {
129
            $drivers = $driver->getDrivers();
130
            foreach ($drivers as $driver) {
131
                if (is_a($driver, AnnotationDriver::class))
132
                    return $driver->getReader();
133
            }
134
        }
135 11
        if (method_exists($driver, 'getReader'))
136 11
            return $driver->getReader();
137
        return new AnnotationReader();
138
    }
139
140
    /**
141
     * Get instance of GraphQL type from a PHP class name
142
     *
143
     * Supported syntaxes are the following:
144
     *
145
     *  - `?MyType`
146
     *  - `null|MyType`
147
     *  - `MyType|null`
148
     *  - `MyType[]`
149
     *  - `?MyType[]`
150
     *  - `null|MyType[]`
151
     *  - `MyType[]|null`
152
     *
153
     * @param ReflectionMethod $method
154
     * @param string|null $typeDeclaration
155
     * @param bool $isEntityId
156
     * @return Type|null
157
     */
158 11
    protected function getTypeFromPhpDeclaration(ReflectionMethod $method, ?string $typeDeclaration, bool $isEntityId = false): ?Type
159
    {
160 11
        if (!$typeDeclaration) {
161 11
            return null;
162
        }
163
164 7
        $isNullable = 0;
165 7
        $name = preg_replace('~(^\?|^null\||\|null$)~', '', $typeDeclaration, -1, $isNullable);
166
167 7
        $isList = 0;
168 7
        $name = preg_replace('~^(.*)\[\]$~', '$1', $name, -1, $isList);
169 7
        $name = $this->adjustNamespace($method, $name);
170 7
        $type = $this->getTypeFromRegistry($name, $isEntityId);
171
172 7
        if ($isList) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $isList of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. 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 integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
173 3
            $type = Type::listOf($type);
174
        }
175
176 7
        if (!$isNullable) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $isNullable of type integer|null is loosely compared to false; this is ambiguous if the integer can be zero. 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 integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
177 7
            $type = Type::nonNull($type);
178
        }
179
180 7
        return $type;
181
    }
182
183
    /**
184
     * Prepend namespace of the method if the class actually exists
185
     * @param ReflectionMethod $method
186
     * @param string $type
187
     * @return string
188
     */
189 7
    private function adjustNamespace(ReflectionMethod $method, string $type): string
190
    {
191 7
        $namespace = $method->getDeclaringClass()->getNamespaceName();
192 7
        if ($namespace) {
193 7
            $namespace = $namespace . '\\';
194
        }
195 7
        $namespacedType = $namespace . $type;
196
197 7
        return class_exists($namespacedType) ? $namespacedType : $type;
198
    }
199
200
    /**
201
     * Get a GraphQL type instance from PHP type hinted type, possibly looking up the content of collections
202
     * @param ReflectionMethod $method
203
     * @param string $fieldName
204
     * @throws Exception
205
     * @return Type|null
206
     */
207 5
    protected function getTypeFromReturnTypeHint(ReflectionMethod $method, string $fieldName): ?Type
208
    {
209 5
        $returnType = $method->getReturnType();
210 5
        if (!$returnType) {
211 1
            return null;
212
        }
213
214 4
        $returnTypeName = (string) $returnType;
215 4
        if (is_a($returnTypeName, Collection::class, true) || $returnTypeName === 'array') {
216 2
            $targetEntity = $this->getTargetEntity($fieldName);
217 2
            if (!$targetEntity) {
218 1
                throw new Exception('The method ' . $this->getMethodFullName($method) . ' is type hinted with a return type of `' . $returnTypeName . '`, but the entity contained in that collection could not be automatically detected. Either fix the type hint, fix the doctrine mapping, or specify the type with `@API\Field` annotation.');
219
            }
220
221 1
            $type = Type::listOf($this->getTypeFromRegistry($targetEntity, false));
222 1
            if (!$returnType->allowsNull()) {
223 1
                $type = Type::nonNull($type);
224
            }
225
226 1
            return $type;
227
        }
228
229 3
        return $this->refelectionTypeToType($returnType);
230
    }
231
232
    /**
233
     * Convert a reflected type to GraphQL Type
234
     * @param ReflectionType $reflectionType
235
     * @param bool $isEntityId
236
     * @return Type
237
     */
238 4
    protected function refelectionTypeToType(ReflectionType $reflectionType, bool $isEntityId = false): Type
239
    {
240 4
        $type = $this->getTypeFromRegistry((string) $reflectionType, $isEntityId);
241 4
        if (!$reflectionType->allowsNull()) {
242 4
            $type = Type::nonNull($type);
243
        }
244
245 4
        return $type;
246
    }
247
248
    /**
249
     * Look up which field is the ID
250
     * @param string $className
251
     */
252 11
    private function findIdentityField(string $className)
253
    {
254 11
        $this->metadata = $this->entityManager->getClassMetadata($className);
255 11
        foreach ($this->metadata->fieldMappings as $meta) {
256 11
            if ($meta['id'] ?? false) {
257 11
                $this->identityField = $meta['fieldName'];
258
            }
259
        }
260 11
    }
261
262
    /**
263
     * Returns the fully qualified method name
264
     * @param ReflectionMethod $method
265
     * @return string
266
     */
267 7
    protected function getMethodFullName(ReflectionMethod $method): string
268
    {
269 7
        return '`' . $method->getDeclaringClass()->getName() . '::' . $method->getName() . '()`';
270
    }
271
272
    /**
273
     * Throws exception if type is an array
274
     * @param ReflectionParameter $param
275
     * @param string|null $type
276
     * @throws Exception
277
     */
278 7
    protected function throwIfArray(ReflectionParameter $param, ?string $type)
279
    {
280 7
        if ($type === 'array') {
281 1
            throw new Exception('The parameter `$' . $param->getName() . '` on method ' . $this->getMethodFullName($param->getDeclaringFunction()) . ' is type hinted as `array` and is not overriden via `@API\Argument` annotation. Either change the type hint or specify the type with `@API\Argument` annotation.');
0 ignored issues
show
Documentation introduced by
$param->getDeclaringFunction() is of type object<ReflectionFunction>, but the function expects a object<ReflectionMethod>.

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...
282
        }
283 6
    }
284
285
    /**
286
     * Returns whether the given field name is the identity for the entity
287
     * @param string $fieldName
288
     * @return bool
289
     */
290 9
    protected function isIdentityField(string $fieldName): bool
291
    {
292 9
        return $this->identityField === $fieldName;
293
    }
294
295
    /**
296
     * Finds the target entity in the given association
297
     * @param string $fieldName
298
     * @return string|null
299
     */
300 2
    private function getTargetEntity(string $fieldName): ?string
301
    {
302 2
        return $this->metadata->associationMappings[$fieldName]['targetEntity'] ?? null;
303
    }
304
305
    /**
306
     * Returns a type from our registry
307
     * @param string $type
308
     * @param bool $isEntityid
309
     * @return Type
310
     */
311 8
    private function getTypeFromRegistry(string $type, bool $isEntityid): Type
312
    {
313 8
        if (!$this->types->isEntity($type) || !$isEntityid) {
314 8
            return $this->types->get($type);
315
        }
316
317 2
        return $this->types->getId($type);
318
    }
319
320
    /**
321
     * Input with default values cannot be non-null
322
     * @param ReflectionParameter $param
323
     * @param Type $type
324
     * @return Type
325
     */
326 7
    protected function nonNullIfHasDefault(ReflectionParameter $param, ?Type $type): ?Type
327
    {
328 7
        if ($type instanceof NonNull && $param->isDefaultValueAvailable()) {
329 3
            return $type->getWrappedType();
330
        }
331
332 7
        return $type;
333
    }
334
335
    /**
336
     * Throws exception if argument type is invalid
337
     * @param ReflectionParameter $param
338
     * @param Type $type
339
     * @throws Exception
340
     */
341 7
    protected function throwIfNotInputType(ReflectionParameter $param, ?Type $type, string $annotation)
342
    {
343 7
        if (!$type) {
344 2
            throw new Exception('Could not find type for parameter `$' . $param->name . '` for method ' . $this->getMethodFullName($param->getDeclaringFunction()) . '. Either type hint the parameter, or specify the type with `@API\\' . $annotation . '` annotation.');
0 ignored issues
show
Documentation introduced by
$param->getDeclaringFunction() is of type object<ReflectionFunction>, but the function expects a object<ReflectionMethod>.

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...
345
        }
346
347 5
        if ($type instanceof WrappingType) {
348 5
            $type = $type->getWrappedType(true);
349
        }
350
351 5
        if (!($type instanceof InputType)) {
352 1
            throw new Exception('Type for parameter `$' . $param->name . '` for method ' . $this->getMethodFullName($param->getDeclaringFunction()) . ' must be an instance of `' . InputType::class . '`, but was `' . get_class($type) . '`. Use `@API\\' . $annotation . '` annotation to specify a custom InputType.');
0 ignored issues
show
Documentation introduced by
$param->getDeclaringFunction() is of type object<ReflectionFunction>, but the function expects a object<ReflectionMethod>.

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...
353
        }
354 4
    }
355
}
356