1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Oro\Component\EntitySerializer; |
4
|
|
|
|
5
|
|
|
use Doctrine\DBAL\Types\Type; |
6
|
|
|
use Doctrine\ORM\Query; |
7
|
|
|
use Doctrine\ORM\QueryBuilder; |
8
|
|
|
|
9
|
|
|
/** |
10
|
|
|
* @todo: This is draft implementation of the entity serializer. |
11
|
|
|
* It is expected that the full implementation will be done when new API component is implemented. |
12
|
|
|
* What need to do: |
13
|
|
|
* * by default the value of identifier field should be used |
14
|
|
|
* for related entities (now it should be configured manually in serialization rules) |
15
|
|
|
* * add support for extended fields |
16
|
|
|
* |
17
|
|
|
* Example of serialization rules used in the $config parameter of |
18
|
|
|
* {@see serialize}, {@see serializeEntities} and {@see prepareQuery} methods: |
19
|
|
|
* |
20
|
|
|
* [ |
21
|
|
|
* // exclude the 'email' field |
22
|
|
|
* 'fields' => [ |
23
|
|
|
* // exclude the 'email' field |
24
|
|
|
* 'email' => ['exclude' => true] |
25
|
|
|
* // serialize the 'status' many-to-one relation using the value of the 'name' field |
26
|
|
|
* 'status' => ['fields' => 'name'], |
27
|
|
|
* // order the 'phones' many-to-many relation by the 'primary' field and |
28
|
|
|
* // serialize each phone as a pair of 'phone' and 'primary' field |
29
|
|
|
* 'phones' => [ |
30
|
|
|
* 'exclusion_policy' => 'all', |
31
|
|
|
* 'fields' => [ |
32
|
|
|
* 'phone' => null, |
33
|
|
|
* 'isPrimary' => [ |
34
|
|
|
* // as example we can convert boolean to Yes/No string |
35
|
|
|
* // the data transformer must implement either |
36
|
|
|
* // Symfony\Component\Form\DataTransformerInterface |
37
|
|
|
* // or Oro\Component\EntitySerializer\DataTransformerInterface |
38
|
|
|
* // Also several data transformers can be specified, for example |
39
|
|
|
* // 'data_transformer' => ['first_transformer_service_id', 'second_transformer_service_id'], |
40
|
|
|
* 'data_transformer' => 'boolean_to_string_transformer_service_id', |
41
|
|
|
* // the "primary" field should be named as "isPrimary" in the result |
42
|
|
|
* 'property_path' => 'primary' |
43
|
|
|
* ] |
44
|
|
|
* ], |
45
|
|
|
* 'order_by' => [ |
46
|
|
|
* 'primary' => 'DESC' |
47
|
|
|
* ] |
48
|
|
|
* ], |
49
|
|
|
* 'addresses' => [ |
50
|
|
|
* 'fields' => [ |
51
|
|
|
* 'owner' => ['exclude' => true], |
52
|
|
|
* 'country' => ['fields' => 'name'], |
53
|
|
|
* 'types' => [ |
54
|
|
|
* 'fields' => 'name', |
55
|
|
|
* 'order_by' => [ |
56
|
|
|
* 'name' => 'ASC' |
57
|
|
|
* ] |
58
|
|
|
* ] |
59
|
|
|
* ] |
60
|
|
|
* ] |
61
|
|
|
* ] |
62
|
|
|
* ] |
63
|
|
|
* |
64
|
|
|
* Example of the serialization result by this config (it is supposed that the serializing entity has |
65
|
|
|
* the following fields: |
66
|
|
|
* id |
67
|
|
|
* name |
68
|
|
|
* email |
69
|
|
|
* status -> many-to-one |
70
|
|
|
* name |
71
|
|
|
* label |
72
|
|
|
* phones -> many-to-many |
73
|
|
|
* id |
74
|
|
|
* phone |
75
|
|
|
* primary |
76
|
|
|
* addresses -> many-to-many |
77
|
|
|
* id |
78
|
|
|
* owner -> many-to-one |
79
|
|
|
* country -> many-to-one |
80
|
|
|
* code, |
81
|
|
|
* name |
82
|
|
|
* types -> many-to-many |
83
|
|
|
* name |
84
|
|
|
* label |
85
|
|
|
* [ |
86
|
|
|
* 'id' => 123, |
87
|
|
|
* 'name' => 'John Smith', |
88
|
|
|
* 'status' => 'active', |
89
|
|
|
* 'phones' => [ |
90
|
|
|
* ['phone' => '123-123', 'primary' => true], |
91
|
|
|
* ['phone' => '456-456', 'primary' => false] |
92
|
|
|
* ], |
93
|
|
|
* 'addresses' => [ |
94
|
|
|
* ['country' => 'USA', 'types' => ['billing', 'shipping']] |
95
|
|
|
* ] |
96
|
|
|
* ] |
97
|
|
|
* |
98
|
|
|
* Special attributes: |
99
|
|
|
* * 'disable_partial_load' - Disables using of Doctrine partial object. |
100
|
|
|
* It can be helpful for entities with SINGLE_TABLE inheritance mapping |
101
|
|
|
* * 'hints' - The list of Doctrine query hints. Each item can be a string or name/value pair. |
102
|
|
|
* Example: |
103
|
|
|
* 'hints' => [ |
104
|
|
|
* 'HINT_TRANSLATABLE', |
105
|
|
|
* ['name' => 'HINT_CUSTOM_OUTPUT_WALKER', 'value' => 'Acme\AST_Walker_Class'] |
106
|
|
|
* ] |
107
|
|
|
* |
108
|
|
|
* Metadata properties: |
109
|
|
|
* * '__discriminator__' - The discriminator value an entity. |
110
|
|
|
* * '__class__' - FQCN of an entity. |
111
|
|
|
* An example of a metadata property usage: |
112
|
|
|
* 'fields' => [ |
113
|
|
|
* 'type' => ['property_path' => '__discriminator__'] |
114
|
|
|
* ] |
115
|
|
|
* |
116
|
|
|
* @SuppressWarnings(PHPMD.ExcessiveClassComplexity) |
117
|
|
|
*/ |
118
|
|
|
class EntitySerializer |
119
|
|
|
{ |
120
|
|
|
/** @var DoctrineHelper */ |
121
|
|
|
protected $doctrineHelper; |
122
|
|
|
|
123
|
|
|
/** @var DataAccessorInterface */ |
124
|
|
|
protected $dataAccessor; |
125
|
|
|
|
126
|
|
|
/** @var DataTransformerInterface */ |
127
|
|
|
protected $dataTransformer; |
128
|
|
|
|
129
|
|
|
/** @var QueryFactory */ |
130
|
|
|
protected $queryFactory; |
131
|
|
|
|
132
|
|
|
/** @var FieldAccessor */ |
133
|
|
|
protected $fieldAccessor; |
134
|
|
|
|
135
|
|
|
/** @var ConfigNormalizer */ |
136
|
|
|
protected $configNormalizer; |
137
|
|
|
|
138
|
|
|
/** @var ConfigConverter */ |
139
|
|
|
protected $configConverter; |
140
|
|
|
|
141
|
|
|
/** @var DataNormalizer */ |
142
|
|
|
protected $dataNormalizer; |
143
|
|
|
|
144
|
|
|
/** |
145
|
|
|
* @param DoctrineHelper $doctrineHelper |
146
|
|
|
* @param DataAccessorInterface $dataAccessor |
147
|
|
|
* @param DataTransformerInterface $dataTransformer |
148
|
|
|
* @param QueryFactory $queryFactory |
149
|
|
|
* @param FieldAccessor $fieldAccessor |
150
|
|
|
* @param ConfigNormalizer $configNormalizer |
151
|
|
|
* @param DataNormalizer $dataNormalizer |
152
|
|
|
*/ |
153
|
|
|
public function __construct( |
154
|
|
|
DoctrineHelper $doctrineHelper, |
155
|
|
|
DataAccessorInterface $dataAccessor, |
156
|
|
|
DataTransformerInterface $dataTransformer, |
157
|
|
|
QueryFactory $queryFactory, |
158
|
|
|
FieldAccessor $fieldAccessor, |
159
|
|
|
ConfigNormalizer $configNormalizer, |
160
|
|
|
DataNormalizer $dataNormalizer |
161
|
|
|
) { |
162
|
|
|
$this->doctrineHelper = $doctrineHelper; |
163
|
|
|
$this->dataAccessor = $dataAccessor; |
164
|
|
|
$this->dataTransformer = $dataTransformer; |
165
|
|
|
$this->queryFactory = $queryFactory; |
166
|
|
|
$this->fieldAccessor = $fieldAccessor; |
167
|
|
|
$this->configNormalizer = $configNormalizer; |
168
|
|
|
$this->dataNormalizer = $dataNormalizer; |
169
|
|
|
|
170
|
|
|
$this->configConverter = new ConfigConverter(); |
171
|
|
|
} |
172
|
|
|
|
173
|
|
|
/** |
174
|
|
|
* @param QueryBuilder $qb A query builder is used to get data |
175
|
|
|
* @param EntityConfig|array $config Serialization rules |
176
|
|
|
* |
177
|
|
|
* @return array |
178
|
|
|
*/ |
179
|
|
|
public function serialize(QueryBuilder $qb, $config) |
180
|
|
|
{ |
181
|
|
|
$entityConfig = $this->normalizeConfig($config); |
182
|
|
|
|
183
|
|
|
$this->updateQuery($qb, $entityConfig); |
184
|
|
|
$data = $this->queryFactory->getQuery($qb, $entityConfig)->getResult(); |
185
|
|
|
$data = $this->serializeItems((array)$data, $this->doctrineHelper->getRootEntityClass($qb), $entityConfig); |
186
|
|
|
|
187
|
|
|
return $this->dataNormalizer->normalizeData($data, $entityConfig); |
188
|
|
|
} |
189
|
|
|
|
190
|
|
|
/** |
191
|
|
|
* @param object[] $entities The list of entities to be serialized |
192
|
|
|
* @param string $entityClass The entity class name |
193
|
|
|
* @param EntityConfig|array $config Serialization rules |
194
|
|
|
* |
195
|
|
|
* @return array |
196
|
|
|
*/ |
197
|
|
|
public function serializeEntities(array $entities, $entityClass, $config) |
198
|
|
|
{ |
199
|
|
|
$entityConfig = $this->normalizeConfig($config); |
200
|
|
|
|
201
|
|
|
$data = $this->serializeItems($entities, $entityClass, $entityConfig); |
202
|
|
|
|
203
|
|
|
return $this->dataNormalizer->normalizeData($data, $entityConfig); |
204
|
|
|
} |
205
|
|
|
|
206
|
|
|
/** |
207
|
|
|
* @param QueryBuilder $qb |
208
|
|
|
* @param EntityConfig|array $config |
209
|
|
|
*/ |
210
|
|
|
public function prepareQuery(QueryBuilder $qb, $config) |
211
|
|
|
{ |
212
|
|
|
$this->updateQuery($qb, $this->normalizeConfig($config)); |
213
|
|
|
} |
214
|
|
|
|
215
|
|
|
/** |
216
|
|
|
* @param EntityConfig|array $config |
217
|
|
|
* |
218
|
|
|
* @return EntityConfig |
219
|
|
|
*/ |
220
|
|
|
protected function normalizeConfig($config) |
221
|
|
|
{ |
222
|
|
|
$normalizedConfig = $this->configNormalizer->normalizeConfig( |
223
|
|
|
$config instanceof EntityConfig ? $config->toArray() : $config |
224
|
|
|
); |
225
|
|
|
|
226
|
|
|
return $this->configConverter->convertConfig($normalizedConfig); |
227
|
|
|
} |
228
|
|
|
|
229
|
|
|
/** |
230
|
|
|
* @param object[] $entities The list of entities to be serialized |
231
|
|
|
* @param string $entityClass The entity class name |
232
|
|
|
* @param EntityConfig $config Serialization rules |
233
|
|
|
* @param bool $useIdAsKey Defines whether the entity id should be used as a key of the result array |
234
|
|
|
* |
235
|
|
|
* @return array |
236
|
|
|
*/ |
237
|
|
|
protected function serializeItems(array $entities, $entityClass, EntityConfig $config, $useIdAsKey = false) |
238
|
|
|
{ |
239
|
|
|
if (empty($entities)) { |
240
|
|
|
return []; |
241
|
|
|
} |
242
|
|
|
|
243
|
|
|
$result = []; |
244
|
|
|
|
245
|
|
|
$idFieldName = $this->doctrineHelper->getEntityIdFieldName($entityClass); |
246
|
|
|
if ($useIdAsKey) { |
247
|
|
|
foreach ($entities as $entity) { |
248
|
|
|
$id = $this->dataAccessor->getValue($entity, $idFieldName); |
249
|
|
|
$result[$id] = $this->serializeItem($entity, $entityClass, $config); |
250
|
|
|
} |
251
|
|
|
} else { |
252
|
|
|
foreach ($entities as $entity) { |
253
|
|
|
$result[] = $this->serializeItem($entity, $entityClass, $config); |
254
|
|
|
} |
255
|
|
|
} |
256
|
|
|
|
257
|
|
|
$this->loadRelatedData($result, $entityClass, $this->getEntityIds($entities, $idFieldName), $config); |
258
|
|
|
|
259
|
|
|
$postSerializeHandler = $config->getPostSerializeHandler(); |
260
|
|
|
if (null !== $postSerializeHandler) { |
261
|
|
|
foreach ($result as &$resultItem) { |
262
|
|
|
$resultItem = $this->postSerialize($resultItem, $postSerializeHandler); |
263
|
|
|
} |
264
|
|
|
} |
265
|
|
|
|
266
|
|
|
return $result; |
267
|
|
|
} |
268
|
|
|
|
269
|
|
|
/** |
270
|
|
|
* @param mixed $entity |
271
|
|
|
* @param string $entityClass |
272
|
|
|
* @param EntityConfig $config |
273
|
|
|
* |
274
|
|
|
* @return array |
275
|
|
|
*/ |
276
|
|
|
protected function serializeItem($entity, $entityClass, EntityConfig $config) |
277
|
|
|
{ |
278
|
|
|
if (!$entity) { |
279
|
|
|
return []; |
280
|
|
|
} |
281
|
|
|
|
282
|
|
|
$result = []; |
283
|
|
|
$entityMetadata = $this->doctrineHelper->getEntityMetadata($entityClass); |
284
|
|
|
$resultFields = $this->fieldAccessor->getFieldsToSerialize($entityClass, $config); |
285
|
|
|
foreach ($resultFields as $field) { |
286
|
|
|
$fieldConfig = $config->getField($field); |
287
|
|
|
|
288
|
|
|
$value = null; |
289
|
|
|
if ($this->dataAccessor->tryGetValue($entity, $field, $value)) { |
290
|
|
|
if ($entityMetadata->isAssociation($field)) { |
291
|
|
|
if ($value !== null) { |
292
|
|
|
$targetConfig = $this->getTargetEntity($config, $field); |
293
|
|
|
if (null !== $targetConfig && !$targetConfig->isEmpty()) { |
294
|
|
|
$targetEntityClass = $entityMetadata->getAssociationTargetClass($field); |
295
|
|
|
$targetEntityId = $this->dataAccessor->getValue( |
296
|
|
|
$value, |
297
|
|
|
$this->doctrineHelper->getEntityIdFieldName($targetEntityClass) |
298
|
|
|
); |
299
|
|
|
|
300
|
|
|
$value = $this->serializeItem($value, $targetEntityClass, $targetConfig); |
301
|
|
|
$items = [$value]; |
302
|
|
|
$this->loadRelatedData($items, $targetEntityClass, [$targetEntityId], $targetConfig); |
303
|
|
|
$value = reset($items); |
304
|
|
|
|
305
|
|
|
$postSerializeHandler = $targetConfig->getPostSerializeHandler(); |
306
|
|
|
if (null !== $postSerializeHandler) { |
307
|
|
|
$value = $this->postSerialize($value, $postSerializeHandler); |
308
|
|
|
} |
309
|
|
|
} else { |
310
|
|
|
$value = $this->transformValue($entityClass, $field, $value, $fieldConfig); |
311
|
|
|
} |
312
|
|
|
} |
313
|
|
|
} else { |
314
|
|
|
$value = $this->transformValue($entityClass, $field, $value, $fieldConfig); |
315
|
|
|
} |
316
|
|
|
$result[$field] = $value; |
317
|
|
|
} elseif (null !== $fieldConfig) { |
318
|
|
|
$propertyPath = $fieldConfig->getPropertyPath() ?: $field; |
319
|
|
|
if (ConfigUtil::isMetadataProperty($propertyPath)) { |
320
|
|
|
$result[$field] = $this->fieldAccessor->getMetadataProperty( |
321
|
|
|
$entity, |
322
|
|
|
$propertyPath, |
323
|
|
|
$entityMetadata |
324
|
|
|
); |
325
|
|
|
} |
326
|
|
|
} |
327
|
|
|
} |
328
|
|
|
|
329
|
|
|
return $result; |
330
|
|
|
} |
331
|
|
|
|
332
|
|
|
/** |
333
|
|
|
* @param QueryBuilder $qb |
334
|
|
|
* @param EntityConfig $config |
335
|
|
|
*/ |
336
|
|
|
protected function updateQuery(QueryBuilder $qb, EntityConfig $config) |
337
|
|
|
{ |
338
|
|
|
$rootAlias = $this->doctrineHelper->getRootAlias($qb); |
339
|
|
|
$entityClass = $this->doctrineHelper->getRootEntityClass($qb); |
340
|
|
|
$entityMetadata = $this->doctrineHelper->getEntityMetadata($entityClass); |
341
|
|
|
|
342
|
|
|
$qb->resetDQLPart('select'); |
343
|
|
|
$this->updateSelectQueryPart($qb, $rootAlias, $entityClass, $config); |
344
|
|
|
|
345
|
|
|
$aliasCounter = 0; |
346
|
|
|
$fields = $this->fieldAccessor->getFields($entityClass, $config); |
347
|
|
|
foreach ($fields as $field) { |
348
|
|
|
if (!$entityMetadata->isAssociation($field) || $entityMetadata->isCollectionValuedAssociation($field)) { |
349
|
|
|
continue; |
350
|
|
|
} |
351
|
|
|
|
352
|
|
|
$join = sprintf('%s.%s', $rootAlias, $field); |
353
|
|
|
$alias = $this->getExistingJoinAlias($qb, $rootAlias, $join); |
354
|
|
|
if (!$alias) { |
|
|
|
|
355
|
|
|
$alias = 'a' . ++$aliasCounter; |
356
|
|
|
$qb->leftJoin($join, $alias); |
357
|
|
|
} |
358
|
|
|
$this->updateSelectQueryPart( |
359
|
|
|
$qb, |
360
|
|
|
$alias, |
361
|
|
|
$entityMetadata->getAssociationTargetClass($field), |
362
|
|
|
$this->getTargetEntity($config, $field), |
363
|
|
|
true |
364
|
|
|
); |
365
|
|
|
} |
366
|
|
|
} |
367
|
|
|
|
368
|
|
|
/** |
369
|
|
|
* @param QueryBuilder $qb |
370
|
|
|
* @param string $rootAlias |
371
|
|
|
* @param string $join |
372
|
|
|
* |
373
|
|
|
* @return string|null |
374
|
|
|
*/ |
375
|
|
|
protected function getExistingJoinAlias(QueryBuilder $qb, $rootAlias, $join) |
376
|
|
|
{ |
377
|
|
|
$joins = $qb->getDQLPart('join'); |
378
|
|
|
if (!empty($joins[$rootAlias])) { |
379
|
|
|
/** @var Query\Expr\Join $item */ |
380
|
|
|
foreach ($joins[$rootAlias] as $item) { |
381
|
|
|
if ($item->getJoin() === $join) { |
382
|
|
|
return $item->getAlias(); |
383
|
|
|
} |
384
|
|
|
} |
385
|
|
|
} |
386
|
|
|
|
387
|
|
|
return null; |
388
|
|
|
} |
389
|
|
|
|
390
|
|
|
/** |
391
|
|
|
* @param QueryBuilder $qb |
392
|
|
|
* @param string $alias |
393
|
|
|
* @param string $entityClass |
394
|
|
|
* @param EntityConfig $config |
395
|
|
|
* @param bool $withAssociations |
396
|
|
|
*/ |
397
|
|
|
protected function updateSelectQueryPart( |
398
|
|
|
QueryBuilder $qb, |
399
|
|
|
$alias, |
400
|
|
|
$entityClass, |
401
|
|
|
EntityConfig $config, |
402
|
|
|
$withAssociations = false |
403
|
|
|
) { |
404
|
|
|
if ($config->isPartialLoadEnabled()) { |
405
|
|
|
$fields = $this->fieldAccessor->getFieldsToSelect($entityClass, $config, $withAssociations); |
406
|
|
|
$qb->addSelect(sprintf('partial %s.{%s}', $alias, implode(',', $fields))); |
407
|
|
|
} else { |
408
|
|
|
$qb->addSelect($alias); |
409
|
|
|
} |
410
|
|
|
} |
411
|
|
|
|
412
|
|
|
/** |
413
|
|
|
* @param array $result |
414
|
|
|
* @param string $entityClass |
415
|
|
|
* @param array $entityIds |
416
|
|
|
* @param EntityConfig $config |
417
|
|
|
*/ |
418
|
|
|
protected function loadRelatedData(array &$result, $entityClass, $entityIds, EntityConfig $config) |
419
|
|
|
{ |
420
|
|
|
$relatedData = []; |
421
|
|
|
$entityMetadata = $this->doctrineHelper->getEntityMetadata($entityClass); |
422
|
|
|
$fields = $this->fieldAccessor->getFields($entityClass, $config); |
423
|
|
|
foreach ($fields as $field) { |
424
|
|
|
if (!$entityMetadata->isCollectionValuedAssociation($field)) { |
425
|
|
|
continue; |
426
|
|
|
} |
427
|
|
|
|
428
|
|
|
$mapping = $entityMetadata->getAssociationMapping($field); |
429
|
|
|
$targetConfig = $this->getTargetEntity($config, $field); |
430
|
|
|
|
431
|
|
|
$relatedData[$field] = $this->isSingleStepLoading($mapping['targetEntity'], $targetConfig) |
432
|
|
|
? $this->loadRelatedItemsForSimpleEntity($entityIds, $mapping, $targetConfig) |
433
|
|
|
: $this->loadRelatedItems($entityIds, $mapping, $targetConfig); |
434
|
|
|
} |
435
|
|
|
if (!empty($relatedData)) { |
436
|
|
|
$this->applyRelatedData($result, $entityClass, $relatedData); |
437
|
|
|
} |
438
|
|
|
} |
439
|
|
|
|
440
|
|
|
/** |
441
|
|
|
* @param array $result |
442
|
|
|
* @param string $entityClass |
443
|
|
|
* @param array $relatedData [field => [entityId => [field => value, ...], ...], ...] |
444
|
|
|
* |
445
|
|
|
* @throws \RuntimeException |
446
|
|
|
*/ |
447
|
|
|
protected function applyRelatedData(array &$result, $entityClass, $relatedData) |
448
|
|
|
{ |
449
|
|
|
$entityIdFieldName = $this->doctrineHelper->getEntityIdFieldName($entityClass); |
450
|
|
|
foreach ($result as &$resultItem) { |
451
|
|
|
if (!array_key_exists($entityIdFieldName, $resultItem)) { |
452
|
|
|
throw new \RuntimeException( |
453
|
|
|
sprintf('The result item does not contain the entity identifier. Entity: %s.', $entityClass) |
454
|
|
|
); |
455
|
|
|
} |
456
|
|
|
$entityId = $resultItem[$entityIdFieldName]; |
457
|
|
|
foreach ($relatedData as $field => $relatedItems) { |
458
|
|
|
$resultItem[$field] = []; |
459
|
|
|
if (!empty($relatedItems[$entityId])) { |
460
|
|
|
foreach ($relatedItems[$entityId] as $relatedItem) { |
461
|
|
|
$resultItem[$field][] = $relatedItem; |
462
|
|
|
} |
463
|
|
|
} |
464
|
|
|
} |
465
|
|
|
} |
466
|
|
|
} |
467
|
|
|
|
468
|
|
|
/** |
469
|
|
|
* @param string $entityClass |
470
|
|
|
* @param EntityConfig $config |
471
|
|
|
* |
472
|
|
|
* @return bool |
473
|
|
|
*/ |
474
|
|
|
protected function isSingleStepLoading($entityClass, EntityConfig $config) |
475
|
|
|
{ |
476
|
|
|
return |
477
|
|
|
null === $config->getMaxResults() |
478
|
|
|
&& !$this->hasAssociations($entityClass, $config); |
479
|
|
|
} |
480
|
|
|
|
481
|
|
|
/** |
482
|
|
|
* @param array $entityIds |
483
|
|
|
* @param array $mapping |
484
|
|
|
* @param EntityConfig $config |
485
|
|
|
* |
486
|
|
|
* @return array [entityId => [field => value, ...], ...] |
487
|
|
|
*/ |
488
|
|
|
protected function loadRelatedItems($entityIds, $mapping, EntityConfig $config) |
489
|
|
|
{ |
490
|
|
|
$result = []; |
491
|
|
|
|
492
|
|
|
$entityClass = $mapping['targetEntity']; |
493
|
|
|
$bindings = $this->getRelatedItemsBindings($entityIds, $mapping, $config); |
494
|
|
|
|
495
|
|
|
$items = []; |
496
|
|
|
$resultFieldName = $this->getIdFieldNameIfIdOnlyRequested($config, $entityClass); |
497
|
|
|
if (null !== $resultFieldName) { |
498
|
|
|
$postSerializeHandler = $config->getPostSerializeHandler(); |
499
|
|
|
$relatedItemIds = $this->getRelatedItemsIds($bindings); |
500
|
|
|
foreach ($relatedItemIds as $relatedItemId) { |
501
|
|
|
$relatedItem = [$resultFieldName => $relatedItemId]; |
502
|
|
|
if (null !== $postSerializeHandler) { |
503
|
|
|
$relatedItem = $this->postSerialize($relatedItem, $postSerializeHandler); |
504
|
|
|
} |
505
|
|
|
$items[$relatedItemId] = $relatedItem; |
506
|
|
|
} |
507
|
|
|
} else { |
508
|
|
|
$qb = $this->queryFactory->getRelatedItemsQueryBuilder( |
509
|
|
|
$entityClass, |
510
|
|
|
$this->getRelatedItemsIds($bindings) |
511
|
|
|
); |
512
|
|
|
$this->updateQuery($qb, $config); |
513
|
|
|
$data = $this->queryFactory->getQuery($qb, $config)->getResult(); |
514
|
|
|
if (!empty($data)) { |
515
|
|
|
$items = $this->serializeItems((array)$data, $entityClass, $config, true); |
516
|
|
|
} |
517
|
|
|
} |
518
|
|
|
if (!empty($items)) { |
519
|
|
|
foreach ($bindings as $entityId => $relatedEntityIds) { |
520
|
|
|
foreach ($relatedEntityIds as $relatedEntityId) { |
521
|
|
|
if (isset($items[$relatedEntityId])) { |
522
|
|
|
$result[$entityId][] = $items[$relatedEntityId]; |
523
|
|
|
} |
524
|
|
|
} |
525
|
|
|
} |
526
|
|
|
} |
527
|
|
|
|
528
|
|
|
return $result; |
529
|
|
|
} |
530
|
|
|
|
531
|
|
|
/** |
532
|
|
|
* @param EntityConfig $config |
533
|
|
|
* @param string $entityClass |
534
|
|
|
* |
535
|
|
|
* @return string|null The name of result field if only identity field should be returned; |
536
|
|
|
* otherwise, NULL |
537
|
|
|
*/ |
538
|
|
|
protected function getIdFieldNameIfIdOnlyRequested(EntityConfig $config, $entityClass) |
539
|
|
|
{ |
540
|
|
|
if (!$config->isExcludeAll()) { |
541
|
|
|
return null; |
542
|
|
|
} |
543
|
|
|
$fields = $config->getFields(); |
544
|
|
|
if (count($fields) !== 1) { |
545
|
|
|
return null; |
546
|
|
|
} |
547
|
|
|
reset($fields); |
548
|
|
|
/** @var FieldConfig $field */ |
549
|
|
|
list($fieldName, $field) = each($fields); |
550
|
|
|
$targetConfig = $field->getTargetEntity(); |
551
|
|
|
if (null !== $targetConfig && !$targetConfig->isEmpty()) { |
552
|
|
|
return null; |
553
|
|
|
} |
554
|
|
|
|
555
|
|
|
$propertyPath = $field->getPropertyPath() ?: $fieldName; |
556
|
|
|
if ($this->doctrineHelper->getEntityIdFieldName($entityClass) !== $propertyPath) { |
557
|
|
|
return null; |
558
|
|
|
} |
559
|
|
|
|
560
|
|
|
return $fieldName; |
561
|
|
|
} |
562
|
|
|
|
563
|
|
|
/** |
564
|
|
|
* @param array $entityIds |
565
|
|
|
* @param array $mapping |
566
|
|
|
* @param EntityConfig $config |
567
|
|
|
* |
568
|
|
|
* @return array [entityId => [relatedEntityId, ...], ...] |
569
|
|
|
*/ |
570
|
|
|
protected function getRelatedItemsBindings($entityIds, $mapping, EntityConfig $config) |
571
|
|
|
{ |
572
|
|
|
$rows = $this->queryFactory->getRelatedItemsIds($mapping, $entityIds, $config); |
573
|
|
|
|
574
|
|
|
$result = []; |
575
|
|
|
if (!empty($rows)) { |
576
|
|
|
$relatedEntityIdType = $this->getEntityIdType($mapping['targetEntity']); |
577
|
|
|
foreach ($rows as $row) { |
578
|
|
|
$result[$row['entityId']][] = $this->getTypedEntityId($row['relatedEntityId'], $relatedEntityIdType); |
|
|
|
|
579
|
|
|
} |
580
|
|
|
} |
581
|
|
|
|
582
|
|
|
return $result; |
583
|
|
|
} |
584
|
|
|
|
585
|
|
|
/** |
586
|
|
|
* @param array $bindings [entityId => relatedEntityId, ...] |
587
|
|
|
* |
588
|
|
|
* @return array of unique ids of all related entities from $bindings array |
589
|
|
|
*/ |
590
|
|
|
protected function getRelatedItemsIds($bindings) |
591
|
|
|
{ |
592
|
|
|
$result = []; |
593
|
|
|
foreach ($bindings as $ids) { |
594
|
|
|
foreach ($ids as $id) { |
595
|
|
|
if (!isset($result[$id])) { |
596
|
|
|
$result[$id] = $id; |
597
|
|
|
} |
598
|
|
|
} |
599
|
|
|
} |
600
|
|
|
|
601
|
|
|
return array_values($result); |
602
|
|
|
} |
603
|
|
|
|
604
|
|
|
/** |
605
|
|
|
* @param array $entityIds |
606
|
|
|
* @param array $mapping |
607
|
|
|
* @param EntityConfig $config |
608
|
|
|
* |
609
|
|
|
* @return array [entityId => [field => value, ...], ...] |
610
|
|
|
*/ |
611
|
|
|
protected function loadRelatedItemsForSimpleEntity($entityIds, $mapping, EntityConfig $config) |
612
|
|
|
{ |
613
|
|
|
$qb = $this->queryFactory->getToManyAssociationQueryBuilder($mapping, $entityIds); |
614
|
|
|
|
615
|
|
|
$orderBy = $config->getOrderBy(); |
616
|
|
|
foreach ($orderBy as $field => $direction) { |
617
|
|
|
$qb->addOrderBy(sprintf('r.%s', $field), $direction); |
618
|
|
|
} |
619
|
|
|
|
620
|
|
|
$fields = $this->fieldAccessor->getFieldsToSerialize($mapping['targetEntity'], $config); |
621
|
|
|
foreach ($fields as $field) { |
622
|
|
|
$qb->addSelect(sprintf('r.%s', $field)); |
623
|
|
|
} |
624
|
|
|
|
625
|
|
|
$items = $this->queryFactory->getQuery($qb, $config)->getArrayResult(); |
626
|
|
|
|
627
|
|
|
$result = []; |
628
|
|
|
$entityClass = $mapping['targetEntity']; |
629
|
|
|
|
630
|
|
|
$postSerializeHandler = $config->getPostSerializeHandler(); |
631
|
|
|
if (null !== $postSerializeHandler) { |
632
|
|
|
foreach ($items as $item) { |
633
|
|
|
$result[$item['entityId']][] = $this->postSerialize( |
634
|
|
|
$this->serializeItem($item, $entityClass, $config), |
635
|
|
|
$postSerializeHandler |
636
|
|
|
); |
637
|
|
|
} |
638
|
|
|
} else { |
639
|
|
|
foreach ($items as $item) { |
640
|
|
|
$result[$item['entityId']][] = $this->serializeItem($item, $entityClass, $config); |
641
|
|
|
} |
642
|
|
|
} |
643
|
|
|
|
644
|
|
|
return $result; |
645
|
|
|
} |
646
|
|
|
|
647
|
|
|
/** |
648
|
|
|
* @param string $entityClass |
649
|
|
|
* @param EntityConfig $config |
650
|
|
|
* |
651
|
|
|
* @return bool |
652
|
|
|
*/ |
653
|
|
|
protected function hasAssociations($entityClass, EntityConfig $config) |
654
|
|
|
{ |
655
|
|
|
$entityMetadata = $this->doctrineHelper->getEntityMetadata($entityClass); |
656
|
|
|
$fields = $this->fieldAccessor->getFields($entityClass, $config); |
657
|
|
|
foreach ($fields as $field) { |
658
|
|
|
if ($entityMetadata->isAssociation($field)) { |
659
|
|
|
return true; |
660
|
|
|
} |
661
|
|
|
} |
662
|
|
|
|
663
|
|
|
return false; |
664
|
|
|
} |
665
|
|
|
|
666
|
|
|
/** |
667
|
|
|
* @param object[] $entities A list of entities |
668
|
|
|
* @param string $idFieldName The name of entity identifier field |
669
|
|
|
* |
670
|
|
|
* @return array of unique ids of all entities from $entities array |
671
|
|
|
*/ |
672
|
|
|
protected function getEntityIds($entities, $idFieldName) |
673
|
|
|
{ |
674
|
|
|
$ids = []; |
675
|
|
|
foreach ($entities as $entity) { |
676
|
|
|
$id = $this->dataAccessor->getValue($entity, $idFieldName); |
677
|
|
|
if (!isset($ids[$id])) { |
678
|
|
|
$ids[$id] = $id; |
679
|
|
|
} |
680
|
|
|
} |
681
|
|
|
|
682
|
|
|
return array_values($ids); |
683
|
|
|
} |
684
|
|
|
|
685
|
|
|
/** |
686
|
|
|
* @param string $entityClass |
687
|
|
|
* |
688
|
|
|
* @return string|null |
689
|
|
|
*/ |
690
|
|
|
protected function getEntityIdType($entityClass) |
691
|
|
|
{ |
692
|
|
|
$metadata = $this->doctrineHelper->getEntityMetadata($entityClass); |
693
|
|
|
|
694
|
|
|
return $metadata->getFieldType($metadata->getSingleIdentifierFieldName()); |
695
|
|
|
} |
696
|
|
|
|
697
|
|
|
/** |
698
|
|
|
* @param mixed $value |
699
|
|
|
* @param string $type |
700
|
|
|
* |
701
|
|
|
* @return mixed |
702
|
|
|
*/ |
703
|
|
|
protected function getTypedEntityId($value, $type) |
704
|
|
|
{ |
705
|
|
|
if (Type::INTEGER === $type || Type::SMALLINT === $type) { |
706
|
|
|
$value = (int)$value; |
707
|
|
|
} |
708
|
|
|
|
709
|
|
|
return $value; |
710
|
|
|
} |
711
|
|
|
|
712
|
|
|
/** |
713
|
|
|
* @param EntityConfig $config |
714
|
|
|
* @param string $field |
715
|
|
|
* |
716
|
|
|
* @return EntityConfig |
717
|
|
|
*/ |
718
|
|
|
public function getTargetEntity(EntityConfig $config, $field) |
719
|
|
|
{ |
720
|
|
|
$fieldConfig = $config->getField($field); |
721
|
|
|
if (null === $fieldConfig) { |
722
|
|
|
return new InternalEntityConfig(); |
723
|
|
|
} |
724
|
|
|
|
725
|
|
|
$targetConfig = $fieldConfig->getTargetEntity(); |
726
|
|
|
if (null === $targetConfig) { |
727
|
|
|
$targetConfig = new InternalEntityConfig(); |
728
|
|
|
$fieldConfig->setTargetEntity($targetConfig); |
729
|
|
|
} |
730
|
|
|
|
731
|
|
|
return $targetConfig; |
732
|
|
|
} |
733
|
|
|
|
734
|
|
|
/** |
735
|
|
|
* @param string $entityClass |
736
|
|
|
* @param string $fieldName |
737
|
|
|
* @param mixed $fieldValue |
738
|
|
|
* @param FieldConfig|null $fieldConfig |
739
|
|
|
* |
740
|
|
|
* @return mixed |
741
|
|
|
*/ |
742
|
|
|
protected function transformValue($entityClass, $fieldName, $fieldValue, FieldConfig $fieldConfig = null) |
743
|
|
|
{ |
744
|
|
|
return $this->dataTransformer->transform( |
745
|
|
|
$entityClass, |
746
|
|
|
$fieldName, |
747
|
|
|
$fieldValue, |
748
|
|
|
null !== $fieldConfig ? $fieldConfig->toArray(true) : [] |
749
|
|
|
); |
750
|
|
|
} |
751
|
|
|
|
752
|
|
|
/** |
753
|
|
|
* @param array $item |
754
|
|
|
* @param callable $handler |
755
|
|
|
* |
756
|
|
|
* @return array |
757
|
|
|
*/ |
758
|
|
|
protected function postSerialize(array $item, $handler) |
759
|
|
|
{ |
760
|
|
|
// @deprecated since 1.9. New signature of 'post_serialize' callback is function (array $item) : array |
761
|
|
|
// Old signature was function (array &$item) : void |
762
|
|
|
// The following implementation supports both new and old signature of the callback |
763
|
|
|
// Remove this implementation when a support of old signature will not be required |
764
|
|
|
if ($handler instanceof \Closure) { |
765
|
|
|
$handleResult = $handler($item); |
766
|
|
|
if (null !== $handleResult) { |
767
|
|
|
$item = $handleResult; |
768
|
|
|
} |
769
|
|
|
} else { |
770
|
|
|
$item = call_user_func($handler, $item); |
771
|
|
|
} |
772
|
|
|
|
773
|
|
|
/* New implementation, uncomment it when a support of old signature will not be required |
774
|
|
|
$item = call_user_func($handler, $item); |
775
|
|
|
*/ |
776
|
|
|
|
777
|
|
|
return $item; |
778
|
|
|
} |
779
|
|
|
} |
780
|
|
|
|
In PHP, under loose comparison (like
==
, or!=
, orswitch
conditions), values of different types might be equal.For
string
values, the empty string''
is a special case, in particular the following results might be unexpected: