1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
namespace GraphQL\Doctrine\Factory; |
6
|
|
|
|
7
|
|
|
use Doctrine\Common\Annotations\Reader; |
8
|
|
|
use Doctrine\Common\Collections\Collection; |
9
|
|
|
use Doctrine\Common\Persistence\Mapping\Driver\AnnotationDriver; |
10
|
|
|
use Doctrine\Common\Persistence\Mapping\Driver\MappingDriverChain; |
11
|
|
|
use Doctrine\ORM\EntityManager; |
12
|
|
|
use Doctrine\ORM\Mapping\ClassMetadata; |
13
|
|
|
use GraphQL\Doctrine\Annotation\Exclude; |
14
|
|
|
use GraphQL\Doctrine\Exception; |
15
|
|
|
use GraphQL\Doctrine\Types; |
16
|
|
|
use GraphQL\Type\Definition\InputType; |
17
|
|
|
use GraphQL\Type\Definition\NonNull; |
18
|
|
|
use GraphQL\Type\Definition\Type; |
19
|
|
|
use GraphQL\Type\Definition\WrappingType; |
20
|
|
|
use ReflectionClass; |
21
|
|
|
use ReflectionMethod; |
22
|
|
|
use ReflectionParameter; |
23
|
|
|
use ReflectionType; |
24
|
|
|
|
25
|
|
|
/** |
26
|
|
|
* A factory to create a configuration for all fields of an entity |
27
|
|
|
*/ |
28
|
|
|
abstract class AbstractFieldsConfigurationFactory |
29
|
|
|
{ |
30
|
|
|
/** |
31
|
|
|
* @var Types |
32
|
|
|
*/ |
33
|
|
|
private $types; |
34
|
|
|
|
35
|
|
|
/** |
36
|
|
|
* @var EntityManager |
37
|
|
|
*/ |
38
|
|
|
private $entityManager; |
39
|
|
|
|
40
|
|
|
/** |
41
|
|
|
* Doctrine metadata for the entity |
42
|
|
|
* |
43
|
|
|
* @var ClassMetadata |
44
|
|
|
*/ |
45
|
|
|
private $metadata; |
46
|
|
|
|
47
|
|
|
/** |
48
|
|
|
* The identity field name, eg: "id" |
49
|
|
|
* |
50
|
|
|
* @var string |
51
|
|
|
*/ |
52
|
|
|
private $identityField; |
53
|
|
|
|
54
|
12 |
|
public function __construct(Types $types, EntityManager $entityManager) |
|
|
|
|
55
|
|
|
{ |
56
|
12 |
|
$this->types = $types; |
57
|
12 |
|
$this->entityManager = $entityManager; |
58
|
12 |
|
} |
59
|
|
|
|
60
|
|
|
/** |
61
|
|
|
* Returns the regexp pattern to filter method names |
62
|
|
|
*/ |
63
|
|
|
abstract protected function getMethodPattern(): string; |
64
|
|
|
|
65
|
|
|
/** |
66
|
|
|
* Get the entire configuration for a method |
67
|
|
|
* |
68
|
|
|
* @param ReflectionMethod $method |
69
|
|
|
* |
70
|
|
|
* @return null|array |
71
|
|
|
*/ |
72
|
|
|
abstract protected function methodToConfiguration(ReflectionMethod $method): ?array; |
73
|
|
|
|
74
|
|
|
/** |
75
|
|
|
* Create a configuration for all fields of Doctrine entity |
76
|
|
|
* |
77
|
|
|
* @param string $className |
78
|
|
|
* |
79
|
|
|
* @return array |
80
|
|
|
*/ |
81
|
12 |
|
public function create(string $className): array |
82
|
|
|
{ |
83
|
12 |
|
$this->findIdentityField($className); |
84
|
|
|
|
85
|
12 |
|
$class = new ReflectionClass($className); |
86
|
12 |
|
$methods = $class->getMethods(ReflectionMethod::IS_PUBLIC); |
87
|
12 |
|
$fieldConfigurations = []; |
88
|
12 |
|
foreach ($methods as $method) { |
89
|
|
|
// Skip non-callable, non-instance or non-getter methods |
90
|
12 |
|
if ($method->isAbstract() || $method->isStatic()) { |
91
|
1 |
|
continue; |
92
|
|
|
} |
93
|
|
|
|
94
|
|
|
// Skip non-getter methods |
95
|
12 |
|
$name = $method->getName(); |
96
|
12 |
|
if (!preg_match($this->getMethodPattern(), $name)) { |
97
|
5 |
|
continue; |
98
|
|
|
} |
99
|
|
|
|
100
|
|
|
// Skip exclusion specified by user |
101
|
12 |
|
if ($this->isExcluded($method)) { |
102
|
3 |
|
continue; |
103
|
|
|
} |
104
|
|
|
|
105
|
12 |
|
$configuration = $this->methodToConfiguration($method); |
106
|
5 |
|
if ($configuration) { |
107
|
5 |
|
$fieldConfigurations[] = $configuration; |
108
|
|
|
} |
109
|
|
|
} |
110
|
|
|
|
111
|
5 |
|
return $fieldConfigurations; |
112
|
|
|
} |
113
|
|
|
|
114
|
|
|
/** |
115
|
|
|
* Returns whether the getter is excluded |
116
|
|
|
* |
117
|
|
|
* @param ReflectionMethod $method |
118
|
|
|
* |
119
|
|
|
* @return bool |
120
|
|
|
*/ |
121
|
12 |
|
private function isExcluded(ReflectionMethod $method): bool |
122
|
|
|
{ |
123
|
12 |
|
$exclude = $this->getAnnotationReader()->getMethodAnnotation($method, Exclude::class); |
124
|
|
|
|
125
|
12 |
|
return $exclude !== null; |
126
|
|
|
} |
127
|
|
|
|
128
|
|
|
/** |
129
|
|
|
* Get annotation reader |
130
|
|
|
* |
131
|
|
|
* @return Reader |
132
|
|
|
*/ |
133
|
12 |
|
protected function getAnnotationReader(): Reader |
134
|
|
|
{ |
135
|
12 |
|
$mappingDriver = $this->entityManager->getConfiguration()->getMetadataDriverImpl(); |
136
|
12 |
|
if ($mappingDriver instanceof AnnotationDriver) { |
137
|
12 |
|
return $mappingDriver->getReader(); |
138
|
|
|
} |
139
|
|
|
if ($mappingDriver instanceof MappingDriverChain) { |
140
|
|
|
foreach ($mappingDriver->getDrivers() as $driver) { |
141
|
|
|
if ($driver instanceof AnnotationDriver) { |
142
|
|
|
return $driver->getReader(); |
143
|
|
|
} |
144
|
|
|
} |
145
|
|
|
} |
146
|
|
|
|
147
|
|
|
throw new \Exception('AnnotationDriver not found in the entityManager'); |
148
|
|
|
} |
149
|
|
|
|
150
|
|
|
/** |
151
|
|
|
* Get instance of GraphQL type from a PHP class name |
152
|
|
|
* |
153
|
|
|
* Supported syntaxes are the following: |
154
|
|
|
* |
155
|
|
|
* - `?MyType` |
156
|
|
|
* - `null|MyType` |
157
|
|
|
* - `MyType|null` |
158
|
|
|
* - `MyType[]` |
159
|
|
|
* - `?MyType[]` |
160
|
|
|
* - `null|MyType[]` |
161
|
|
|
* - `MyType[]|null` |
162
|
|
|
* |
163
|
|
|
* @param ReflectionMethod $method |
164
|
|
|
* @param null|string $typeDeclaration |
165
|
|
|
* @param bool $isEntityId |
166
|
|
|
* |
167
|
|
|
* @return null|Type |
168
|
|
|
*/ |
169
|
12 |
|
protected function getTypeFromPhpDeclaration(ReflectionMethod $method, ?string $typeDeclaration, bool $isEntityId = false): ?Type |
170
|
|
|
{ |
171
|
12 |
|
if (!$typeDeclaration) { |
172
|
12 |
|
return null; |
173
|
|
|
} |
174
|
|
|
|
175
|
8 |
|
$isNullable = 0; |
176
|
8 |
|
$name = preg_replace('~(^\?|^null\||\|null$)~', '', $typeDeclaration, -1, $isNullable); |
177
|
|
|
|
178
|
8 |
|
$isList = 0; |
179
|
8 |
|
$name = preg_replace('~^(.*)\[\]$~', '$1', $name, -1, $isList); |
180
|
8 |
|
$name = $this->adjustNamespace($method, $name); |
181
|
8 |
|
$type = $this->getTypeFromRegistry($name, $isEntityId); |
182
|
|
|
|
183
|
8 |
|
if ($isList) { |
|
|
|
|
184
|
4 |
|
$type = Type::listOf($type); |
185
|
|
|
} |
186
|
|
|
|
187
|
8 |
|
if (!$isNullable) { |
|
|
|
|
188
|
8 |
|
$type = Type::nonNull($type); |
189
|
|
|
} |
190
|
|
|
|
191
|
8 |
|
return $type; |
192
|
|
|
} |
193
|
|
|
|
194
|
|
|
/** |
195
|
|
|
* Prepend namespace of the method if the class actually exists |
196
|
|
|
* |
197
|
|
|
* @param ReflectionMethod $method |
198
|
|
|
* @param string $type |
199
|
|
|
* |
200
|
|
|
* @return string |
201
|
|
|
*/ |
202
|
8 |
|
private function adjustNamespace(ReflectionMethod $method, string $type): string |
203
|
|
|
{ |
204
|
8 |
|
$namespace = $method->getDeclaringClass()->getNamespaceName(); |
205
|
8 |
|
if ($namespace) { |
206
|
8 |
|
$namespace = $namespace . '\\'; |
207
|
|
|
} |
208
|
8 |
|
$namespacedType = $namespace . $type; |
209
|
|
|
|
210
|
8 |
|
return class_exists($namespacedType) ? $namespacedType : $type; |
211
|
|
|
} |
212
|
|
|
|
213
|
|
|
/** |
214
|
|
|
* Get a GraphQL type instance from PHP type hinted type, possibly looking up the content of collections |
215
|
|
|
* |
216
|
|
|
* @param ReflectionMethod $method |
217
|
|
|
* @param string $fieldName |
218
|
|
|
* |
219
|
|
|
* @throws Exception |
220
|
|
|
* |
221
|
|
|
* @return null|Type |
222
|
|
|
*/ |
223
|
6 |
|
protected function getTypeFromReturnTypeHint(ReflectionMethod $method, string $fieldName): ?Type |
224
|
|
|
{ |
225
|
6 |
|
$returnType = $method->getReturnType(); |
226
|
6 |
|
if (!$returnType) { |
227
|
1 |
|
return null; |
228
|
|
|
} |
229
|
|
|
|
230
|
5 |
|
$returnTypeName = (string) $returnType; |
231
|
5 |
|
if (is_a($returnTypeName, Collection::class, true) || $returnTypeName === 'array') { |
232
|
3 |
|
$targetEntity = $this->getTargetEntity($fieldName); |
233
|
3 |
|
if (!$targetEntity) { |
234
|
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.'); |
235
|
|
|
} |
236
|
|
|
|
237
|
2 |
|
$type = Type::listOf($this->getTypeFromRegistry($targetEntity, false)); |
238
|
2 |
|
if (!$returnType->allowsNull()) { |
239
|
2 |
|
$type = Type::nonNull($type); |
240
|
|
|
} |
241
|
|
|
|
242
|
2 |
|
return $type; |
243
|
|
|
} |
244
|
|
|
|
245
|
4 |
|
return $this->reflectionTypeToType($returnType); |
246
|
|
|
} |
247
|
|
|
|
248
|
|
|
/** |
249
|
|
|
* Convert a reflected type to GraphQL Type |
250
|
|
|
* |
251
|
|
|
* @param ReflectionType $reflectionType |
252
|
|
|
* @param bool $isEntityId |
253
|
|
|
* |
254
|
|
|
* @return Type |
255
|
|
|
*/ |
256
|
5 |
|
protected function reflectionTypeToType(ReflectionType $reflectionType, bool $isEntityId = false): Type |
257
|
|
|
{ |
258
|
5 |
|
$type = $this->getTypeFromRegistry((string) $reflectionType, $isEntityId); |
259
|
5 |
|
if (!$reflectionType->allowsNull()) { |
260
|
5 |
|
$type = Type::nonNull($type); |
261
|
|
|
} |
262
|
|
|
|
263
|
5 |
|
return $type; |
264
|
|
|
} |
265
|
|
|
|
266
|
|
|
/** |
267
|
|
|
* Look up which field is the ID |
268
|
|
|
* |
269
|
|
|
* @param string $className |
270
|
|
|
*/ |
271
|
12 |
|
private function findIdentityField(string $className): void |
272
|
|
|
{ |
273
|
12 |
|
$this->metadata = $this->entityManager->getClassMetadata($className); |
274
|
12 |
|
foreach ($this->metadata->fieldMappings as $meta) { |
275
|
12 |
|
if ($meta['id'] ?? false) { |
276
|
12 |
|
$this->identityField = $meta['fieldName']; |
277
|
|
|
} |
278
|
|
|
} |
279
|
12 |
|
} |
280
|
|
|
|
281
|
|
|
/** |
282
|
|
|
* Returns the fully qualified method name |
283
|
|
|
* |
284
|
|
|
* @param ReflectionMethod $method |
285
|
|
|
* |
286
|
|
|
* @return string |
287
|
|
|
*/ |
288
|
7 |
|
protected function getMethodFullName(ReflectionMethod $method): string |
289
|
|
|
{ |
290
|
7 |
|
return '`' . $method->getDeclaringClass()->getName() . '::' . $method->getName() . '()`'; |
291
|
|
|
} |
292
|
|
|
|
293
|
|
|
/** |
294
|
|
|
* Throws exception if type is an array |
295
|
|
|
* |
296
|
|
|
* @param ReflectionParameter $param |
297
|
|
|
* @param null|string $type |
298
|
|
|
* |
299
|
|
|
* @throws Exception |
300
|
|
|
*/ |
301
|
8 |
|
protected function throwIfArray(ReflectionParameter $param, ?string $type): void |
302
|
|
|
{ |
303
|
8 |
|
if ($type === 'array') { |
304
|
1 |
|
throw new Exception('The parameter `$' . $param->getName() . '` on method ' . $this->getMethodFullName($param->getDeclaringFunction()) . ' is type hinted as `array` and is not overridden via `@API\Argument` annotation. Either change the type hint or specify the type with `@API\Argument` annotation.'); |
|
|
|
|
305
|
|
|
} |
306
|
7 |
|
} |
307
|
|
|
|
308
|
|
|
/** |
309
|
|
|
* Returns whether the given field name is the identity for the entity |
310
|
|
|
* |
311
|
|
|
* @param string $fieldName |
312
|
|
|
* |
313
|
|
|
* @return bool |
314
|
|
|
*/ |
315
|
10 |
|
protected function isIdentityField(string $fieldName): bool |
316
|
|
|
{ |
317
|
10 |
|
return $this->identityField === $fieldName; |
318
|
|
|
} |
319
|
|
|
|
320
|
|
|
/** |
321
|
|
|
* Finds the target entity in the given association |
322
|
|
|
* |
323
|
|
|
* @param string $fieldName |
324
|
|
|
* |
325
|
|
|
* @return null|string |
326
|
|
|
*/ |
327
|
3 |
|
private function getTargetEntity(string $fieldName): ?string |
328
|
|
|
{ |
329
|
3 |
|
return $this->metadata->associationMappings[$fieldName]['targetEntity'] ?? null; |
330
|
|
|
} |
331
|
|
|
|
332
|
|
|
/** |
333
|
|
|
* Returns a type from our registry |
334
|
|
|
* |
335
|
|
|
* @param string $type |
336
|
|
|
* @param bool $isEntityId |
337
|
|
|
* |
338
|
|
|
* @return Type |
339
|
|
|
*/ |
340
|
9 |
|
private function getTypeFromRegistry(string $type, bool $isEntityId): Type |
341
|
|
|
{ |
342
|
9 |
|
if (!$this->types->isEntity($type) || !$isEntityId) { |
343
|
9 |
|
return $this->types->get($type); |
344
|
|
|
} |
345
|
|
|
|
346
|
3 |
|
return $this->types->getId($type); |
347
|
|
|
} |
348
|
|
|
|
349
|
|
|
/** |
350
|
|
|
* Input with default values cannot be non-null |
351
|
|
|
* |
352
|
|
|
* @param ReflectionParameter $param |
353
|
|
|
* @param Type $type |
354
|
|
|
* |
355
|
|
|
* @return Type |
356
|
|
|
*/ |
357
|
8 |
|
protected function nonNullIfHasDefault(ReflectionParameter $param, ?Type $type): ?Type |
358
|
|
|
{ |
359
|
8 |
|
if ($type instanceof NonNull && $param->isDefaultValueAvailable()) { |
360
|
4 |
|
return $type->getWrappedType(); |
361
|
|
|
} |
362
|
|
|
|
363
|
8 |
|
return $type; |
364
|
|
|
} |
365
|
|
|
|
366
|
|
|
/** |
367
|
|
|
* Throws exception if argument type is invalid |
368
|
|
|
* |
369
|
|
|
* @param ReflectionParameter $param |
370
|
|
|
* @param Type $type |
371
|
|
|
* @param string $annotation |
372
|
|
|
* |
373
|
|
|
* @throws Exception |
374
|
|
|
*/ |
375
|
8 |
|
protected function throwIfNotInputType(ReflectionParameter $param, ?Type $type, string $annotation): void |
376
|
|
|
{ |
377
|
8 |
|
if (!$type) { |
378
|
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.'); |
|
|
|
|
379
|
|
|
} |
380
|
|
|
|
381
|
6 |
|
if ($type instanceof WrappingType) { |
382
|
6 |
|
$type = $type->getWrappedType(true); |
383
|
|
|
} |
384
|
|
|
|
385
|
6 |
|
if (!($type instanceof InputType)) { |
386
|
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.'); |
|
|
|
|
387
|
|
|
} |
388
|
5 |
|
} |
389
|
|
|
} |
390
|
|
|
|
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:If that code throws an exception and the
EntityManager
is closed. Any other code which depends on the same instance of theEntityManager
during this request will fail.On the other hand, if you instead inject the
ManagerRegistry
, thegetManager()
method guarantees that you will always get a usable manager instance.