vendor/shopware/core/Framework/DataAbstractionLayer/Dbal/CriteriaQueryHelper.php line 138

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\Dbal\Exception\InvalidSortingDirectionException;
  6. use Shopware\Core\Framework\DataAbstractionLayer\EntityDefinition;
  7. use Shopware\Core\Framework\DataAbstractionLayer\Field\AssociationField;
  8. use Shopware\Core\Framework\DataAbstractionLayer\Field\FkField;
  9. use Shopware\Core\Framework\DataAbstractionLayer\Field\IdField;
  10. use Shopware\Core\Framework\DataAbstractionLayer\Field\StorageAware;
  11. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  12. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\AntiJoinFilter;
  13. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
  14. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\Filter;
  15. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\MultiFilter;
  16. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\NotFilter;
  17. use Shopware\Core\Framework\DataAbstractionLayer\Search\Parser\SqlQueryParser;
  18. use Shopware\Core\Framework\DataAbstractionLayer\Search\Query\ScoreQuery;
  19. use Shopware\Core\Framework\DataAbstractionLayer\Search\Sorting\FieldSorting;
  20. use Shopware\Core\Framework\DataAbstractionLayer\Search\Term\EntityScoreQueryBuilder;
  21. use Shopware\Core\Framework\DataAbstractionLayer\Search\Term\SearchTermInterpreter;
  22. use Shopware\Core\Framework\Uuid\Uuid;
  23. trait CriteriaQueryHelper
  24. {
  25.     abstract protected function getParser(): SqlQueryParser;
  26.     abstract protected function getDefinitionHelper(): EntityDefinitionQueryHelper;
  27.     abstract protected function getInterpreter(): SearchTermInterpreter;
  28.     abstract protected function getScoreBuilder(): EntityScoreQueryBuilder;
  29.     protected function buildQueryByCriteria(QueryBuilder $queryEntityDefinition $definitionCriteria $criteriaContext $context): QueryBuilder
  30.     {
  31.         $table $definition->getEntityName();
  32.         $query $this->getDefinitionHelper()->getBaseQuery($query$definition$context);
  33.         if ($definition->isInheritanceAware() && $context->considerInheritance()) {
  34.             $parent $definition->getFields()->get('parent');
  35.             $this->getDefinitionHelper()->resolveField($parent$definition$definition->getEntityName(), $query$context);
  36.         }
  37.         if ($criteria->getTerm()) {
  38.             $pattern $this->getInterpreter()->interpret($criteria->getTerm());
  39.             $queries $this->getScoreBuilder()->buildScoreQueries($pattern$definition$definition->getEntityName(), $context);
  40.             $criteria->addQuery(...$queries);
  41.         }
  42.         $filter $this->antiJoinTransform(
  43.             $definition,
  44.             new MultiFilter(
  45.                 'AND',
  46.                 array_merge(
  47.                     $criteria->getFilters(),
  48.                     $criteria->getPostFilters()
  49.                 )
  50.             )
  51.         );
  52.         $criteria->resetFilters();
  53.         if ($filter) {
  54.             $criteria->addFilter($filter);
  55.         }
  56.         $fields $this->getFieldsByCriteria($criteria);
  57.         //join association and translated fields
  58.         foreach ($fields as $fieldName) {
  59.             if ($fieldName === '_score') {
  60.                 continue;
  61.             }
  62.             $this->getDefinitionHelper()->resolveAccessor($fieldName$definition$table$query$context);
  63.         }
  64.         $antiJoins $this->groupAntiJoinConditions($query$filter$definition$context);
  65.         // handle anti-join
  66.         foreach ($antiJoins as $fieldName => $antiJoinConditions) {
  67.             if ($fieldName === '_score') {
  68.                 continue;
  69.             }
  70.             $this->getDefinitionHelper()->resolveAntiJoinAccessors($fieldName$definition$table$query$context$antiJoinConditions);
  71.         }
  72.         $this->addFilter($definition$filter$query$context);
  73.         $this->addQueries($definition$criteria$query$context);
  74.         $this->addSortings($definition$criteria->getSorting(), $query$context);
  75.         return $query;
  76.     }
  77.     protected function addIdCondition(Criteria $criteriaEntityDefinition $definitionQueryBuilder $query): void
  78.     {
  79.         $primaryKeys $criteria->getIds();
  80.         $primaryKeys array_values($primaryKeys);
  81.         if (empty($primaryKeys)) {
  82.             return;
  83.         }
  84.         if (!\is_array($primaryKeys[0]) || \count($primaryKeys[0]) === 1) {
  85.             $primaryKeyField $definition->getPrimaryKeys()->first();
  86.             if ($primaryKeyField instanceof IdField) {
  87.                 $primaryKeys array_map(function ($id) {
  88.                     if (is_array($id)) {
  89.                         return Uuid::fromHexToBytes($id[0]);
  90.                     }
  91.                     return Uuid::fromHexToBytes($id);
  92.                 }, $primaryKeys);
  93.             }
  94.             if (!$primaryKeyField instanceof StorageAware) {
  95.                 throw new \RuntimeException('Primary key fields has to be an instance of StorageAware');
  96.             }
  97.             $query->andWhere(sprintf(
  98.                 '%s.%s IN (:ids)',
  99.                 EntityDefinitionQueryHelper::escape($definition->getEntityName()),
  100.                 EntityDefinitionQueryHelper::escape($primaryKeyField->getStorageName())
  101.             ));
  102.             $query->setParameter('ids'array_values($primaryKeys), Connection::PARAM_STR_ARRAY);
  103.             return;
  104.         }
  105.         $this->addIdConditionWithOr($criteria$definition$query);
  106.     }
  107.     protected function addFilter(EntityDefinition $definition, ?Filter $filterQueryBuilder $queryContext $context): void
  108.     {
  109.         if (!$filter) {
  110.             return;
  111.         }
  112.         $parsed $this->getParser()->parse($filter$definition$context);
  113.         if (empty($parsed->getWheres())) {
  114.             return;
  115.         }
  116.         $query->andWhere(implode(' AND '$parsed->getWheres()));
  117.         foreach ($parsed->getParameters() as $key => $value) {
  118.             $query->setParameter($key$value$parsed->getType($key));
  119.         }
  120.     }
  121.     private function addIdConditionWithOr(Criteria $criteriaEntityDefinition $definitionQueryBuilder $query): void
  122.     {
  123.         $wheres = [];
  124.         foreach ($criteria->getIds() as $primaryKey) {
  125.             if (!is_array($primaryKey)) {
  126.                 $primaryKey = ['id' => $primaryKey];
  127.             }
  128.             $where = [];
  129.             foreach ($primaryKey as $storageName => $value) {
  130.                 $field $definition->getFields()->getByStorageName($storageName);
  131.                 if ($field instanceof IdField || $field instanceof FkField) {
  132.                     $value Uuid::fromHexToBytes($value);
  133.                 }
  134.                 $key 'pk' Uuid::randomHex();
  135.                 $accessor EntityDefinitionQueryHelper::escape($definition->getEntityName()) . '.' EntityDefinitionQueryHelper::escape($storageName);
  136.                 $where[] = $accessor ' = :' $key;
  137.                 $query->setParameter($key$value);
  138.             }
  139.             $wheres[] = '(' implode(' AND '$where) . ')';
  140.         }
  141.         $wheres implode(' OR '$wheres);
  142.         $query->andWhere($wheres);
  143.     }
  144.     private function addQueries(EntityDefinition $definitionCriteria $criteriaQueryBuilder $queryContext $context): void
  145.     {
  146.         $queries $this->getParser()->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.         $select 'SUM(' implode(' + '$queries->getWheres()) . ')';
  157.         $query->addSelect($select ' as _score');
  158.         if (empty($criteria->getSorting())) {
  159.             $query->addOrderBy('_score''DESC');
  160.         }
  161.         $minScore array_map(function (ScoreQuery $query) {
  162.             return $query->getScore();
  163.         }, $criteria->getQueries());
  164.         $minScore min($minScore);
  165.         $query->andHaving('_score >= :_minScore');
  166.         $query->setParameter('_minScore'$minScore);
  167.         $query->addState('_score');
  168.         foreach ($queries->getParameters() as $key => $value) {
  169.             $query->setParameter($key$value$queries->getType($key));
  170.         }
  171.     }
  172.     private function addSortings(EntityDefinition $definition, array $sortingsQueryBuilder $queryContext $context): void
  173.     {
  174.         foreach ($sortings as $sorting) {
  175.             $this->validateSortingDirection($sorting->getDirection());
  176.             if ($sorting->getField() === '_score') {
  177.                 $query->addOrderBy('_score'$sorting->getDirection());
  178.                 $query->addState('_score');
  179.                 continue;
  180.             }
  181.             $accessor $this->getDefinitionHelper()->getFieldAccessor($sorting->getField(), $definition$definition->getEntityName(), $context);
  182.             if ($sorting->getNaturalSorting()) {
  183.                 $query->addOrderBy('LENGTH(' $accessor ')'$sorting->getDirection());
  184.             }
  185.             $query->addOrderBy($accessor$sorting->getDirection());
  186.         }
  187.     }
  188.     /**
  189.      * @return string[]
  190.      */
  191.     private function getFieldsByCriteria(Criteria $criteria): array
  192.     {
  193.         $fields = [];
  194.         foreach ($criteria->getSorting() as $field) {
  195.             $fields[] = $field->getFields();
  196.         }
  197.         foreach ($criteria->getFilters() as $field) {
  198.             $fields[] = $field->getFields();
  199.         }
  200.         foreach ($criteria->getPostFilters() as $field) {
  201.             $fields[] = $field->getFields();
  202.         }
  203.         foreach ($criteria->getQueries() as $field) {
  204.             $fields[] = $field->getFields();
  205.         }
  206.         if (count($fields) === 0) {
  207.             return [];
  208.         }
  209.         return array_unique(array_merge(...$fields));
  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.     /**
  221.      * Groups the anti joins by field name and anti join identifier
  222.      */
  223.     private function groupAntiJoinConditions(QueryBuilder $queryBuilder, ?Filter $filterEntityDefinition $definitionContext $context): array
  224.     {
  225.         if (!$filter) {
  226.             return [];
  227.         }
  228.         $antiJoins = [];
  229.         $this->walkBottomUp($filter, static function (Filter $f) use (&$antiJoins): void {
  230.             if ($f instanceof AntiJoinFilter) {
  231.                 $antiJoins[] = $f;
  232.             }
  233.         });
  234.         $result = [];
  235.         /** @var AntiJoinFilter $antiJoin */
  236.         foreach ($antiJoins as $antiJoin) {
  237.             $groupedFilter = [];
  238.             /** @var Filter $f */
  239.             foreach ($antiJoin->getQueries() as $f) {
  240.                 $field = @current($f->getFields());
  241.                 if (!isset($groupedFilter[$field])) {
  242.                     $groupedFilter[$field] = [];
  243.                 }
  244.                 $groupedFilter[$field][] = $f;
  245.             }
  246.             foreach ($groupedFilter as $fieldName => $group) {
  247.                 $multiFilter = new MultiFilter($antiJoin->getOperator(), $group);
  248.                 $parseResult $this->getParser()->parse($multiFilter$definition$context);
  249.                 foreach ($parseResult->getParameters() as $key => $value) {
  250.                     $queryBuilder->setParameter($key$value$parseResult->getType($key));
  251.                 }
  252.                 if (!isset($result[$fieldName])) {
  253.                     $result[$fieldName] = [];
  254.                 }
  255.                 $result[$fieldName][$antiJoin->getIdentifier()] = implode(' AND '$parseResult->getWheres());
  256.             }
  257.         }
  258.         return $result;
  259.     }
  260.     /**
  261.      * Transforms NotFilter on associations into anti-joins
  262.      *
  263.      * Base case:
  264.      *
  265.      * NotFilter($op, [EqualsFilter, ContainsFilter])
  266.      *   -->
  267.      * AntiJoin($op, [EqualsFilter, ContainsFilter])
  268.      *
  269.      *
  270.      * Mixed case:
  271.      *
  272.      * NotFilter($op, [EqualsFilter, ContainsFilter, Node, Node])
  273.      *   -->
  274.      * MultiFilter(AND,
  275.      *   AntiJoin($op, [ClosedTermOnAssociation, ClosedTermOnAssociation])
  276.      *   NotFilter($op, [Node, Node])
  277.      * )
  278.      */
  279.     private function antiJoinTransform(EntityDefinition $definitionFilter $filter): ?Filter
  280.     {
  281.         return $this->mapBottomUp($filter, function (Filter $notFilter) use ($definition) {
  282.             if (!$notFilter instanceof NotFilter) {
  283.                 return $notFilter;
  284.             }
  285.             $op $notFilter->getOperator();
  286.             $normalFilters = [];
  287.             $antiJoinFilters = [];
  288.             /** @var Filter $childFilter */
  289.             foreach ($notFilter->getQueries() as $childFilter) {
  290.                 $fields $childFilter->getFields();
  291.                 $field = @current($fields);
  292.                 if ($childFilter instanceof MultiFilter
  293.                     || count($fields) !== 1
  294.                     || ($childFilter instanceof EqualsFilter && $childFilter->getValue() === null)
  295.                     || !$this->isAssociationPath($definition$field)
  296.                 ) {
  297.                     $normalFilters[] = $childFilter;
  298.                     continue;
  299.                 }
  300.                 $antiJoinFilters[] = $childFilter;
  301.             }
  302.             if (empty($antiJoinFilters)) {
  303.                 return $notFilter;
  304.             }
  305.             if (empty($normalFilters)) {
  306.                 return new AntiJoinFilter($op$antiJoinFilters);
  307.             }
  308.             return new MultiFilter(
  309.                 $op,
  310.                 [
  311.                     new NotFilter($op$normalFilters),
  312.                     new AntiJoinFilter($op$antiJoinFilters),
  313.                 ]
  314.             );
  315.         });
  316.     }
  317.     private function isAssociationPath(EntityDefinition $definitionstring $fieldName): bool
  318.     {
  319.         $fieldName str_replace('extensions.'''$fieldName);
  320.         $prefix $definition->getEntityName() . '.';
  321.         if (mb_strpos($fieldName$prefix) === 0) {
  322.             $fieldName mb_substr($fieldName, \mb_strlen($prefix));
  323.         }
  324.         $fields $definition->getFields();
  325.         if (!$fields->has($fieldName)) {
  326.             $associationKey explode('.'$fieldName);
  327.             $fieldName array_shift($associationKey);
  328.         }
  329.         $field $fields->get($fieldName);
  330.         return $field instanceof AssociationField;
  331.     }
  332.     /**
  333.      * Transforms the filter tree with $mapFunction, starting from the leaf filter
  334.      *
  335.      * This can be used to rewrite a filter tree.
  336.      */
  337.     private function mapBottomUp(Filter $filter, \Closure $mapFunction): ?Filter
  338.     {
  339.         if ($filter instanceof MultiFilter) {
  340.             $mapped array_map(function ($f) use ($mapFunction) {
  341.                 return $this->mapBottomUp($f$mapFunction);
  342.             }, $filter->getQueries());
  343.             $filtered array_filter($mapped);
  344.             if (empty($filtered)) {
  345.                 return null;
  346.             }
  347.             $op $filter->getOperator();
  348.             if ($filter instanceof NotFilter) {
  349.                 $filter = new NotFilter($op$filtered);
  350.             } elseif ($filter instanceof AntiJoinFilter) {
  351.                 $filter = new AntiJoinFilter($op$filtered$filter->getIdentifier());
  352.             } else {
  353.                 $filter = new MultiFilter($op$filtered);
  354.             }
  355.         }
  356.         return $mapFunction($filter);
  357.     }
  358.     /**
  359.      * Calls $callback for every filter in the filter tree, starting with the leafs
  360.      */
  361.     private function walkBottomUp(Filter $filter, \Closure $callback): void
  362.     {
  363.         $this->mapBottomUp($filter, static function (Filter $f) use ($callback) {
  364.             $callback($f);
  365.             return $f;
  366.         });
  367.     }
  368. }