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

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