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

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