vendor/shopware/core/Framework/DataAbstractionLayer/Dbal/EntityReader.php line 498

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