EntitySerializer   F
last analyzed

Complexity

Total Complexity 96

Size/Duplication

Total Lines 662
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 15
Metric Value
wmc 96
lcom 1
cbo 15
dl 0
loc 662
rs 1.3831

25 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 19 1
A serialize() 0 10 1
A serializeEntities() 0 8 1
A prepareQuery() 0 4 1
A normalizeConfig() 0 8 2
C serializeItems() 0 31 7
C serializeItem() 0 55 12
B updateQuery() 0 31 5
A getExistingJoinAlias() 0 14 4
A updateSelectQueryPart() 0 14 2
B loadRelatedData() 0 21 5
B applyRelatedData() 0 20 6
A isSingleStepLoading() 0 6 2
D loadRelatedItems() 0 42 9
C getIdFieldNameIfIdOnlyRequested() 0 24 7
A getRelatedItemsBindings() 0 14 3
A getRelatedItemsIds() 0 13 4
B loadRelatedItemsForSimpleEntity() 0 35 6
A hasAssociations() 0 12 3
A getEntityIds() 0 12 3
A getEntityIdType() 0 6 1
A getTypedEntityId() 0 8 3
A getTargetEntity() 0 15 3
A transformValue() 0 9 2
A postSerialize() 0 21 3

How to fix   Complexity   

Complex Class

Complex classes like EntitySerializer often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use EntitySerializer, and based on these observations, apply Extract Interface, too.

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) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $alias of type string|null is loosely compared to false; this is ambiguous if the string can be empty. 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 string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
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);
0 ignored issues
show
Bug introduced by
It seems like $relatedEntityIdType defined by $this->getEntityIdType($mapping['targetEntity']) on line 576 can also be of type null or object<Doctrine\DBAL\Types\Type>; however, Oro\Component\EntitySeri...zer::getTypedEntityId() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
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