vendor/shopware/platform/src/Core/Framework/DataAbstractionLayer/Dbal/EntityReader.php line 415

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