vendor/shopware/core/Framework/DataAbstractionLayer/Dbal/CriteriaQueryBuilder.php line 91

  1. <?php declare(strict_types=1);
  2. namespace Shopware\Core\Framework\DataAbstractionLayer\Dbal;
  3. use Shopware\Core\Framework\Context;
  4. use Shopware\Core\Framework\DataAbstractionLayer\Dbal\Exception\InvalidSortingDirectionException;
  5. use Shopware\Core\Framework\DataAbstractionLayer\Dbal\FieldResolver\CriteriaPartResolver;
  6. use Shopware\Core\Framework\DataAbstractionLayer\EntityDefinition;
  7. use Shopware\Core\Framework\DataAbstractionLayer\Field\StorageAware;
  8. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  9. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\AndFilter;
  10. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\Filter;
  11. use Shopware\Core\Framework\DataAbstractionLayer\Search\Parser\SqlQueryParser;
  12. use Shopware\Core\Framework\DataAbstractionLayer\Search\Query\ScoreQuery;
  13. use Shopware\Core\Framework\DataAbstractionLayer\Search\Sorting\CountSorting;
  14. use Shopware\Core\Framework\DataAbstractionLayer\Search\Sorting\FieldSorting;
  15. use Shopware\Core\Framework\DataAbstractionLayer\Search\Term\EntityScoreQueryBuilder;
  16. use Shopware\Core\Framework\DataAbstractionLayer\Search\Term\SearchTermInterpreter;
  17. use Shopware\Core\Framework\Log\Package;
  18. /**
  19.  * @internal
  20.  */
  21. #[Package('core')]
  22. class CriteriaQueryBuilder
  23. {
  24.     public function __construct(
  25.         private readonly SqlQueryParser $parser,
  26.         /***
  27.          * @var EntityDefinitionQueryHelper
  28.          */
  29.         private readonly EntityDefinitionQueryHelper $helper,
  30.         private readonly SearchTermInterpreter $interpreter,
  31.         private readonly EntityScoreQueryBuilder $scoreBuilder,
  32.         private readonly JoinGroupBuilder $joinGrouper,
  33.         private readonly CriteriaPartResolver $criteriaPartResolver
  34.     ) {
  35.     }
  36.     public function build(QueryBuilder $queryEntityDefinition $definitionCriteria $criteriaContext $context, array $paths = []): QueryBuilder
  37.     {
  38.         $query $this->helper->getBaseQuery($query$definition$context);
  39.         if ($definition->isInheritanceAware() && $context->considerInheritance()) {
  40.             $parent $definition->getFields()->get('parent');
  41.             if ($parent) {
  42.                 $this->helper->resolveField($parent$definition$definition->getEntityName(), $query$context);
  43.             }
  44.         }
  45.         if ($criteria->getTerm()) {
  46.             $pattern $this->interpreter->interpret((string) $criteria->getTerm());
  47.             $queries $this->scoreBuilder->buildScoreQueries($pattern$definition$definition->getEntityName(), $context);
  48.             $criteria->addQuery(...$queries);
  49.         }
  50.         $filters $this->groupFilters($definition$criteria$paths);
  51.         $this->criteriaPartResolver->resolve($filters$definition$query$context);
  52.         $this->criteriaPartResolver->resolve($criteria->getQueries(), $definition$query$context);
  53.         $this->criteriaPartResolver->resolve($criteria->getSorting(), $definition$query$context);
  54.         // do not use grouped filters, because the grouped filters are mapped flat and the logical OR/AND are removed
  55.         $filter = new AndFilter(array_merge(
  56.             $criteria->getFilters(),
  57.             $criteria->getPostFilters()
  58.         ));
  59.         $this->addFilter($definition$filter$query$context);
  60.         $this->addQueries($definition$criteria$query$context);
  61.         if ($criteria->getLimit() === 1) {
  62.             $query->removeState(EntityDefinitionQueryHelper::HAS_TO_MANY_JOIN);
  63.         }
  64.         $this->addSortings($definition$criteria$criteria->getSorting(), $query$context);
  65.         return $query;
  66.     }
  67.     public function addFilter(EntityDefinition $definition, ?Filter $filterQueryBuilder $queryContext $context): void
  68.     {
  69.         if (!$filter) {
  70.             return;
  71.         }
  72.         $parsed $this->parser->parse($filter$definition$context);
  73.         if (empty($parsed->getWheres())) {
  74.             return;
  75.         }
  76.         $query->andWhere(implode(' AND '$parsed->getWheres()));
  77.         foreach ($parsed->getParameters() as $key => $value) {
  78.             $query->setParameter($key$value$parsed->getType($key));
  79.         }
  80.     }
  81.     public function addSortings(EntityDefinition $definitionCriteria $criteria, array $sortingsQueryBuilder $queryContext $context): void
  82.     {
  83.         /** @var FieldSorting $sorting */
  84.         foreach ($sortings as $sorting) {
  85.             $this->validateSortingDirection($sorting->getDirection());
  86.             if ($sorting->getField() === '_score') {
  87.                 if (!$this->hasQueriesOrTerm($criteria)) {
  88.                     continue;
  89.                 }
  90.                 // Only add manual _score sorting if the query contains a _score calculation and selection (i.e. the
  91.                 // criteria has a term or queries). Otherwise the SQL selection would fail because no _score field
  92.                 // exists in any entity.
  93.                 $query->addOrderBy('_score'$sorting->getDirection());
  94.                 $query->addState('_score');
  95.                 continue;
  96.             }
  97.             $accessor $this->helper->getFieldAccessor($sorting->getField(), $definition$definition->getEntityName(), $context);
  98.             if ($sorting instanceof CountSorting) {
  99.                 $query->addOrderBy(sprintf('COUNT(%s)'$accessor), $sorting->getDirection());
  100.                 continue;
  101.             }
  102.             if ($sorting->getNaturalSorting()) {
  103.                 $query->addOrderBy('LENGTH(' $accessor ')'$sorting->getDirection());
  104.             }
  105.             if (!$this->hasGroupBy($criteria$query)) {
  106.                 $query->addOrderBy($accessor$sorting->getDirection());
  107.                 continue;
  108.             }
  109.             if (!\in_array($sorting->getField(), ['product.cheapestPrice''cheapestPrice'], true)) {
  110.                 if ($sorting->getDirection() === FieldSorting::ASCENDING) {
  111.                     $accessor 'MIN(' $accessor ')';
  112.                 } else {
  113.                     $accessor 'MAX(' $accessor ')';
  114.                 }
  115.             }
  116.             $query->addOrderBy($accessor$sorting->getDirection());
  117.         }
  118.     }
  119.     private function addQueries(EntityDefinition $definitionCriteria $criteriaQueryBuilder $queryContext $context): void
  120.     {
  121.         $queries $this->parser->parseRanking(
  122.             $criteria->getQueries(),
  123.             $definition,
  124.             $definition->getEntityName(),
  125.             $context
  126.         );
  127.         if (empty($queries->getWheres())) {
  128.             return;
  129.         }
  130.         $query->addState(EntityDefinitionQueryHelper::HAS_TO_MANY_JOIN);
  131.         $primary $definition->getPrimaryKeys()->first();
  132.         \assert($primary instanceof StorageAware);
  133.         $select 'SUM(' implode(' + '$queries->getWheres()) . ') / ' \sprintf('COUNT(%s.%s)'$definition->getEntityName(), $primary->getStorageName());
  134.         $query->addSelect($select ' as _score');
  135.         // Sort by _score primarily if the criteria has a score query or search term
  136.         if (!$this->hasScoreSorting($criteria)) {
  137.             $criteria->addSorting(new FieldSorting('_score'FieldSorting::DESCENDING));
  138.         }
  139.         $minScore array_map(fn (ScoreQuery $query) => $query->getScore(), $criteria->getQueries());
  140.         $minScore min($minScore);
  141.         $query->andHaving('_score >= :_minScore');
  142.         $query->setParameter('_minScore'$minScore);
  143.         $query->addState('_score');
  144.         foreach ($queries->getParameters() as $key => $value) {
  145.             $query->setParameter($key$value$queries->getType($key));
  146.         }
  147.     }
  148.     private function hasGroupBy(Criteria $criteriaQueryBuilder $query): bool
  149.     {
  150.         if ($query->hasState(EntityReader::MANY_TO_MANY_LIMIT_QUERY)) {
  151.             return false;
  152.         }
  153.         return $query->hasState(EntityDefinitionQueryHelper::HAS_TO_MANY_JOIN) || !empty($criteria->getGroupFields());
  154.     }
  155.     private function groupFilters(EntityDefinition $definitionCriteria $criteria, array $additionalFields = []): array
  156.     {
  157.         $filters = [];
  158.         foreach ($criteria->getFilters() as $filter) {
  159.             $filters[] = new AndFilter([$filter]);
  160.         }
  161.         foreach ($criteria->getPostFilters() as $filter) {
  162.             $filters[] = new AndFilter([$filter]);
  163.         }
  164.         // $additionalFields is used by the entity aggregator.
  165.         // For example, if an aggregation is to be created on a to many association that is already stored as a filter.
  166.         // The association is therefore referenced twice in the query and would have to be created as a sub-join in each case. But since only the filters are considered, the association is referenced only once.
  167.         return $this->joinGrouper->group($filters$definition$additionalFields);
  168.     }
  169.     private function hasScoreSorting(Criteria $criteria): bool
  170.     {
  171.         foreach ($criteria->getSorting() as $sorting) {
  172.             if ($sorting->getField() === '_score') {
  173.                 return true;
  174.             }
  175.         }
  176.         return false;
  177.     }
  178.     private function hasQueriesOrTerm(Criteria $criteria): bool
  179.     {
  180.         return !empty($criteria->getQueries()) || $criteria->getTerm();
  181.     }
  182.     /**
  183.      * @throws InvalidSortingDirectionException
  184.      */
  185.     private function validateSortingDirection(string $direction): void
  186.     {
  187.         if (!\in_array(mb_strtoupper($direction), [FieldSorting::ASCENDINGFieldSorting::DESCENDING], true)) {
  188.             throw new InvalidSortingDirectionException($direction);
  189.         }
  190.     }
  191. }