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

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