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

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