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

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