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

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