1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
namespace Overblog\GraphQLBundle\Hydrator; |
6
|
|
|
|
7
|
|
|
use Doctrine\Common\Annotations\AnnotationReader; |
8
|
|
|
use Doctrine\Common\Annotations\AnnotationRegistry; |
9
|
|
|
use Doctrine\ORM\EntityManagerInterface; |
10
|
|
|
use Doctrine\ORM\Mapping as ORM; |
11
|
|
|
use Doctrine\ORM\NonUniqueResultException; |
12
|
|
|
use Exception; |
13
|
|
|
use GraphQL\Type\Definition\InputObjectType; |
14
|
|
|
use GraphQL\Type\Definition\ListOfType; |
15
|
|
|
use GraphQL\Type\Definition\NonNull; |
16
|
|
|
use GraphQL\Type\Definition\ResolveInfo; |
17
|
|
|
use GraphQL\Type\Definition\Type; |
18
|
|
|
use Overblog\GraphQLBundle\Config\Parser\AnnotationParser; |
19
|
|
|
use Overblog\GraphQLBundle\Definition\ArgumentInterface; |
20
|
|
|
use Overblog\GraphQLBundle\Hydrator\Annotation\Field; |
21
|
|
|
use Overblog\GraphQLBundle\Hydrator\Annotation\Model; |
22
|
|
|
use Overblog\GraphQLBundle\Hydrator\Converters\ConverterAnnotationInterface; |
23
|
|
|
use Overblog\GraphQLBundle\Hydrator\Converters\Entity; |
24
|
|
|
use ReflectionClass; |
25
|
|
|
use ReflectionException; |
26
|
|
|
use ReflectionProperty; |
27
|
|
|
use RuntimeException; |
28
|
|
|
use Symfony\Component\DependencyInjection\ServiceLocator; |
29
|
|
|
use Symfony\Component\PropertyAccess\PropertyAccessorInterface; |
30
|
|
|
|
31
|
|
|
class Hydrator |
32
|
|
|
{ |
33
|
|
|
private const ENTITY_ANNOTATIONS = [ |
34
|
|
|
ORM\OneToOne::class, |
35
|
|
|
ORM\OneToMany::class, |
36
|
|
|
ORM\ManyToOne::class, |
37
|
|
|
ORM\ManyToMany::class |
38
|
|
|
]; |
39
|
|
|
|
40
|
|
|
private static array $annotationCache = []; |
41
|
|
|
|
42
|
|
|
private AnnotationReader $annotationReader; |
43
|
|
|
private PropertyAccessorInterface $propertyAccessor; |
44
|
|
|
private EntityManagerInterface $em; |
45
|
|
|
private ServiceLocator $converters; |
46
|
|
|
private array $args; |
47
|
|
|
|
48
|
|
|
public function __construct( |
49
|
|
|
PropertyAccessorInterface $propertyAccessor, |
50
|
|
|
ServiceLocator $converters, |
51
|
|
|
EntityManagerInterface $entityManager |
52
|
|
|
) { |
53
|
|
|
if (!class_exists(AnnotationReader::class) || !class_exists(AnnotationRegistry::class)) { |
54
|
|
|
throw new RuntimeException('In order to use graphql annotation, you need to require doctrine annotations'); |
55
|
|
|
} |
56
|
|
|
|
57
|
|
|
AnnotationRegistry::registerLoader('class_exists'); |
|
|
|
|
58
|
|
|
|
59
|
|
|
$this->annotationReader = new AnnotationReader(); |
60
|
|
|
$this->propertyAccessor = $propertyAccessor; |
61
|
|
|
$this->converters = $converters; |
62
|
|
|
$this->em = $entityManager; |
63
|
|
|
} |
64
|
|
|
|
65
|
|
|
/** |
66
|
|
|
* @throws ORM\MappingException|NonUniqueResultException|ReflectionException|Exception |
67
|
|
|
*/ |
68
|
|
|
public function hydrate(ArgumentInterface $args, ResolveInfo $info): Models |
69
|
|
|
{ |
70
|
|
|
$this->args = $args->getArrayCopy(); |
71
|
|
|
$requestedField = $info->parentType->getField($info->fieldName); |
72
|
|
|
|
73
|
|
|
$models = new Models(); |
74
|
|
|
|
75
|
|
|
foreach ($this->args as $argName => $input) { |
76
|
|
|
/** @var ListOfType|NonNull $argType */ |
77
|
|
|
$argType = $requestedField->getArg($argName)->getType(); |
78
|
|
|
|
79
|
|
|
/** @var InputObjectType $inputType */ |
80
|
|
|
$inputType = $argType->getOfType(); |
81
|
|
|
|
82
|
|
|
if (!isset($inputType->config['model'])) { |
83
|
|
|
continue; |
84
|
|
|
} |
85
|
|
|
|
86
|
|
|
$models->models[$argName] = $this->hydrateInputType($inputType, $input); |
87
|
|
|
} |
88
|
|
|
|
89
|
|
|
return $models; |
90
|
|
|
} |
91
|
|
|
|
92
|
|
|
/** |
93
|
|
|
* @param mixed $inputValues |
94
|
|
|
* |
95
|
|
|
* @return object |
96
|
|
|
* |
97
|
|
|
* @throws ORM\MappingException|ReflectionException|NonUniqueResultException |
98
|
|
|
*/ |
99
|
|
|
private function hydrateInputType(InputObjectType $inputType, $inputValues, object $model = null): object |
100
|
|
|
{ |
101
|
|
|
if (empty($inputType->config['model'])) { |
102
|
|
|
return $inputValues; |
103
|
|
|
} |
104
|
|
|
|
105
|
|
|
$modelName = null !== $model ? get_class($model) : $inputType->config['model']; |
106
|
|
|
$modelReflection = new ReflectionClass($modelName); |
107
|
|
|
|
108
|
|
|
$entityAnnotation = $this->annotationReader->getClassAnnotation($modelReflection, Orm\Entity::class); |
109
|
|
|
|
110
|
|
|
if (null === $model) { |
111
|
|
|
if (null !== $entityAnnotation) { |
112
|
|
|
$model = $this->getEntityModel($modelName, $inputValues); |
113
|
|
|
} else { |
114
|
|
|
$model = new $modelName(); |
115
|
|
|
} |
116
|
|
|
} |
117
|
|
|
|
118
|
|
|
$annotationMapping = $this->readAnnotationMapping($modelReflection); |
119
|
|
|
$fields = $inputType->getFields(); |
120
|
|
|
|
121
|
|
|
foreach ($inputValues as $fieldName => $fieldValue) { |
122
|
|
|
if (empty($fields[$fieldName])) { |
123
|
|
|
continue; |
124
|
|
|
} |
125
|
|
|
|
126
|
|
|
$fieldObject = $fields[$fieldName]; |
127
|
|
|
$targetField = $annotationMapping[$fieldName] ?? $fieldName; |
128
|
|
|
|
129
|
|
|
if ($this->propertyAccessor->isWritable($model, $targetField)) { |
|
|
|
|
130
|
|
|
|
131
|
|
|
$resultValue = $this->resolveConverter( |
132
|
|
|
$modelName, |
133
|
|
|
$targetField, |
134
|
|
|
$fieldValue, |
135
|
|
|
$modelReflection, |
136
|
|
|
$fieldObject |
137
|
|
|
); |
138
|
|
|
|
139
|
|
|
$this->propertyAccessor->setValue( |
140
|
|
|
$model, |
|
|
|
|
141
|
|
|
$targetField, |
142
|
|
|
$resultValue |
143
|
|
|
); |
144
|
|
|
} |
145
|
|
|
} |
146
|
|
|
|
147
|
|
|
return $model; |
148
|
|
|
} |
149
|
|
|
|
150
|
|
|
public function resolveConverter($modelName, $targetField, $fieldValue, $modelReflection, $fieldObject) |
151
|
|
|
{ |
152
|
|
|
# 1. Check if converter declared explicitely ----------------------------------------------------------- |
153
|
|
|
$converterAnnotation = $this->getPropertyAnnotation($modelName, $targetField, ConverterAnnotationInterface::class); |
154
|
|
|
if (null !== $converterAnnotation) { |
155
|
|
|
$converter = $this->converters->get($converterAnnotation::getConverterClass()); |
156
|
|
|
$resultValue = $converter->convert($fieldValue, $converterAnnotation); |
157
|
|
|
} |
158
|
|
|
|
159
|
|
|
# ------------------------------------------------------------------------------------------------------ |
160
|
|
|
|
161
|
|
|
# 2. Check if an entity converter can be applied automatically (single or collection) |
162
|
|
|
// Check if target property has a type-hint which is en Entity itself |
163
|
|
|
$typeHint = $modelReflection->getProperty($targetField)->getType(); |
164
|
|
|
if (null !== $typeHint && class_exists((string) $typeHint)) { |
165
|
|
|
$typeAnno = $this->annotationReader->getClassAnnotation(new ReflectionClass($typeHint), ORM\Entity::class); |
166
|
|
|
if (null !== $typeAnno) { |
167
|
|
|
// use entity converter |
168
|
|
|
$converter = $this->converters->get(Converters\Entity::class); |
169
|
|
|
$a = new Converters\Entity; |
170
|
|
|
$a->value = (string) $typeHint; |
171
|
|
|
$resultValue = $converter->convert($fieldValue, $a); |
172
|
|
|
} |
173
|
|
|
} |
174
|
|
|
|
175
|
|
|
// Check if target property has a Doctrine annotation declared on it |
176
|
|
|
foreach (self::ENTITY_ANNOTATIONS as $annotationName) { |
177
|
|
|
/** @var ORM\OneToOne|ORM\OneToMany|ORM\ManyToOne|ORM\ManyToMany $a */ |
178
|
|
|
$columnAnnotation = $this->getPropertyAnnotation($modelName, $targetField, $annotationName); |
179
|
|
|
|
180
|
|
|
if (null !== $columnAnnotation) { |
181
|
|
|
if (strpos($columnAnnotation->targetEntity, '\\') === false) { |
182
|
|
|
// Fix namespace |
183
|
|
|
$columnAnnotation->targetEntity = $modelReflection->getNamespaceName()."\\$columnAnnotation->targetEntity"; |
184
|
|
|
} |
185
|
|
|
|
186
|
|
|
switch (true) { |
187
|
|
|
case $columnAnnotation instanceof ORM\OneToOne: |
188
|
|
|
case $columnAnnotation instanceof ORM\ManyToOne: |
189
|
|
|
$converter = $this->converters->get($columnAnnotation->targetEntity); |
190
|
|
|
$entity = new Entity(); |
191
|
|
|
$entity->value = ""; |
192
|
|
|
$entity->isCollection = true; |
193
|
|
|
$resultValue = $converter->convert($fieldValue, $entity); |
194
|
|
|
break; |
195
|
|
|
|
196
|
|
|
case $columnAnnotation instanceof ORM\OneToMany: |
197
|
|
|
case $columnAnnotation instanceof ORM\ManyToMany: |
198
|
|
|
$converter = $this->converters->get($columnAnnotation->targetEntity); |
199
|
|
|
$entity = new Entity(); |
200
|
|
|
$entity->value = ""; |
201
|
|
|
$entity->isCollection = true; |
202
|
|
|
$resultValue = $converter->convert($fieldValue, $columnAnnotation); |
203
|
|
|
break; |
204
|
|
|
} |
205
|
|
|
|
206
|
|
|
break; |
207
|
|
|
} |
208
|
|
|
} |
209
|
|
|
|
210
|
|
|
# ------------------------------------------------------------------------------------------------------ |
211
|
|
|
|
212
|
|
|
# 3. Use default converter (single or collection) |
213
|
|
|
if (Type::getNullableType($fieldObject->getType()) instanceof ListOfType) { |
214
|
|
|
$resultValue = $this->hydrateCollectionValue($fieldObject, $fieldValue, $modelName); |
215
|
|
|
} else { |
216
|
|
|
$resultValue = $this->hydrateValue($fieldObject, $fieldValue, $resultValue); |
|
|
|
|
217
|
|
|
} |
218
|
|
|
|
219
|
|
|
return $resultValue; |
220
|
|
|
} |
221
|
|
|
|
222
|
|
|
/** |
223
|
|
|
* Returns property annotation from cache. |
224
|
|
|
* |
225
|
|
|
* @throws ReflectionException |
226
|
|
|
*/ |
227
|
|
|
private function getPropertyAnnotation(string $className, string $propertyName, string $annotationName): ?object |
228
|
|
|
{ |
229
|
|
|
self::$annotationCache[$className][$propertyName] ??= $this->annotationReader->getPropertyAnnotations(new ReflectionProperty($className, $propertyName)); |
230
|
|
|
|
231
|
|
|
foreach (self::$annotationCache[$className][$propertyName] as $annotation) { |
232
|
|
|
if ($annotation instanceof $annotationName) { |
233
|
|
|
return $annotation; |
234
|
|
|
} |
235
|
|
|
} |
236
|
|
|
|
237
|
|
|
return null; |
238
|
|
|
} |
239
|
|
|
|
240
|
|
|
/** |
241
|
|
|
* Returns class annotation from cache. |
242
|
|
|
* |
243
|
|
|
* @throws ReflectionException |
244
|
|
|
*/ |
245
|
|
|
private function getClassAnnotation(string $className, string $annotationName): ?object |
246
|
|
|
{ |
247
|
|
|
static $cache = []; |
248
|
|
|
|
249
|
|
|
return $cache[$className][$annotationName] ??= |
250
|
|
|
$this->annotationReader->getClassAnnotation(new ReflectionClass($className), $annotationName); |
251
|
|
|
} |
252
|
|
|
|
253
|
|
|
/** |
254
|
|
|
* @param string $modelName |
255
|
|
|
* @param array $inputValues |
256
|
|
|
* @return int|mixed|string|null |
257
|
|
|
* |
258
|
|
|
* @throws ORM\MappingException|NonUniqueResultException|ReflectionException |
259
|
|
|
*/ |
260
|
|
|
private function getEntityModel(string $modelName, array $inputValues) |
261
|
|
|
{ |
262
|
|
|
$idValue = $this->resolveIdValue($modelName, $inputValues); |
263
|
|
|
|
264
|
|
|
if (null === $idValue) { |
265
|
|
|
return new $modelName(); |
266
|
|
|
} |
267
|
|
|
|
268
|
|
|
// entity |
269
|
|
|
$meta = $this->em->getClassMetadata($modelName); |
270
|
|
|
$entityIdField = $meta->getSingleIdentifierFieldName(); |
271
|
|
|
|
272
|
|
|
$builder = $this->em->createQueryBuilder() |
273
|
|
|
->select('o') |
274
|
|
|
->from($modelName, 'o') |
275
|
|
|
->where("o.$entityIdField = :identifier") |
276
|
|
|
->setParameter('identifier', $idValue); |
277
|
|
|
|
278
|
|
|
$result = $builder->getQuery()->getOneOrNullResult(); |
279
|
|
|
|
280
|
|
|
if (null === $result) { |
281
|
|
|
throw new Exception("Couldn't find entity"); |
282
|
|
|
} |
283
|
|
|
|
284
|
|
|
return $result; |
285
|
|
|
} |
286
|
|
|
|
287
|
|
|
/** |
288
|
|
|
* @return array|mixed|null |
289
|
|
|
* @throws ReflectionException |
290
|
|
|
*/ |
291
|
|
|
private function resolveIdValue(string $modelName, array $inputValues) |
292
|
|
|
{ |
293
|
|
|
$reflectionClass = new ReflectionClass($modelName); |
294
|
|
|
$modelAnnotation = $this->annotationReader->getClassAnnotation($reflectionClass, Model::class); |
295
|
|
|
|
296
|
|
|
$identifier = $modelAnnotation->identifier ?? 'id'; |
297
|
|
|
$path = explode('.', $identifier); |
298
|
|
|
|
299
|
|
|
// If a path is provided, search the value from top argument down |
300
|
|
|
if (count($path) > 1) { |
301
|
|
|
$temp = &$this->args; |
302
|
|
|
foreach($path as $key) { |
303
|
|
|
$temp = &$temp[$key]; |
304
|
|
|
} |
305
|
|
|
return $temp; |
306
|
|
|
} |
307
|
|
|
|
308
|
|
|
return $inputValues[$identifier] ?? $this->args[$identifier] ?? null; |
309
|
|
|
} |
310
|
|
|
|
311
|
|
|
/** |
312
|
|
|
* @param $fieldObject |
313
|
|
|
* @param $fieldValue |
314
|
|
|
* @return mixed |
315
|
|
|
* @throws ReflectionException |
316
|
|
|
*/ |
317
|
|
|
private function hydrateValue($fieldObject, $fieldValue, object $model = null) |
318
|
|
|
{ |
319
|
|
|
$field = Type::getNamedType($fieldObject->getType()); |
320
|
|
|
|
321
|
|
|
if ($field instanceof InputObjectType) { |
322
|
|
|
$fieldValue = $this->hydrateInputType($field, $fieldValue, $model); |
323
|
|
|
} |
324
|
|
|
|
325
|
|
|
return $fieldValue; |
326
|
|
|
} |
327
|
|
|
|
328
|
|
|
/** |
329
|
|
|
* @param $fieldObject |
330
|
|
|
* @param $fieldValue |
331
|
|
|
* @param string $modelName |
332
|
|
|
* @return array |
333
|
|
|
* @throws ORM\MappingException |
334
|
|
|
* @throws ReflectionException |
335
|
|
|
*/ |
336
|
|
|
private function hydrateCollectionValue($fieldObject, $fieldValue, string $modelName) |
337
|
|
|
{ |
338
|
|
|
$isBuiltInTypes = Type::isBuiltInType(Type::getNamedType($fieldObject->getType())); |
|
|
|
|
339
|
|
|
|
340
|
|
|
if (true === $isBuiltInTypes) { |
341
|
|
|
$meta = $this->em->getClassMetadata($modelName); |
342
|
|
|
$entityIdField = $meta->getSingleIdentifierFieldName(); |
343
|
|
|
|
344
|
|
|
$query = $this->em->createQuery(<<<DQL |
345
|
|
|
SELECT o FROM $modelName o |
346
|
|
|
WHERE o.$entityIdField IN (:ids) |
347
|
|
|
INDEX BY o.$entityIdField |
348
|
|
|
DQL); |
349
|
|
|
|
350
|
|
|
$query->setParameter('ids', $fieldValue); |
351
|
|
|
$entities = $query->getResult(); |
352
|
|
|
|
353
|
|
|
if (count($entities) !== count($fieldValue)) { |
354
|
|
|
throw new Exception("Couldn't find all entities."); |
355
|
|
|
} |
356
|
|
|
} |
357
|
|
|
|
358
|
|
|
$result = []; |
359
|
|
|
foreach ($fieldValue as $value) { |
360
|
|
|
$result[] = $this->hydrateValue($fieldObject, $value, $entities[$value] ?? null); |
361
|
|
|
} |
362
|
|
|
|
363
|
|
|
return $result; |
364
|
|
|
} |
365
|
|
|
|
366
|
|
|
/** |
367
|
|
|
* @param mixed $value |
368
|
|
|
* @return mixed |
369
|
|
|
* @throws ReflectionException |
370
|
|
|
*/ |
371
|
|
|
private function convertValue($value, object $model, string $targetName) |
|
|
|
|
372
|
|
|
{ |
373
|
|
|
$reflectionClass = new ReflectionClass($model); |
374
|
|
|
$property = $reflectionClass->getProperty($targetName); |
375
|
|
|
|
376
|
|
|
/** @var ConverterAnnotationInterface $annotation */ |
377
|
|
|
$annotation = $this->annotationReader->getPropertyAnnotation($property, ConverterAnnotationInterface::class); |
378
|
|
|
|
379
|
|
|
if (null !== $annotation) { |
380
|
|
|
$converter = $this->converters->get($annotation::getConverterClass()); |
381
|
|
|
return $converter->convert($value, $annotation); |
382
|
|
|
} |
383
|
|
|
|
384
|
|
|
return $value; |
385
|
|
|
} |
386
|
|
|
|
387
|
|
|
private function readAnnotationMapping(ReflectionClass $reflectionClass): array |
388
|
|
|
{ |
389
|
|
|
$reader = AnnotationParser::getAnnotationReader(); |
|
|
|
|
390
|
|
|
$properties = $reflectionClass->getProperties(); |
391
|
|
|
|
392
|
|
|
$mapping = []; |
393
|
|
|
foreach ($properties as $property) { |
394
|
|
|
$annotation = $reader->getPropertyAnnotation($property, Field::class); |
395
|
|
|
|
396
|
|
|
if (isset($annotation->name)) { |
397
|
|
|
$mapping[$annotation->name] = $property->name; |
398
|
|
|
} |
399
|
|
|
} |
400
|
|
|
|
401
|
|
|
return $mapping; |
402
|
|
|
} |
403
|
|
|
} |
404
|
|
|
|
This function has been deprecated. The supplier of the function has supplied an explanatory message.
The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.