Total Complexity | 159 |
Total Lines | 1180 |
Duplicated Lines | 0 % |
Changes | 0 |
Complex classes like EntityReader 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.
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 EntityReader, and based on these observations, apply Extract Interface, too.
1 | <?php declare(strict_types=1); |
||
43 | #[Package('core')] |
||
44 | class EntityReader implements EntityReaderInterface |
||
45 | { |
||
46 | final public const INTERNAL_MAPPING_STORAGE = 'internal_mapping_storage'; |
||
47 | final public const FOREIGN_KEYS = 'foreignKeys'; |
||
48 | final public const MANY_TO_MANY_LIMIT_QUERY = 'many_to_many_limit_query'; |
||
49 | |||
50 | public function __construct( |
||
51 | private readonly Connection $connection, |
||
52 | private readonly EntityHydrator $hydrator, |
||
53 | private readonly EntityDefinitionQueryHelper $queryHelper, |
||
54 | private readonly SqlQueryParser $parser, |
||
55 | private readonly CriteriaQueryBuilder $criteriaQueryBuilder, |
||
56 | private readonly LoggerInterface $logger, |
||
57 | private readonly CriteriaFieldsResolver $criteriaFieldsResolver |
||
58 | ) { |
||
59 | } |
||
60 | |||
61 | /** |
||
62 | * @return EntityCollection<Entity> |
||
63 | */ |
||
64 | public function read(EntityDefinition $definition, Criteria $criteria, Context $context): EntityCollection |
||
65 | { |
||
66 | $criteria->resetSorting(); |
||
67 | $criteria->resetQueries(); |
||
68 | |||
69 | /** @var EntityCollection<Entity> $collectionClass */ |
||
70 | $collectionClass = $definition->getCollectionClass(); |
||
71 | |||
72 | $fields = $this->criteriaFieldsResolver->resolve($criteria, $definition); |
||
73 | |||
74 | return $this->_read( |
||
75 | $criteria, |
||
76 | $definition, |
||
77 | $context, |
||
78 | new $collectionClass(), |
||
79 | $definition->getFields()->getBasicFields(), |
||
80 | true, |
||
81 | $fields |
||
82 | ); |
||
83 | } |
||
84 | |||
85 | protected function getParser(): SqlQueryParser |
||
86 | { |
||
87 | return $this->parser; |
||
88 | } |
||
89 | |||
90 | /** |
||
91 | * @param EntityCollection<Entity> $collection |
||
92 | * @param array<string, mixed> $partial |
||
93 | * |
||
94 | * @return EntityCollection<Entity> |
||
95 | */ |
||
96 | private function _read( |
||
97 | Criteria $criteria, |
||
98 | EntityDefinition $definition, |
||
99 | Context $context, |
||
100 | EntityCollection $collection, |
||
101 | FieldCollection $fields, |
||
102 | bool $performEmptySearch = false, |
||
103 | array $partial = [] |
||
104 | ): EntityCollection { |
||
105 | $hasFilters = !empty($criteria->getFilters()) || !empty($criteria->getPostFilters()); |
||
106 | $hasIds = !empty($criteria->getIds()); |
||
107 | |||
108 | if (!$performEmptySearch && !$hasFilters && !$hasIds) { |
||
109 | return $collection; |
||
110 | } |
||
111 | |||
112 | if ($partial !== []) { |
||
113 | $fields = $definition->getFields()->filter(function (Field $field) use (&$partial) { |
||
114 | if ($field->getFlag(PrimaryKey::class)) { |
||
115 | $partial[$field->getPropertyName()] = []; |
||
116 | |||
117 | return true; |
||
118 | } |
||
119 | |||
120 | return isset($partial[$field->getPropertyName()]); |
||
121 | }); |
||
122 | } |
||
123 | |||
124 | // always add the criteria fields to the collection, otherwise we have conflicts between criteria.fields and criteria.association logic |
||
125 | $fields = $this->addAssociationFieldsToCriteria($criteria, $definition, $fields); |
||
126 | |||
127 | if ($definition->isInheritanceAware() && $criteria->hasAssociation('parent')) { |
||
128 | throw new ParentAssociationCanNotBeFetched(); |
||
129 | } |
||
130 | |||
131 | $rows = $this->fetch($criteria, $definition, $context, $fields, $partial); |
||
132 | |||
133 | $collection = $this->hydrator->hydrate($collection, $definition->getEntityClass(), $definition, $rows, $definition->getEntityName(), $context, $partial); |
||
134 | |||
135 | $collection = $this->fetchAssociations($criteria, $definition, $context, $collection, $fields, $partial); |
||
136 | |||
137 | $hasIds = !empty($criteria->getIds()); |
||
138 | if ($hasIds && empty($criteria->getSorting())) { |
||
139 | $collection->sortByIdArray($criteria->getIds()); |
||
140 | } |
||
141 | |||
142 | return $collection; |
||
143 | } |
||
144 | |||
145 | /** |
||
146 | * @param array<string, mixed> $partial |
||
147 | */ |
||
148 | private function joinBasic( |
||
149 | EntityDefinition $definition, |
||
150 | Context $context, |
||
151 | string $root, |
||
152 | QueryBuilder $query, |
||
153 | FieldCollection $fields, |
||
154 | ?Criteria $criteria = null, |
||
155 | array $partial = [] |
||
156 | ): void { |
||
157 | $isPartial = $partial !== []; |
||
158 | $filtered = $fields->filter(static function (Field $field) use ($isPartial, $partial) { |
||
159 | if ($field->is(Runtime::class)) { |
||
160 | return false; |
||
161 | } |
||
162 | |||
163 | if (!$isPartial || $field->getFlag(PrimaryKey::class)) { |
||
164 | return true; |
||
165 | } |
||
166 | |||
167 | return isset($partial[$field->getPropertyName()]); |
||
168 | }); |
||
169 | |||
170 | $parentAssociation = null; |
||
171 | |||
172 | if ($definition->isInheritanceAware() && $context->considerInheritance()) { |
||
173 | $parentAssociation = $definition->getFields()->get('parent'); |
||
174 | |||
175 | if ($parentAssociation !== null) { |
||
176 | $this->queryHelper->resolveField($parentAssociation, $definition, $root, $query, $context); |
||
177 | } |
||
178 | } |
||
179 | |||
180 | $addTranslation = false; |
||
181 | |||
182 | /** @var Field $field */ |
||
183 | foreach ($filtered as $field) { |
||
184 | // translated fields are handled after loop all together |
||
185 | if ($field instanceof TranslatedField) { |
||
186 | $this->queryHelper->resolveField($field, $definition, $root, $query, $context); |
||
187 | |||
188 | $addTranslation = true; |
||
189 | |||
190 | continue; |
||
191 | } |
||
192 | |||
193 | // self references can not be resolved if set to autoload, otherwise we get an endless loop |
||
194 | if (!$field instanceof ParentAssociationField && $field instanceof AssociationField && $field->getAutoload() && $field->getReferenceDefinition() === $definition) { |
||
195 | continue; |
||
196 | } |
||
197 | |||
198 | // many to one associations can be directly fetched in same query |
||
199 | if ($field instanceof ManyToOneAssociationField || $field instanceof OneToOneAssociationField) { |
||
200 | $reference = $field->getReferenceDefinition(); |
||
201 | |||
202 | $basics = $reference->getFields()->getBasicFields(); |
||
203 | |||
204 | $this->queryHelper->resolveField($field, $definition, $root, $query, $context); |
||
205 | |||
206 | $alias = $root . '.' . $field->getPropertyName(); |
||
207 | |||
208 | $joinCriteria = null; |
||
209 | if ($criteria && $criteria->hasAssociation($field->getPropertyName())) { |
||
210 | $joinCriteria = $criteria->getAssociation($field->getPropertyName()); |
||
211 | $basics = $this->addAssociationFieldsToCriteria($joinCriteria, $reference, $basics); |
||
212 | } |
||
213 | |||
214 | $this->joinBasic($reference, $context, $alias, $query, $basics, $joinCriteria, $partial[$field->getPropertyName()] ?? []); |
||
215 | |||
216 | continue; |
||
217 | } |
||
218 | |||
219 | // add sub select for many to many field |
||
220 | if ($field instanceof ManyToManyAssociationField) { |
||
221 | if ($this->isAssociationRestricted($criteria, $field->getPropertyName())) { |
||
222 | continue; |
||
223 | } |
||
224 | |||
225 | // requested a paginated, filtered or sorted list |
||
226 | |||
227 | $this->addManyToManySelect($definition, $root, $field, $query, $context); |
||
228 | |||
229 | continue; |
||
230 | } |
||
231 | |||
232 | // other associations like OneToManyAssociationField fetched lazy by additional query |
||
233 | if ($field instanceof AssociationField) { |
||
234 | continue; |
||
235 | } |
||
236 | |||
237 | if ($parentAssociation !== null |
||
238 | && $field instanceof StorageAware |
||
239 | && $field->is(Inherited::class) |
||
240 | && $context->considerInheritance() |
||
241 | ) { |
||
242 | $parentAlias = $root . '.' . $parentAssociation->getPropertyName(); |
||
243 | |||
244 | // contains the field accessor for the child value (eg. `product.name`.`name`) |
||
245 | $childAccessor = EntityDefinitionQueryHelper::escape($root) . '.' |
||
246 | . EntityDefinitionQueryHelper::escape($field->getStorageName()); |
||
247 | |||
248 | // contains the field accessor for the parent value (eg. `product.parent`.`name`) |
||
249 | $parentAccessor = EntityDefinitionQueryHelper::escape($parentAlias) . '.' |
||
250 | . EntityDefinitionQueryHelper::escape($field->getStorageName()); |
||
251 | |||
252 | // contains the alias for the resolved field (eg. `product.name`) |
||
253 | $fieldAlias = EntityDefinitionQueryHelper::escape($root . '.' . $field->getPropertyName()); |
||
254 | |||
255 | if ($field instanceof JsonField) { |
||
256 | // merged in hydrator |
||
257 | $parentFieldAlias = EntityDefinitionQueryHelper::escape($root . '.' . $field->getPropertyName() . '.inherited'); |
||
258 | $query->addSelect(sprintf('%s as %s', $parentAccessor, $parentFieldAlias)); |
||
259 | } |
||
260 | // add selection for resolved parent-child inheritance field |
||
261 | $query->addSelect(sprintf('COALESCE(%s, %s) as %s', $childAccessor, $parentAccessor, $fieldAlias)); |
||
262 | |||
263 | continue; |
||
264 | } |
||
265 | |||
266 | // all other StorageAware fields are stored inside the main entity |
||
267 | if ($field instanceof StorageAware) { |
||
268 | $query->addSelect( |
||
269 | EntityDefinitionQueryHelper::escape($root) . '.' |
||
270 | . EntityDefinitionQueryHelper::escape($field->getStorageName()) . ' as ' |
||
271 | . EntityDefinitionQueryHelper::escape($root . '.' . $field->getPropertyName()) |
||
272 | ); |
||
273 | } |
||
274 | } |
||
275 | |||
276 | if ($addTranslation) { |
||
277 | $this->queryHelper->addTranslationSelect($root, $definition, $query, $context, $partial); |
||
278 | } |
||
279 | } |
||
280 | |||
281 | /** |
||
282 | * @param array<string, mixed> $partial |
||
283 | * |
||
284 | * @return list<array<string, mixed>> |
||
285 | */ |
||
286 | private function fetch(Criteria $criteria, EntityDefinition $definition, Context $context, FieldCollection $fields, array $partial = []): array |
||
287 | { |
||
288 | $table = $definition->getEntityName(); |
||
289 | |||
290 | $query = $this->criteriaQueryBuilder->build( |
||
291 | new QueryBuilder($this->connection), |
||
292 | $definition, |
||
293 | $criteria, |
||
294 | $context |
||
295 | ); |
||
296 | |||
297 | $this->joinBasic($definition, $context, $table, $query, $fields, $criteria, $partial); |
||
298 | |||
299 | if (!empty($criteria->getIds())) { |
||
300 | $this->queryHelper->addIdCondition($criteria, $definition, $query); |
||
301 | } |
||
302 | |||
303 | if ($criteria->getTitle()) { |
||
304 | $query->setTitle($criteria->getTitle() . '::read'); |
||
305 | } |
||
306 | |||
307 | return $query->executeQuery()->fetchAllAssociative(); |
||
308 | } |
||
309 | |||
310 | /** |
||
311 | * @param EntityCollection<Entity> $collection |
||
312 | * @param array<string, mixed> $partial |
||
313 | */ |
||
314 | private function loadManyToMany( |
||
315 | Criteria $criteria, |
||
316 | ManyToManyAssociationField $association, |
||
317 | Context $context, |
||
318 | EntityCollection $collection, |
||
319 | array $partial |
||
320 | ): void { |
||
321 | $associationCriteria = $criteria->getAssociation($association->getPropertyName()); |
||
322 | |||
323 | if (!$associationCriteria->getTitle() && $criteria->getTitle()) { |
||
324 | $associationCriteria->setTitle( |
||
325 | $criteria->getTitle() . '::association::' . $association->getPropertyName() |
||
326 | ); |
||
327 | } |
||
328 | |||
329 | // check if the requested criteria is restricted (limit, offset, sorting, filtering) |
||
330 | if ($this->isAssociationRestricted($criteria, $association->getPropertyName())) { |
||
331 | // if restricted load paginated list of many to many |
||
332 | $this->loadManyToManyWithCriteria($associationCriteria, $association, $context, $collection, $partial); |
||
333 | |||
334 | return; |
||
335 | } |
||
336 | |||
337 | // otherwise the association is loaded in the root query of the entity as sub select which contains all ids |
||
338 | // the ids are extracted in the entity hydrator (see: \Shopware\Core\Framework\DataAbstractionLayer\Dbal\EntityHydrator::extractManyToManyIds) |
||
339 | $this->loadManyToManyOverExtension($associationCriteria, $association, $context, $collection, $partial); |
||
340 | } |
||
341 | |||
342 | private function addManyToManySelect( |
||
343 | EntityDefinition $definition, |
||
344 | string $root, |
||
345 | ManyToManyAssociationField $field, |
||
346 | QueryBuilder $query, |
||
347 | Context $context |
||
348 | ): void { |
||
349 | $mapping = $field->getMappingDefinition(); |
||
350 | |||
351 | $versionCondition = ''; |
||
352 | if ($mapping->isVersionAware() && $definition->isVersionAware() && $field->is(CascadeDelete::class)) { |
||
353 | $versionField = $definition->getEntityName() . '_version_id'; |
||
354 | $versionCondition = ' AND #alias#.' . $versionField . ' = #root#.version_id'; |
||
355 | } |
||
356 | |||
357 | $source = EntityDefinitionQueryHelper::escape($root) . '.' . EntityDefinitionQueryHelper::escape($field->getLocalField()); |
||
358 | if ($field->is(Inherited::class) && $context->considerInheritance()) { |
||
359 | $source = EntityDefinitionQueryHelper::escape($root) . '.' . EntityDefinitionQueryHelper::escape($field->getPropertyName()); |
||
360 | } |
||
361 | |||
362 | $parameters = [ |
||
363 | '#alias#' => EntityDefinitionQueryHelper::escape($root . '.' . $field->getPropertyName() . '.mapping'), |
||
364 | '#mapping_reference_column#' => EntityDefinitionQueryHelper::escape($field->getMappingReferenceColumn()), |
||
365 | '#mapping_table#' => EntityDefinitionQueryHelper::escape($mapping->getEntityName()), |
||
366 | '#mapping_local_column#' => EntityDefinitionQueryHelper::escape($field->getMappingLocalColumn()), |
||
367 | '#root#' => EntityDefinitionQueryHelper::escape($root), |
||
368 | '#source#' => $source, |
||
369 | '#property#' => EntityDefinitionQueryHelper::escape($root . '.' . $field->getPropertyName() . '.id_mapping'), |
||
370 | ]; |
||
371 | |||
372 | $query->addSelect( |
||
373 | str_replace( |
||
374 | array_keys($parameters), |
||
375 | array_values($parameters), |
||
376 | '(SELECT GROUP_CONCAT(HEX(#alias#.#mapping_reference_column#) SEPARATOR \'||\') |
||
377 | FROM #mapping_table# #alias# |
||
378 | WHERE #alias#.#mapping_local_column# = #source#' |
||
379 | . $versionCondition |
||
380 | . ' ) as #property#' |
||
381 | ) |
||
382 | ); |
||
383 | } |
||
384 | |||
385 | /** |
||
386 | * @param EntityCollection<Entity> $collection |
||
387 | * |
||
388 | * @return array<string> |
||
389 | */ |
||
390 | private function collectManyToManyIds(EntityCollection $collection, AssociationField $association): array |
||
391 | { |
||
392 | $ids = []; |
||
393 | $property = $association->getPropertyName(); |
||
394 | /** @var Entity $struct */ |
||
395 | foreach ($collection as $struct) { |
||
396 | /** @var ArrayStruct<string, mixed> $ext */ |
||
397 | $ext = $struct->getExtension(self::INTERNAL_MAPPING_STORAGE); |
||
398 | /** @var array<string> $tmp */ |
||
399 | $tmp = $ext->get($property); |
||
400 | foreach ($tmp as $id) { |
||
401 | $ids[] = $id; |
||
402 | } |
||
403 | } |
||
404 | |||
405 | return $ids; |
||
406 | } |
||
407 | |||
408 | /** |
||
409 | * @param EntityCollection<Entity> $collection |
||
410 | * @param array<string, mixed> $partial |
||
411 | */ |
||
412 | private function loadOneToMany( |
||
413 | Criteria $criteria, |
||
414 | EntityDefinition $definition, |
||
415 | OneToManyAssociationField $association, |
||
416 | Context $context, |
||
417 | EntityCollection $collection, |
||
418 | array $partial |
||
419 | ): void { |
||
420 | $fieldCriteria = new Criteria(); |
||
421 | if ($criteria->hasAssociation($association->getPropertyName())) { |
||
422 | $fieldCriteria = $criteria->getAssociation($association->getPropertyName()); |
||
423 | } |
||
424 | |||
425 | if (!$fieldCriteria->getTitle() && $criteria->getTitle()) { |
||
426 | $fieldCriteria->setTitle( |
||
427 | $criteria->getTitle() . '::association::' . $association->getPropertyName() |
||
428 | ); |
||
429 | } |
||
430 | |||
431 | // association should not be paginated > load data over foreign key condition |
||
432 | if ($fieldCriteria->getLimit() === null) { |
||
433 | $this->loadOneToManyWithoutPagination($definition, $association, $context, $collection, $fieldCriteria, $partial); |
||
434 | |||
435 | return; |
||
436 | } |
||
437 | |||
438 | // load association paginated > use internal counter loops |
||
439 | $this->loadOneToManyWithPagination($definition, $association, $context, $collection, $fieldCriteria, $partial); |
||
440 | } |
||
441 | |||
442 | /** |
||
443 | * @param EntityCollection<Entity> $collection |
||
444 | * @param array<string, mixed> $partial |
||
445 | */ |
||
446 | private function loadOneToManyWithoutPagination( |
||
447 | EntityDefinition $definition, |
||
448 | OneToManyAssociationField $association, |
||
449 | Context $context, |
||
450 | EntityCollection $collection, |
||
451 | Criteria $fieldCriteria, |
||
452 | array $partial |
||
453 | ): void { |
||
454 | $ref = $association->getReferenceDefinition()->getFields()->getByStorageName( |
||
455 | $association->getReferenceField() |
||
456 | ); |
||
457 | |||
458 | \assert($ref instanceof Field); |
||
459 | |||
460 | $propertyName = $ref->getPropertyName(); |
||
461 | if ($association instanceof ChildrenAssociationField) { |
||
462 | $propertyName = 'parentId'; |
||
463 | } |
||
464 | |||
465 | // build orm property accessor to add field sortings and conditions `customer_address.customerId` |
||
466 | $propertyAccessor = $association->getReferenceDefinition()->getEntityName() . '.' . $propertyName; |
||
467 | |||
468 | $ids = array_values($collection->getIds()); |
||
469 | |||
470 | $isInheritanceAware = $definition->isInheritanceAware() && $context->considerInheritance(); |
||
471 | |||
472 | if ($isInheritanceAware) { |
||
473 | $parentIds = array_values(\array_filter($collection->map(fn (Entity $entity) => $entity->get('parentId')))); |
||
474 | |||
475 | $ids = array_unique([...$ids, ...$parentIds]); |
||
476 | } |
||
477 | |||
478 | $fieldCriteria->addFilter(new EqualsAnyFilter($propertyAccessor, $ids)); |
||
479 | |||
480 | $referenceClass = $association->getReferenceDefinition(); |
||
481 | /** @var EntityCollection<Entity> $collectionClass */ |
||
482 | $collectionClass = $referenceClass->getCollectionClass(); |
||
483 | |||
484 | if ($partial !== []) { |
||
485 | // Make sure our collection index will be loaded |
||
486 | $partial[$propertyName] = []; |
||
487 | $collectionClass = EntityCollection::class; |
||
488 | } |
||
489 | |||
490 | $data = $this->_read( |
||
491 | $fieldCriteria, |
||
492 | $referenceClass, |
||
493 | $context, |
||
494 | new $collectionClass(), |
||
495 | $referenceClass->getFields()->getBasicFields(), |
||
496 | false, |
||
497 | $partial |
||
498 | ); |
||
499 | |||
500 | $grouped = []; |
||
501 | foreach ($data as $entity) { |
||
502 | $fk = $entity->get($propertyName); |
||
503 | |||
504 | $grouped[$fk][] = $entity; |
||
505 | } |
||
506 | |||
507 | // assign loaded data to root entities |
||
508 | foreach ($collection as $entity) { |
||
509 | $structData = new $collectionClass(); |
||
510 | if (isset($grouped[$entity->getUniqueIdentifier()])) { |
||
511 | $structData->fill($grouped[$entity->getUniqueIdentifier()]); |
||
512 | } |
||
513 | |||
514 | // assign data of child immediately |
||
515 | if ($association->is(Extension::class)) { |
||
516 | $entity->addExtension($association->getPropertyName(), $structData); |
||
517 | } else { |
||
518 | // otherwise the data will be assigned directly as properties |
||
519 | $entity->assign([$association->getPropertyName() => $structData]); |
||
520 | } |
||
521 | |||
522 | if (!$association->is(Inherited::class) || $structData->count() > 0 || !$context->considerInheritance()) { |
||
523 | continue; |
||
524 | } |
||
525 | |||
526 | // if association can be inherited by the parent and the struct data is empty, filter again for the parent id |
||
527 | $structData = new $collectionClass(); |
||
528 | if (isset($grouped[$entity->get('parentId')])) { |
||
529 | $structData->fill($grouped[$entity->get('parentId')]); |
||
530 | } |
||
531 | |||
532 | if ($association->is(Extension::class)) { |
||
533 | $entity->addExtension($association->getPropertyName(), $structData); |
||
534 | |||
535 | continue; |
||
536 | } |
||
537 | $entity->assign([$association->getPropertyName() => $structData]); |
||
538 | } |
||
539 | } |
||
540 | |||
541 | /** |
||
542 | * @param EntityCollection<Entity> $collection |
||
543 | * @param array<string, mixed> $partial |
||
544 | */ |
||
545 | private function loadOneToManyWithPagination( |
||
546 | EntityDefinition $definition, |
||
547 | OneToManyAssociationField $association, |
||
548 | Context $context, |
||
549 | EntityCollection $collection, |
||
550 | Criteria $fieldCriteria, |
||
551 | array $partial |
||
552 | ): void { |
||
553 | $isPartial = $partial !== []; |
||
554 | |||
555 | $propertyAccessor = $this->buildOneToManyPropertyAccessor($definition, $association); |
||
556 | |||
557 | // inject sorting for foreign key, otherwise the internal counter wouldn't work `order by customer_address.customer_id, other_sortings` |
||
558 | $sorting = array_merge( |
||
559 | [new FieldSorting($propertyAccessor, FieldSorting::ASCENDING)], |
||
560 | $fieldCriteria->getSorting() |
||
561 | ); |
||
562 | |||
563 | $fieldCriteria->resetSorting(); |
||
564 | $fieldCriteria->addSorting(...$sorting); |
||
565 | |||
566 | $ids = array_values($collection->getIds()); |
||
567 | |||
568 | if ($isPartial) { |
||
569 | // Make sure our collection index will be loaded |
||
570 | $partial[$association->getPropertyName()] = []; |
||
571 | } |
||
572 | |||
573 | $isInheritanceAware = $definition->isInheritanceAware() && $context->considerInheritance(); |
||
574 | |||
575 | if ($isInheritanceAware) { |
||
576 | $parentIds = array_values(\array_filter($collection->map(fn (Entity $entity) => $entity->get('parentId')))); |
||
577 | |||
578 | $ids = array_unique([...$ids, ...$parentIds]); |
||
579 | } |
||
580 | |||
581 | $fieldCriteria->addFilter(new EqualsAnyFilter($propertyAccessor, $ids)); |
||
582 | |||
583 | $mapping = $this->fetchPaginatedOneToManyMapping($definition, $association, $context, $collection, $fieldCriteria); |
||
584 | |||
585 | $ids = []; |
||
586 | foreach ($mapping as $associationIds) { |
||
587 | foreach ($associationIds as $associationId) { |
||
588 | $ids[] = $associationId; |
||
589 | } |
||
590 | } |
||
591 | |||
592 | $fieldCriteria->setIds(\array_filter($ids)); |
||
593 | $fieldCriteria->resetSorting(); |
||
594 | $fieldCriteria->resetFilters(); |
||
595 | $fieldCriteria->resetPostFilters(); |
||
596 | |||
597 | $referenceClass = $association->getReferenceDefinition(); |
||
598 | /** @var EntityCollection<Entity> $collectionClass */ |
||
599 | $collectionClass = $referenceClass->getCollectionClass(); |
||
600 | |||
601 | $data = $this->_read( |
||
602 | $fieldCriteria, |
||
603 | $referenceClass, |
||
604 | $context, |
||
605 | new $collectionClass(), |
||
606 | $referenceClass->getFields()->getBasicFields(), |
||
607 | false, |
||
608 | $partial |
||
609 | ); |
||
610 | |||
611 | // assign loaded reference collections to root entities |
||
612 | /** @var Entity $entity */ |
||
613 | foreach ($collection as $entity) { |
||
614 | // extract mapping ids for the current entity |
||
615 | $mappingIds = $mapping[$entity->getUniqueIdentifier()] ?? []; |
||
616 | |||
617 | $structData = $data->getList($mappingIds); |
||
618 | |||
619 | // assign data of child immediately |
||
620 | if ($association->is(Extension::class)) { |
||
621 | $entity->addExtension($association->getPropertyName(), $structData); |
||
622 | } else { |
||
623 | $entity->assign([$association->getPropertyName() => $structData]); |
||
624 | } |
||
625 | |||
626 | if (!$association->is(Inherited::class) || $structData->count() > 0 || !$context->considerInheritance()) { |
||
627 | continue; |
||
628 | } |
||
629 | |||
630 | $parentId = $entity->get('parentId'); |
||
631 | |||
632 | if ($parentId === null) { |
||
633 | continue; |
||
634 | } |
||
635 | |||
636 | // extract mapping ids for the current entity |
||
637 | $mappingIds = $mapping[$parentId]; |
||
638 | |||
639 | $structData = $data->getList($mappingIds); |
||
640 | |||
641 | // assign data of child immediately |
||
642 | if ($association->is(Extension::class)) { |
||
643 | $entity->addExtension($association->getPropertyName(), $structData); |
||
644 | } else { |
||
645 | $entity->assign([$association->getPropertyName() => $structData]); |
||
646 | } |
||
647 | } |
||
648 | } |
||
649 | |||
650 | /** |
||
651 | * @param EntityCollection<Entity> $collection |
||
652 | * @param array<string, mixed> $partial |
||
653 | */ |
||
654 | private function loadManyToManyOverExtension( |
||
655 | Criteria $criteria, |
||
656 | ManyToManyAssociationField $association, |
||
657 | Context $context, |
||
658 | EntityCollection $collection, |
||
659 | array $partial |
||
660 | ): void { |
||
661 | // collect all ids of many to many association which already stored inside the struct instances |
||
662 | $ids = $this->collectManyToManyIds($collection, $association); |
||
663 | |||
664 | $criteria->setIds($ids); |
||
665 | |||
666 | $referenceClass = $association->getToManyReferenceDefinition(); |
||
667 | /** @var EntityCollection<Entity> $collectionClass */ |
||
668 | $collectionClass = $referenceClass->getCollectionClass(); |
||
669 | |||
670 | $data = $this->_read( |
||
671 | $criteria, |
||
672 | $referenceClass, |
||
673 | $context, |
||
674 | new $collectionClass(), |
||
675 | $referenceClass->getFields()->getBasicFields(), |
||
676 | false, |
||
677 | $partial |
||
678 | ); |
||
679 | |||
680 | /** @var Entity $struct */ |
||
681 | foreach ($collection as $struct) { |
||
682 | /** @var ArrayEntity $extension */ |
||
683 | $extension = $struct->getExtension(self::INTERNAL_MAPPING_STORAGE); |
||
684 | |||
685 | // use assign function to avoid setter name building |
||
686 | $structData = $data->getList( |
||
687 | $extension->get($association->getPropertyName()) |
||
688 | ); |
||
689 | |||
690 | // if the association is added as extension (for plugins), we have to add the data as extension |
||
691 | if ($association->is(Extension::class)) { |
||
692 | $struct->addExtension($association->getPropertyName(), $structData); |
||
693 | } else { |
||
694 | $struct->assign([$association->getPropertyName() => $structData]); |
||
695 | } |
||
696 | } |
||
697 | } |
||
698 | |||
699 | /** |
||
700 | * @param EntityCollection<Entity> $collection |
||
701 | * @param array<string, mixed> $partial |
||
702 | */ |
||
703 | private function loadManyToManyWithCriteria( |
||
704 | Criteria $fieldCriteria, |
||
705 | ManyToManyAssociationField $association, |
||
706 | Context $context, |
||
707 | EntityCollection $collection, |
||
708 | array $partial |
||
709 | ): void { |
||
710 | $fields = $association->getToManyReferenceDefinition()->getFields(); |
||
711 | $reference = null; |
||
712 | foreach ($fields as $field) { |
||
713 | if (!$field instanceof ManyToManyAssociationField) { |
||
714 | continue; |
||
715 | } |
||
716 | |||
717 | if ($field->getReferenceDefinition() !== $association->getReferenceDefinition()) { |
||
718 | continue; |
||
719 | } |
||
720 | |||
721 | $reference = $field; |
||
722 | |||
723 | break; |
||
724 | } |
||
725 | |||
726 | if (!$reference) { |
||
727 | throw new \RuntimeException( |
||
728 | sprintf( |
||
729 | 'No inverse many to many association found, for association %s', |
||
730 | $association->getPropertyName() |
||
731 | ) |
||
732 | ); |
||
733 | } |
||
734 | |||
735 | // build inverse accessor `product.categories.id` |
||
736 | $accessor = $association->getToManyReferenceDefinition()->getEntityName() . '.' . $reference->getPropertyName() . '.id'; |
||
737 | |||
738 | $fieldCriteria->addFilter(new EqualsAnyFilter($accessor, $collection->getIds())); |
||
739 | |||
740 | $root = EntityDefinitionQueryHelper::escape( |
||
741 | $association->getToManyReferenceDefinition()->getEntityName() . '.' . $reference->getPropertyName() . '.mapping' |
||
742 | ); |
||
743 | |||
744 | $query = new QueryBuilder($this->connection); |
||
745 | // to many selects results in a `group by` clause. In this case the order by parts will be executed with MIN/MAX aggregation |
||
746 | // but at this point the order by will be moved to an sub select where we don't have a group state, the `state` prevents this behavior |
||
747 | $query->addState(self::MANY_TO_MANY_LIMIT_QUERY); |
||
748 | |||
749 | $query = $this->criteriaQueryBuilder->build( |
||
750 | $query, |
||
751 | $association->getToManyReferenceDefinition(), |
||
752 | $fieldCriteria, |
||
753 | $context |
||
754 | ); |
||
755 | |||
756 | $localColumn = EntityDefinitionQueryHelper::escape($association->getMappingLocalColumn()); |
||
757 | $referenceColumn = EntityDefinitionQueryHelper::escape($association->getMappingReferenceColumn()); |
||
758 | |||
759 | $orderBy = ''; |
||
760 | $parts = $query->getQueryPart('orderBy'); |
||
761 | if (!empty($parts)) { |
||
762 | $orderBy = ' ORDER BY ' . implode(', ', $parts); |
||
763 | $query->resetQueryPart('orderBy'); |
||
764 | } |
||
765 | // order by is handled in group_concat |
||
766 | $fieldCriteria->resetSorting(); |
||
767 | |||
768 | $query->select([ |
||
769 | 'LOWER(HEX(' . $root . '.' . $localColumn . ')) as `key`', |
||
770 | 'GROUP_CONCAT(LOWER(HEX(' . $root . '.' . $referenceColumn . ')) ' . $orderBy . ') as `value`', |
||
771 | ]); |
||
772 | |||
773 | $query->addGroupBy($root . '.' . $localColumn); |
||
774 | |||
775 | if ($fieldCriteria->getLimit() !== null) { |
||
776 | $limitQuery = $this->buildManyToManyLimitQuery($association); |
||
777 | |||
778 | $params = [ |
||
779 | '#source_column#' => EntityDefinitionQueryHelper::escape($association->getMappingLocalColumn()), |
||
780 | '#reference_column#' => EntityDefinitionQueryHelper::escape($association->getMappingReferenceColumn()), |
||
781 | '#table#' => $root, |
||
782 | ]; |
||
783 | $query->innerJoin( |
||
784 | $root, |
||
785 | '(' . $limitQuery . ')', |
||
786 | 'counter_table', |
||
787 | str_replace( |
||
788 | array_keys($params), |
||
789 | array_values($params), |
||
790 | 'counter_table.#source_column# = #table#.#source_column# AND |
||
791 | counter_table.#reference_column# = #table#.#reference_column# AND |
||
792 | counter_table.id_count <= :limit' |
||
793 | ) |
||
794 | ); |
||
795 | $query->setParameter('limit', $fieldCriteria->getLimit()); |
||
796 | |||
797 | $this->connection->executeQuery('SET @n = 0; SET @c = null;'); |
||
798 | } |
||
799 | |||
800 | $mapping = $query->executeQuery()->fetchAllKeyValue(); |
||
801 | |||
802 | $ids = []; |
||
803 | foreach ($mapping as &$row) { |
||
804 | $row = \array_filter(explode(',', (string) $row)); |
||
805 | foreach ($row as $id) { |
||
806 | $ids[] = $id; |
||
807 | } |
||
808 | } |
||
809 | unset($row); |
||
810 | |||
811 | $fieldCriteria->setIds($ids); |
||
812 | |||
813 | $referenceClass = $association->getToManyReferenceDefinition(); |
||
814 | /** @var EntityCollection<Entity> $collectionClass */ |
||
815 | $collectionClass = $referenceClass->getCollectionClass(); |
||
816 | $data = $this->_read( |
||
817 | $fieldCriteria, |
||
818 | $referenceClass, |
||
819 | $context, |
||
820 | new $collectionClass(), |
||
821 | $referenceClass->getFields()->getBasicFields(), |
||
822 | false, |
||
823 | $partial |
||
824 | ); |
||
825 | |||
826 | /** @var Entity $struct */ |
||
827 | foreach ($collection as $struct) { |
||
828 | $structData = new $collectionClass(); |
||
829 | |||
830 | $id = $struct->getUniqueIdentifier(); |
||
831 | |||
832 | $parentId = $struct->has('parentId') ? $struct->get('parentId') : ''; |
||
833 | |||
834 | if (\array_key_exists($struct->getUniqueIdentifier(), $mapping)) { |
||
835 | // filter mapping list of whole data array |
||
836 | $structData = $data->getList($mapping[$id]); |
||
837 | |||
838 | // sort list by ids if the criteria contained a sorting |
||
839 | $structData->sortByIdArray($mapping[$id]); |
||
840 | } elseif (\array_key_exists($parentId, $mapping) && $association->is(Inherited::class) && $context->considerInheritance()) { |
||
841 | // filter mapping for the inherited parent association |
||
842 | $structData = $data->getList($mapping[$parentId]); |
||
843 | |||
844 | // sort list by ids if the criteria contained a sorting |
||
845 | $structData->sortByIdArray($mapping[$parentId]); |
||
846 | } |
||
847 | |||
848 | // if the association is added as extension (for plugins), we have to add the data as extension |
||
849 | if ($association->is(Extension::class)) { |
||
850 | $struct->addExtension($association->getPropertyName(), $structData); |
||
851 | } else { |
||
852 | $struct->assign([$association->getPropertyName() => $structData]); |
||
853 | } |
||
854 | } |
||
855 | } |
||
856 | |||
857 | /** |
||
858 | * @param EntityCollection<Entity> $collection |
||
859 | * |
||
860 | * @return array<string, string[]> |
||
861 | */ |
||
862 | private function fetchPaginatedOneToManyMapping( |
||
863 | EntityDefinition $definition, |
||
864 | OneToManyAssociationField $association, |
||
865 | Context $context, |
||
866 | EntityCollection $collection, |
||
867 | Criteria $fieldCriteria |
||
868 | ): array { |
||
869 | $sortings = $fieldCriteria->getSorting(); |
||
870 | |||
871 | // Remove first entry |
||
872 | array_shift($sortings); |
||
873 | |||
874 | // build query based on provided association criteria (sortings, search, filter) |
||
875 | $query = $this->criteriaQueryBuilder->build( |
||
876 | new QueryBuilder($this->connection), |
||
877 | $association->getReferenceDefinition(), |
||
878 | $fieldCriteria, |
||
879 | $context |
||
880 | ); |
||
881 | |||
882 | $foreignKey = $association->getReferenceField(); |
||
883 | |||
884 | if (!$association->getReferenceDefinition()->getField('id')) { |
||
885 | throw new \RuntimeException( |
||
886 | sprintf( |
||
887 | 'Paginated to many association must have an id field. No id field found for association %s.%s', |
||
888 | $definition->getEntityName(), |
||
889 | $association->getPropertyName() |
||
890 | ) |
||
891 | ); |
||
892 | } |
||
893 | |||
894 | // build sql accessor for foreign key field in reference table `customer_address.customer_id` |
||
895 | $sqlAccessor = EntityDefinitionQueryHelper::escape($association->getReferenceDefinition()->getEntityName()) . '.' |
||
896 | . EntityDefinitionQueryHelper::escape($foreignKey); |
||
897 | |||
898 | $query->select( |
||
899 | [ |
||
900 | // build select with an internal counter loop, the counter loop will be reset if the foreign key changed (this is the reason for the sorting inject above) |
||
901 | '@n:=IF(@c=' . $sqlAccessor . ', @n+1, IF(@c:=' . $sqlAccessor . ',1,1)) as id_count', |
||
902 | |||
903 | // add select for foreign key for join condition |
||
904 | $sqlAccessor, |
||
905 | |||
906 | // add primary key select to group concat them |
||
907 | EntityDefinitionQueryHelper::escape($association->getReferenceDefinition()->getEntityName()) . '.id', |
||
908 | ] |
||
909 | ); |
||
910 | |||
911 | foreach ($query->getQueryPart('orderBy') as $i => $sorting) { |
||
912 | // The first order is the primary key |
||
913 | if ($i === 0) { |
||
914 | continue; |
||
915 | } |
||
916 | --$i; |
||
917 | |||
918 | // Strip the ASC/DESC at the end of the sort |
||
919 | $query->addSelect(\sprintf('%s as sort_%d', substr((string) $sorting, 0, -4), $i)); |
||
920 | } |
||
921 | |||
922 | $root = EntityDefinitionQueryHelper::escape($definition->getEntityName()); |
||
923 | |||
924 | // create a wrapper query which select the root primary key and the grouped reference ids |
||
925 | $wrapper = $this->connection->createQueryBuilder(); |
||
926 | $wrapper->select( |
||
927 | [ |
||
928 | 'LOWER(HEX(' . $root . '.id)) as id', |
||
929 | 'LOWER(HEX(child.id)) as child_id', |
||
930 | ] |
||
931 | ); |
||
932 | |||
933 | foreach ($sortings as $i => $sorting) { |
||
934 | $wrapper->addOrderBy(sprintf('sort_%s', $i), $sorting->getDirection()); |
||
935 | } |
||
936 | |||
937 | $wrapper->from($root, $root); |
||
938 | |||
939 | // wrap query into a sub select to restrict the association count from the outer query |
||
940 | $wrapper->leftJoin( |
||
941 | $root, |
||
942 | '(' . $query->getSQL() . ')', |
||
943 | 'child', |
||
944 | 'child.' . $foreignKey . ' = ' . $root . '.id AND id_count >= :offset AND id_count <= :limit' |
||
945 | ); |
||
946 | |||
947 | // filter result to loaded root entities |
||
948 | $wrapper->andWhere($root . '.id IN (:rootIds)'); |
||
949 | |||
950 | $bytes = $collection->map( |
||
951 | fn (Entity $entity) => Uuid::fromHexToBytes($entity->getUniqueIdentifier()) |
||
952 | ); |
||
953 | |||
954 | if ($definition->isInheritanceAware() && $context->considerInheritance()) { |
||
955 | /** @var Entity $entity */ |
||
956 | foreach ($collection->getElements() as $entity) { |
||
957 | if ($entity->get('parentId')) { |
||
958 | $bytes[$entity->get('parentId')] = Uuid::fromHexToBytes($entity->get('parentId')); |
||
959 | } |
||
960 | } |
||
961 | } |
||
962 | |||
963 | $wrapper->setParameter('rootIds', $bytes, ArrayParameterType::STRING); |
||
964 | |||
965 | $limit = $fieldCriteria->getOffset() + $fieldCriteria->getLimit(); |
||
966 | $offset = $fieldCriteria->getOffset() + 1; |
||
967 | |||
968 | $wrapper->setParameter('limit', $limit); |
||
969 | $wrapper->setParameter('offset', $offset); |
||
970 | |||
971 | foreach ($query->getParameters() as $key => $value) { |
||
972 | $type = $query->getParameterType($key); |
||
973 | $wrapper->setParameter($key, $value, $type); |
||
974 | } |
||
975 | |||
976 | // initials the cursor and loop counter, pdo do not allow to execute SET and SELECT in one statement |
||
977 | $this->connection->executeQuery('SET @n = 0; SET @c = null;'); |
||
978 | |||
979 | $rows = $wrapper->executeQuery()->fetchAllAssociative(); |
||
980 | |||
981 | $grouped = []; |
||
982 | foreach ($rows as $row) { |
||
983 | $id = (string) $row['id']; |
||
984 | |||
985 | if (!isset($grouped[$id])) { |
||
986 | $grouped[$id] = []; |
||
987 | } |
||
988 | |||
989 | if (empty($row['child_id'])) { |
||
990 | continue; |
||
991 | } |
||
992 | |||
993 | $grouped[$id][] = (string) $row['child_id']; |
||
994 | } |
||
995 | |||
996 | return $grouped; |
||
997 | } |
||
998 | |||
999 | private function buildManyToManyLimitQuery(ManyToManyAssociationField $association): QueryBuilder |
||
1000 | { |
||
1001 | $table = EntityDefinitionQueryHelper::escape($association->getMappingDefinition()->getEntityName()); |
||
1002 | |||
1003 | $sourceColumn = EntityDefinitionQueryHelper::escape($association->getMappingLocalColumn()); |
||
1004 | $referenceColumn = EntityDefinitionQueryHelper::escape($association->getMappingReferenceColumn()); |
||
1005 | |||
1006 | $params = [ |
||
1007 | '#table#' => $table, |
||
1008 | '#source_column#' => $sourceColumn, |
||
1009 | ]; |
||
1010 | |||
1011 | $query = new QueryBuilder($this->connection); |
||
1012 | $query->select([ |
||
1013 | str_replace( |
||
1014 | array_keys($params), |
||
1015 | array_values($params), |
||
1016 | '@n:=IF(@c=#table#.#source_column#, @n+1, IF(@c:=#table#.#source_column#,1,1)) as id_count' |
||
1017 | ), |
||
1018 | $table . '.' . $referenceColumn, |
||
1019 | $table . '.' . $sourceColumn, |
||
1020 | ]); |
||
1021 | $query->from($table, $table); |
||
1022 | $query->orderBy($table . '.' . $sourceColumn); |
||
1023 | |||
1024 | return $query; |
||
1025 | } |
||
1026 | |||
1027 | private function buildOneToManyPropertyAccessor(EntityDefinition $definition, OneToManyAssociationField $association): string |
||
1028 | { |
||
1029 | $reference = $association->getReferenceDefinition(); |
||
1030 | |||
1031 | if ($association instanceof ChildrenAssociationField) { |
||
1032 | return $reference->getEntityName() . '.parentId'; |
||
1033 | } |
||
1034 | |||
1035 | $ref = $reference->getFields()->getByStorageName( |
||
1036 | $association->getReferenceField() |
||
1037 | ); |
||
1038 | |||
1039 | if (!$ref) { |
||
1040 | throw new \RuntimeException( |
||
1041 | sprintf( |
||
1042 | 'Reference field %s not found in definition %s for definition %s', |
||
1043 | $association->getReferenceField(), |
||
1044 | $reference->getEntityName(), |
||
1045 | $definition->getEntityName() |
||
1046 | ) |
||
1047 | ); |
||
1048 | } |
||
1049 | |||
1050 | return $reference->getEntityName() . '.' . $ref->getPropertyName(); |
||
1051 | } |
||
1052 | |||
1053 | private function isAssociationRestricted(?Criteria $criteria, string $accessor): bool |
||
1054 | { |
||
1055 | if ($criteria === null) { |
||
1056 | return false; |
||
1057 | } |
||
1058 | |||
1059 | if (!$criteria->hasAssociation($accessor)) { |
||
1060 | return false; |
||
1061 | } |
||
1062 | |||
1063 | $fieldCriteria = $criteria->getAssociation($accessor); |
||
1064 | |||
1065 | return $fieldCriteria->getOffset() !== null |
||
1066 | || $fieldCriteria->getLimit() !== null |
||
1067 | || !empty($fieldCriteria->getSorting()) |
||
1068 | || !empty($fieldCriteria->getFilters()) |
||
1069 | || !empty($fieldCriteria->getPostFilters()) |
||
1070 | ; |
||
1071 | } |
||
1072 | |||
1073 | private function addAssociationFieldsToCriteria( |
||
1074 | Criteria $criteria, |
||
1075 | EntityDefinition $definition, |
||
1076 | FieldCollection $fields |
||
1077 | ): FieldCollection { |
||
1078 | foreach ($criteria->getAssociations() as $fieldName => $_fieldCriteria) { |
||
1079 | $field = $definition->getFields()->get($fieldName); |
||
1080 | if (!$field) { |
||
1081 | $this->logger->warning( |
||
1082 | sprintf('Criteria association "%s" could not be resolved. Double check your Criteria!', $fieldName) |
||
1083 | ); |
||
1084 | |||
1085 | continue; |
||
1086 | } |
||
1087 | |||
1088 | $fields->add($field); |
||
1089 | } |
||
1090 | |||
1091 | return $fields; |
||
1092 | } |
||
1093 | |||
1094 | /** |
||
1095 | * @param EntityCollection<Entity> $collection |
||
1096 | * @param array<string, mixed> $partial |
||
1097 | */ |
||
1098 | private function loadToOne( |
||
1099 | AssociationField $association, |
||
1100 | Context $context, |
||
1101 | EntityCollection $collection, |
||
1102 | Criteria $criteria, |
||
1103 | array $partial |
||
1104 | ): void { |
||
1105 | if (!$association instanceof OneToOneAssociationField && !$association instanceof ManyToOneAssociationField) { |
||
1106 | return; |
||
1107 | } |
||
1108 | |||
1109 | if (!$criteria->hasAssociation($association->getPropertyName())) { |
||
1110 | return; |
||
1111 | } |
||
1112 | |||
1113 | $associationCriteria = $criteria->getAssociation($association->getPropertyName()); |
||
1114 | if (!$associationCriteria->getAssociations()) { |
||
1115 | return; |
||
1116 | } |
||
1117 | |||
1118 | if (!$associationCriteria->getTitle() && $criteria->getTitle()) { |
||
1119 | $associationCriteria->setTitle( |
||
1120 | $criteria->getTitle() . '::association::' . $association->getPropertyName() |
||
1121 | ); |
||
1122 | } |
||
1123 | |||
1124 | $related = \array_filter($collection->map(function (Entity $entity) use ($association) { |
||
1125 | if ($association->is(Extension::class)) { |
||
1126 | return $entity->getExtension($association->getPropertyName()); |
||
1127 | } |
||
1128 | |||
1129 | return $entity->get($association->getPropertyName()); |
||
1130 | })); |
||
1131 | |||
1132 | $referenceDefinition = $association->getReferenceDefinition(); |
||
1133 | $collectionClass = $referenceDefinition->getCollectionClass(); |
||
1134 | |||
1135 | if ($partial !== []) { |
||
1136 | $collectionClass = EntityCollection::class; |
||
1137 | } |
||
1138 | |||
1139 | $fields = $referenceDefinition->getFields()->getBasicFields(); |
||
1140 | $fields = $this->addAssociationFieldsToCriteria($associationCriteria, $referenceDefinition, $fields); |
||
1141 | |||
1142 | // This line removes duplicate entries, so after fetchAssociations the association must be reassigned |
||
1143 | $relatedCollection = new $collectionClass(); |
||
1144 | if (!$relatedCollection instanceof EntityCollection) { |
||
1145 | throw new \RuntimeException(sprintf('Collection class %s has to be an instance of EntityCollection', $collectionClass)); |
||
1146 | } |
||
1147 | |||
1148 | $relatedCollection->fill($related); |
||
1149 | |||
1150 | $this->fetchAssociations($associationCriteria, $referenceDefinition, $context, $relatedCollection, $fields, $partial); |
||
1151 | |||
1152 | foreach ($collection as $entity) { |
||
1153 | if ($association->is(Extension::class)) { |
||
1154 | $item = $entity->getExtension($association->getPropertyName()); |
||
1155 | } else { |
||
1156 | $item = $entity->get($association->getPropertyName()); |
||
1157 | } |
||
1158 | |||
1159 | if (!$item instanceof Entity) { |
||
1160 | continue; |
||
1161 | } |
||
1162 | |||
1163 | if ($association->is(Extension::class)) { |
||
1164 | $extension = $relatedCollection->get($item->getUniqueIdentifier()); |
||
1165 | if ($extension !== null) { |
||
1166 | $entity->addExtension($association->getPropertyName(), $extension); |
||
1167 | } |
||
1168 | |||
1169 | continue; |
||
1170 | } |
||
1171 | |||
1172 | $entity->assign([ |
||
1173 | $association->getPropertyName() => $relatedCollection->get($item->getUniqueIdentifier()), |
||
1174 | ]); |
||
1175 | } |
||
1176 | } |
||
1177 | |||
1178 | /** |
||
1179 | * @param EntityCollection<Entity> $collection |
||
1180 | * @param array<string, mixed> $partial |
||
1181 | * |
||
1182 | * @return EntityCollection<Entity> |
||
1183 | */ |
||
1184 | private function fetchAssociations( |
||
1223 | } |
||
1224 | } |
||
1225 |