src/Core/Framework/DataAbstractionLayer/Dbal/EntityAggregator.php line 79

  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\DefinitionInstanceRegistry;
  6. use Shopware\Core\Framework\DataAbstractionLayer\EntityCollection;
  7. use Shopware\Core\Framework\DataAbstractionLayer\EntityDefinition;
  8. use Shopware\Core\Framework\DataAbstractionLayer\Exception\InvalidAggregationQueryException;
  9. use Shopware\Core\Framework\DataAbstractionLayer\Field\AssociationField;
  10. use Shopware\Core\Framework\DataAbstractionLayer\Field\Field;
  11. use Shopware\Core\Framework\DataAbstractionLayer\Field\FkField;
  12. use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\PrimaryKey;
  13. use Shopware\Core\Framework\DataAbstractionLayer\Field\FloatField;
  14. use Shopware\Core\Framework\DataAbstractionLayer\Field\IdField;
  15. use Shopware\Core\Framework\DataAbstractionLayer\Field\IntField;
  16. use Shopware\Core\Framework\DataAbstractionLayer\Field\ManyToManyAssociationField;
  17. use Shopware\Core\Framework\DataAbstractionLayer\Field\OneToManyAssociationField;
  18. use Shopware\Core\Framework\DataAbstractionLayer\Field\PriceField;
  19. use Shopware\Core\Framework\DataAbstractionLayer\Field\StorageAware;
  20. use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Aggregation;
  21. use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Bucket\BucketAggregation;
  22. use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Bucket\DateHistogramAggregation;
  23. use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Bucket\FilterAggregation;
  24. use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Bucket\TermsAggregation;
  25. use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Metric\AvgAggregation;
  26. use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Metric\CountAggregation;
  27. use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Metric\EntityAggregation;
  28. use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Metric\MaxAggregation;
  29. use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Metric\MinAggregation;
  30. use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Metric\RangeAggregation;
  31. use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Metric\StatsAggregation;
  32. use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Metric\SumAggregation;
  33. use Shopware\Core\Framework\DataAbstractionLayer\Search\AggregationResult\AggregationResult;
  34. use Shopware\Core\Framework\DataAbstractionLayer\Search\AggregationResult\AggregationResultCollection;
  35. use Shopware\Core\Framework\DataAbstractionLayer\Search\AggregationResult\Bucket\Bucket;
  36. use Shopware\Core\Framework\DataAbstractionLayer\Search\AggregationResult\Bucket\DateHistogramResult;
  37. use Shopware\Core\Framework\DataAbstractionLayer\Search\AggregationResult\Bucket\TermsResult;
  38. use Shopware\Core\Framework\DataAbstractionLayer\Search\AggregationResult\Metric\AvgResult;
  39. use Shopware\Core\Framework\DataAbstractionLayer\Search\AggregationResult\Metric\CountResult;
  40. use Shopware\Core\Framework\DataAbstractionLayer\Search\AggregationResult\Metric\EntityResult;
  41. use Shopware\Core\Framework\DataAbstractionLayer\Search\AggregationResult\Metric\MaxResult;
  42. use Shopware\Core\Framework\DataAbstractionLayer\Search\AggregationResult\Metric\MinResult;
  43. use Shopware\Core\Framework\DataAbstractionLayer\Search\AggregationResult\Metric\RangeResult;
  44. use Shopware\Core\Framework\DataAbstractionLayer\Search\AggregationResult\Metric\StatsResult;
  45. use Shopware\Core\Framework\DataAbstractionLayer\Search\AggregationResult\Metric\SumResult;
  46. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  47. use Shopware\Core\Framework\DataAbstractionLayer\Search\EntityAggregatorInterface;
  48. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\MultiFilter;
  49. use Shopware\Core\Framework\DataAbstractionLayer\Search\Sorting\FieldSorting;
  50. use Shopware\Core\Framework\DataAbstractionLayer\Search\Term\EntityScoreQueryBuilder;
  51. use Shopware\Core\Framework\DataAbstractionLayer\Search\Term\SearchTermInterpreter;
  52. use Shopware\Core\Framework\Log\Package;
  53. /**
  54.  * Allows to execute aggregated queries for all entities in the system
  55.  *
  56.  * @internal
  57.  */
  58. #[Package('core')]
  59. class EntityAggregator implements EntityAggregatorInterface
  60. {
  61.     public function __construct(
  62.         private readonly Connection $connection,
  63.         private readonly EntityDefinitionQueryHelper $queryHelper,
  64.         private readonly DefinitionInstanceRegistry $registry,
  65.         private readonly CriteriaQueryBuilder $criteriaQueryBuilder,
  66.         private readonly bool $timeZoneSupportEnabled,
  67.         private readonly SearchTermInterpreter $interpreter,
  68.         private readonly EntityScoreQueryBuilder $scoreBuilder
  69.     ) {
  70.     }
  71.     public function aggregate(EntityDefinition $definitionCriteria $criteriaContext $context): AggregationResultCollection
  72.     {
  73.         $aggregations = new AggregationResultCollection();
  74.         foreach ($criteria->getAggregations() as $aggregation) {
  75.             $result $this->fetchAggregation($aggregation$definition$criteria$context);
  76.             $aggregations->add($result);
  77.         }
  78.         return $aggregations;
  79.     }
  80.     public static function formatDate(string $interval\DateTime $date): string
  81.     {
  82.         switch ($interval) {
  83.             case DateHistogramAggregation::PER_MINUTE:
  84.                 return $date->format('Y-m-d H:i:00');
  85.             case DateHistogramAggregation::PER_HOUR:
  86.                 return $date->format('Y-m-d H:00:00');
  87.             case DateHistogramAggregation::PER_DAY:
  88.                 return $date->format('Y-m-d 00:00:00');
  89.             case DateHistogramAggregation::PER_WEEK:
  90.                 return $date->format('Y W');
  91.             case DateHistogramAggregation::PER_MONTH:
  92.                 return $date->format('Y-m-01 00:00:00');
  93.             case DateHistogramAggregation::PER_QUARTER:
  94.                 $month = (int) $date->format('m');
  95.                 return $date->format('Y') . ' ' ceil($month 3);
  96.             case DateHistogramAggregation::PER_YEAR:
  97.                 return $date->format('Y-01-01 00:00:00');
  98.             default:
  99.                 throw new \RuntimeException('Provided date format is not supported');
  100.         }
  101.     }
  102.     private function fetchAggregation(Aggregation $aggregationEntityDefinition $definitionCriteria $criteriaContext $context): AggregationResult
  103.     {
  104.         $clone = clone $criteria;
  105.         $clone->resetAggregations();
  106.         $clone->resetSorting();
  107.         $clone->resetPostFilters();
  108.         $clone->resetGroupFields();
  109.         // Early resolve terms to extract score queries
  110.         if ($clone->getTerm()) {
  111.             $pattern $this->interpreter->interpret((string) $criteria->getTerm());
  112.             $queries $this->scoreBuilder->buildScoreQueries($pattern$definition$definition->getEntityName(), $context);
  113.             $clone->addQuery(...$queries);
  114.             $clone->setTerm(null);
  115.         }
  116.         $scoreCritera = clone $clone;
  117.         $clone->resetQueries();
  118.         $query = new QueryBuilder($this->connection);
  119.         // If an aggregation is to be created on a to many association that is already stored as a filter.
  120.         // 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.
  121.         // In this case we add the aggregation field as path to the criteria builder and the join group builder will consider this path for the sub-join logic
  122.         $paths array_filter([$this->findToManyPath($aggregation$definition)]);
  123.         $query $this->criteriaQueryBuilder->build($query$definition$clone$context$paths);
  124.         $query->resetQueryPart('orderBy');
  125.         if ($criteria->getTitle()) {
  126.             $query->setTitle($criteria->getTitle() . '::aggregation::' $aggregation->getName());
  127.         }
  128.         $this->queryHelper->addIdCondition($criteria$definition$query);
  129.         $table $definition->getEntityName();
  130.         if (\count($scoreCritera->getQueries()) > 0) {
  131.             $escapedTable EntityDefinitionQueryHelper::escape($table);
  132.             $scoreQuery = new QueryBuilder($this->connection);
  133.             $scoreQuery $this->criteriaQueryBuilder->build($scoreQuery$definition$scoreCritera$context$paths);
  134.             $pks $definition->getFields()->filterByFlag(PrimaryKey::class)->map(fn (StorageAware $f) => $f->getStorageName());
  135.             $join '';
  136.             foreach ($pks as $pk) {
  137.                 $scoreQuery->addGroupBy($pk);
  138.                 $pk EntityDefinitionQueryHelper::escape($pk);
  139.                 $scoreQuery->addSelect($escapedTable '.' $pk);
  140.                 $join .= \sprintf('score_table.%s = %s.%s AND '$pk$escapedTable$pk);
  141.             }
  142.             // Remove remaining AND
  143.             $join substr($join0, -4);
  144.             foreach ($scoreQuery->getParameters() as $key => $value) {
  145.                 $query->setParameter($key$value$scoreQuery->getParameterType($key));
  146.             }
  147.             $query->join(
  148.                 EntityDefinitionQueryHelper::escape($table),
  149.                 '(' $scoreQuery->getSQL() . ')',
  150.                 'score_table',
  151.                 $join
  152.             );
  153.         }
  154.         foreach ($aggregation->getFields() as $fieldName) {
  155.             $this->queryHelper->resolveAccessor($fieldName$definition$table$query$context$aggregation);
  156.         }
  157.         $query->resetQueryPart('groupBy');
  158.         $this->extendQuery($aggregation$query$definition$context);
  159.         $rows $query->executeQuery()->fetchAllAssociative();
  160.         return $this->hydrateResult($aggregation$definition$rows$context);
  161.     }
  162.     private function findToManyPath(Aggregation $aggregationEntityDefinition $definition): ?string
  163.     {
  164.         $fields EntityDefinitionQueryHelper::getFieldsOfAccessor($definition$aggregation->getField(), false);
  165.         if (\count($fields) === 0) {
  166.             return null;
  167.         }
  168.         // contains later the path to the first to many association
  169.         $path = [$definition->getEntityName()];
  170.         $found false;
  171.         /** @var Field $field */
  172.         foreach ($fields as $field) {
  173.             if (!($field instanceof AssociationField)) {
  174.                 break;
  175.             }
  176.             // if to many not already detected, continue with path building
  177.             $path[] = $field->getPropertyName();
  178.             if ($field instanceof ManyToManyAssociationField || $field instanceof OneToManyAssociationField) {
  179.                 $found true;
  180.             }
  181.         }
  182.         if ($found) {
  183.             return implode('.'$path);
  184.         }
  185.         return null;
  186.     }
  187.     private function extendQuery(Aggregation $aggregationQueryBuilder $queryEntityDefinition $definitionContext $context): void
  188.     {
  189.         match (true) {
  190.             $aggregation instanceof DateHistogramAggregation => $this->parseDateHistogramAggregation($aggregation$query$definition$context),
  191.             $aggregation instanceof TermsAggregation => $this->parseTermsAggregation($aggregation$query$definition$context),
  192.             $aggregation instanceof FilterAggregation => $this->parseFilterAggregation($aggregation$query$definition$context),
  193.             $aggregation instanceof AvgAggregation => $this->parseAvgAggregation($aggregation$query$definition$context),
  194.             $aggregation instanceof SumAggregation => $this->parseSumAggregation($aggregation$query$definition$context),
  195.             $aggregation instanceof MaxAggregation => $this->parseMaxAggregation($aggregation$query$definition$context),
  196.             $aggregation instanceof MinAggregation => $this->parseMinAggregation($aggregation$query$definition$context),
  197.             $aggregation instanceof CountAggregation => $this->parseCountAggregation($aggregation$query$definition$context),
  198.             $aggregation instanceof StatsAggregation => $this->parseStatsAggregation($aggregation$query$definition$context),
  199.             $aggregation instanceof EntityAggregation => $this->parseEntityAggregation($aggregation$query$definition$context),
  200.             $aggregation instanceof RangeAggregation => $this->parseRangeAggregation($aggregation$query$definition$context),
  201.             default => throw new InvalidAggregationQueryException(sprintf('Aggregation of type %s not supported'$aggregation::class)),
  202.         };
  203.     }
  204.     private function parseFilterAggregation(FilterAggregation $aggregationQueryBuilder $queryEntityDefinition $definitionContext $context): void
  205.     {
  206.         if (!empty($aggregation->getFilter())) {
  207.             $this->criteriaQueryBuilder->addFilter($definition, new MultiFilter(MultiFilter::CONNECTION_AND$aggregation->getFilter()), $query$context);
  208.         }
  209.         /** @var Aggregation $aggregationStruct FilterAggregations always have an aggregation */
  210.         $aggregationStruct $aggregation->getAggregation();
  211.         $this->extendQuery($aggregationStruct$query$definition$context);
  212.     }
  213.     private function parseDateHistogramAggregation(DateHistogramAggregation $aggregationQueryBuilder $queryEntityDefinition $definitionContext $context): void
  214.     {
  215.         $accessor $this->queryHelper->getFieldAccessor($aggregation->getField(), $definition$definition->getEntityName(), $context);
  216.         if ($this->timeZoneSupportEnabled && $aggregation->getTimeZone()) {
  217.             $accessor 'CONVERT_TZ(' $accessor ', "UTC", "' $aggregation->getTimeZone() . '")';
  218.         }
  219.         $groupBy = match ($aggregation->getInterval()) {
  220.             DateHistogramAggregation::PER_MINUTE => 'DATE_FORMAT(' $accessor ', \'%Y-%m-%d %H:%i\')',
  221.             DateHistogramAggregation::PER_HOUR => 'DATE_FORMAT(' $accessor ', \'%Y-%m-%d %H\')',
  222.             DateHistogramAggregation::PER_DAY => 'DATE_FORMAT(' $accessor ', \'%Y-%m-%d\')',
  223.             DateHistogramAggregation::PER_WEEK => 'DATE_FORMAT(' $accessor ', \'%Y-%v\')',
  224.             DateHistogramAggregation::PER_MONTH => 'DATE_FORMAT(' $accessor ', \'%Y-%m\')',
  225.             DateHistogramAggregation::PER_QUARTER => 'CONCAT(DATE_FORMAT(' $accessor ', \'%Y\'), \'-\', QUARTER(' $accessor '))',
  226.             DateHistogramAggregation::PER_YEAR => 'DATE_FORMAT(' $accessor ', \'%Y\')',
  227.             default => throw new \RuntimeException('Provided date format is not supported'),
  228.         };
  229.         $query->addGroupBy($groupBy);
  230.         $key $aggregation->getName() . '.key';
  231.         $query->addSelect(sprintf('MIN(%s) as `%s`'$accessor$key));
  232.         $key $aggregation->getName() . '.count';
  233.         $countAccessor $this->queryHelper->getFieldAccessor('id'$definition$definition->getEntityName(), $context);
  234.         $query->addSelect(sprintf('COUNT(%s) as `%s`'$countAccessor$key));
  235.         if ($aggregation->getSorting()) {
  236.             $this->addSorting($aggregation->getSorting(), $definition$query$context);
  237.         } else {
  238.             $query->addOrderBy($accessor);
  239.         }
  240.         if ($aggregation->getAggregation()) {
  241.             $this->extendQuery($aggregation->getAggregation(), $query$definition$context);
  242.         }
  243.     }
  244.     private function parseTermsAggregation(TermsAggregation $aggregationQueryBuilder $queryEntityDefinition $definitionContext $context): void
  245.     {
  246.         $keyAccessor $this->queryHelper->getFieldAccessor($aggregation->getField(), $definition$definition->getEntityName(), $context);
  247.         $query->addGroupBy($keyAccessor);
  248.         $key $aggregation->getName() . '.key';
  249.         $field $this->queryHelper->getField($aggregation->getField(), $definition$definition->getEntityName());
  250.         if ($field instanceof FkField || $field instanceof IdField) {
  251.             $keyAccessor 'LOWER(HEX(' $keyAccessor '))';
  252.         }
  253.         $query->addSelect(sprintf('%s as `%s`'$keyAccessor$key));
  254.         $key $aggregation->getName() . '.count';
  255.         $countAccessor $this->queryHelper->getFieldAccessor('id'$definition$definition->getEntityName(), $context);
  256.         $query->addSelect(sprintf('COUNT(%s) as `%s`'$countAccessor$key));
  257.         if ($aggregation->getLimit()) {
  258.             $query->setMaxResults($aggregation->getLimit());
  259.         }
  260.         if ($aggregation->getSorting()) {
  261.             $this->addSorting($aggregation->getSorting(), $definition$query$context);
  262.         }
  263.         if ($aggregation->getAggregation()) {
  264.             $this->extendQuery($aggregation->getAggregation(), $query$definition$context);
  265.         }
  266.     }
  267.     private function parseAvgAggregation(AvgAggregation $aggregationQueryBuilder $queryEntityDefinition $definitionContext $context): void
  268.     {
  269.         $accessor $this->queryHelper->getFieldAccessor($aggregation->getField(), $definition$definition->getEntityName(), $context);
  270.         $query->addSelect(sprintf('AVG(%s) as `%s`'$accessor$aggregation->getName()));
  271.     }
  272.     private function parseSumAggregation(SumAggregation $aggregationQueryBuilder $queryEntityDefinition $definitionContext $context): void
  273.     {
  274.         $accessor $this->queryHelper->getFieldAccessor($aggregation->getField(), $definition$definition->getEntityName(), $context);
  275.         $query->addSelect(sprintf('SUM(%s) as `%s`'$accessor$aggregation->getName()));
  276.     }
  277.     private function parseMaxAggregation(MaxAggregation $aggregationQueryBuilder $queryEntityDefinition $definitionContext $context): void
  278.     {
  279.         $accessor $this->queryHelper->getFieldAccessor($aggregation->getField(), $definition$definition->getEntityName(), $context);
  280.         $query->addSelect(sprintf('MAX(%s) as `%s`'$accessor$aggregation->getName()));
  281.     }
  282.     private function parseMinAggregation(MinAggregation $aggregationQueryBuilder $queryEntityDefinition $definitionContext $context): void
  283.     {
  284.         $accessor $this->queryHelper->getFieldAccessor($aggregation->getField(), $definition$definition->getEntityName(), $context);
  285.         $query->addSelect(sprintf('MIN(%s) as `%s`'$accessor$aggregation->getName()));
  286.     }
  287.     private function parseCountAggregation(CountAggregation $aggregationQueryBuilder $queryEntityDefinition $definitionContext $context): void
  288.     {
  289.         $accessor $this->queryHelper->getFieldAccessor($aggregation->getField(), $definition$definition->getEntityName(), $context);
  290.         $query->addSelect(sprintf('COUNT(DISTINCT %s) as `%s`'$accessor$aggregation->getName()));
  291.     }
  292.     private function parseStatsAggregation(StatsAggregation $aggregationQueryBuilder $queryEntityDefinition $definitionContext $context): void
  293.     {
  294.         $accessor $this->queryHelper->getFieldAccessor($aggregation->getField(), $definition$definition->getEntityName(), $context);
  295.         if ($aggregation->fetchAvg()) {
  296.             $query->addSelect(sprintf('AVG(%s) as `%s.avg`'$accessor$aggregation->getName()));
  297.         }
  298.         if ($aggregation->fetchMin()) {
  299.             $query->addSelect(sprintf('MIN(%s) as `%s.min`'$accessor$aggregation->getName()));
  300.         }
  301.         if ($aggregation->fetchMax()) {
  302.             $query->addSelect(sprintf('MAX(%s) as `%s.max`'$accessor$aggregation->getName()));
  303.         }
  304.         if ($aggregation->fetchSum()) {
  305.             $query->addSelect(sprintf('SUM(%s) as `%s.sum`'$accessor$aggregation->getName()));
  306.         }
  307.     }
  308.     private function parseRangeAggregation(RangeAggregation $aggregationQueryBuilder $queryEntityDefinition $definitionContext $context): void
  309.     {
  310.         $accessor $this->queryHelper->getFieldAccessor($aggregation->getField(), $definition$definition->getEntityName(), $context);
  311.         $field $this->queryHelper->getField($aggregation->getField(), $definition$definition->getEntityName());
  312.         if (!$field instanceof PriceField && !$field instanceof FloatField && !$field instanceof IntField) {
  313.             throw new \RuntimeException(sprintf('Provided field "%s" is not supported in RangeAggregation (supports : PriceField, FloatField, IntField)'$aggregation->getField()));
  314.         }
  315.         // build SUM() with range criteria for each range and add it to select
  316.         foreach ($aggregation->getRanges() as $range) {
  317.             $id $range['key'] ?? (($range['from'] ?? '*') . '-' . ($range['to'] ?? '*'));
  318.             $sum '1';
  319.             if (isset($range['from'])) {
  320.                 $sum .= sprintf(' AND %s >= %f'$accessor$range['from']);
  321.             }
  322.             if (isset($range['to'])) {
  323.                 $sum .= sprintf(' AND %s < %f'$accessor$range['to']);
  324.             }
  325.             $query->addSelect(sprintf('SUM(%s) as `%s.%s`'$sum$aggregation->getName(), $id));
  326.         }
  327.     }
  328.     private function parseEntityAggregation(EntityAggregation $aggregationQueryBuilder $queryEntityDefinition $definitionContext $context): void
  329.     {
  330.         $accessor $this->queryHelper->getFieldAccessor($aggregation->getField(), $definition$definition->getEntityName(), $context);
  331.         $query->addGroupBy($accessor);
  332.         $accessor 'LOWER(HEX(' $accessor '))';
  333.         $query->addSelect(sprintf('%s as `%s`'$accessor$aggregation->getName()));
  334.     }
  335.     /**
  336.      * @param array<mixed> $rows
  337.      */
  338.     private function hydrateResult(Aggregation $aggregationEntityDefinition $definition, array $rowsContext $context): AggregationResult
  339.     {
  340.         $name $aggregation->getName();
  341.         switch (true) {
  342.             case $aggregation instanceof DateHistogramAggregation:
  343.                 return $this->hydrateDateHistogramAggregation($aggregation$definition$rows$context);
  344.             case $aggregation instanceof TermsAggregation:
  345.                 return $this->hydrateTermsAggregation($aggregation$definition$rows$context);
  346.             case $aggregation instanceof FilterAggregation:
  347.                 /** @var Aggregation $aggregationStruct FilterAggregations always have an aggregation */
  348.                 $aggregationStruct $aggregation->getAggregation();
  349.                 return $this->hydrateResult($aggregationStruct$definition$rows$context);
  350.             case $aggregation instanceof AvgAggregation:
  351.                 $value = isset($rows[0]) ? $rows[0][$name] : 0;
  352.                 return new AvgResult($aggregation->getName(), (float) $value);
  353.             case $aggregation instanceof SumAggregation:
  354.                 $value = isset($rows[0]) ? $rows[0][$name] : 0;
  355.                 return new SumResult($aggregation->getName(), (float) $value);
  356.             case $aggregation instanceof MaxAggregation:
  357.                 $value = isset($rows[0]) ? $rows[0][$name] : 0;
  358.                 return new MaxResult($aggregation->getName(), $value);
  359.             case $aggregation instanceof MinAggregation:
  360.                 $value = isset($rows[0]) ? $rows[0][$name] : 0;
  361.                 return new MinResult($aggregation->getName(), $value);
  362.             case $aggregation instanceof CountAggregation:
  363.                 $value = isset($rows[0]) ? $rows[0][$name] : 0;
  364.                 return new CountResult($aggregation->getName(), (int) $value);
  365.             case $aggregation instanceof StatsAggregation:
  366.                 if (empty($rows)) {
  367.                     return new StatsResult($aggregation->getName(), 000.00.0);
  368.                 }
  369.                 $min $rows[0][$name '.min'] ?? null;
  370.                 $max $rows[0][$name '.max'] ?? null;
  371.                 $avg = isset($rows[0][$name '.avg']) ? (float) $rows[0][$name '.avg'] : null;
  372.                 $sum = isset($rows[0][$name '.sum']) ? (float) $rows[0][$name '.sum'] : null;
  373.                 return new StatsResult($aggregation->getName(), $min$max$avg$sum);
  374.             case $aggregation instanceof EntityAggregation:
  375.                 return $this->hydrateEntityAggregation($aggregation$rows$context);
  376.             case $aggregation instanceof RangeAggregation:
  377.                 return $this->hydrateRangeAggregation($aggregation$rows);
  378.             default:
  379.                 throw new InvalidAggregationQueryException(sprintf('Aggregation of type %s not supported'$aggregation::class));
  380.         }
  381.     }
  382.     /**
  383.      * @param array<mixed> $rows
  384.      */
  385.     private function hydrateEntityAggregation(EntityAggregation $aggregation, array $rowsContext $context): EntityResult
  386.     {
  387.         $ids array_filter(array_column($rows$aggregation->getName()));
  388.         if (empty($ids)) {
  389.             return new EntityResult($aggregation->getName(), new EntityCollection());
  390.         }
  391.         $repository $this->registry->getRepository($aggregation->getEntity());
  392.         $criteria = new Criteria($ids);
  393.         $criteria->setTitle($aggregation->getName() . '-aggregation');
  394.         $entities $repository->search($criteria$context);
  395.         return new EntityResult($aggregation->getName(), $entities->getEntities());
  396.     }
  397.     /**
  398.      * @param array<mixed> $rows
  399.      */
  400.     private function hydrateDateHistogramAggregation(DateHistogramAggregation $aggregationEntityDefinition $definition, array $rowsContext $context): DateHistogramResult
  401.     {
  402.         if (empty($rows)) {
  403.             return new DateHistogramResult($aggregation->getName(), []);
  404.         }
  405.         $buckets = [];
  406.         $grouped $this->groupBuckets($aggregation$rows);
  407.         foreach ($grouped as $value => $group) {
  408.             $count $group['count'];
  409.             $nested null;
  410.             if ($aggregation->getAggregation()) {
  411.                 $nested $this->hydrateResult($aggregation->getAggregation(), $definition$group['buckets'], $context);
  412.             }
  413.             $date = new \DateTime($value);
  414.             if ($aggregation->getFormat()) {
  415.                 $value $date->format($aggregation->getFormat());
  416.             } else {
  417.                 $value self::formatDate($aggregation->getInterval(), $date);
  418.             }
  419.             $buckets[] = new Bucket($value$count$nested);
  420.         }
  421.         return new DateHistogramResult($aggregation->getName(), $buckets);
  422.     }
  423.     /**
  424.      * @param array<mixed> $rows
  425.      */
  426.     private function hydrateTermsAggregation(TermsAggregation $aggregationEntityDefinition $definition, array $rowsContext $context): TermsResult
  427.     {
  428.         $buckets = [];
  429.         $grouped $this->groupBuckets($aggregation$rows);
  430.         foreach ($grouped as $value => $group) {
  431.             $count $group['count'];
  432.             $nested null;
  433.             if ($aggregation->getAggregation()) {
  434.                 $nested $this->hydrateResult($aggregation->getAggregation(), $definition$group['buckets'], $context);
  435.             }
  436.             $buckets[] = new Bucket((string) $value$count$nested);
  437.         }
  438.         return new TermsResult($aggregation->getName(), $buckets);
  439.     }
  440.     private function addSorting(FieldSorting $sortingEntityDefinition $definitionQueryBuilder $queryContext $context): void
  441.     {
  442.         if ($sorting->getField() !== '_count') {
  443.             $this->criteriaQueryBuilder->addSortings($definition, new Criteria(), [$sorting], $query$context);
  444.             return;
  445.         }
  446.         $countAccessor $this->queryHelper->getFieldAccessor('id'$definition$definition->getEntityName(), $context);
  447.         $countAccessor sprintf('COUNT(%s)'$countAccessor);
  448.         $direction $sorting->getDirection() === FieldSorting::ASCENDING FieldSorting::ASCENDING FieldSorting::DESCENDING;
  449.         $query->addOrderBy($countAccessor$direction);
  450.     }
  451.     /**
  452.      * @param array<mixed> $rows
  453.      *
  454.      * @return array<array{ count: int, buckets: list<mixed>}>
  455.      */
  456.     private function groupBuckets(BucketAggregation $aggregation, array $rows): array
  457.     {
  458.         $valueKey $aggregation->getName() . '.key';
  459.         $countKey $aggregation->getName() . '.count';
  460.         $grouped = [];
  461.         foreach ($rows as $row) {
  462.             $value $row[$valueKey];
  463.             $count = (int) $row[$countKey];
  464.             if (isset($grouped[$value])) {
  465.                 $grouped[$value]['count'] += $count;
  466.             } else {
  467.                 $grouped[$value] = ['count' => $count'buckets' => []];
  468.             }
  469.             if ($aggregation->getAggregation()) {
  470.                 $grouped[$value]['buckets'][] = $row;
  471.             }
  472.         }
  473.         return $grouped;
  474.     }
  475.     /**
  476.      * @param array<array<string,string>> $rows
  477.      */
  478.     private function hydrateRangeAggregation(RangeAggregation $aggregation, array $rows): RangeResult
  479.     {
  480.         $ranges = [];
  481.         $row array_shift($rows);
  482.         if ($row) {
  483.             foreach ($aggregation->getRanges() as $range) {
  484.                 $ranges[(string) $range['key']] = (int) $row[sprintf('%s.%s'$aggregation->getName(), $range['key'])];
  485.             }
  486.         }
  487.         return new RangeResult($aggregation->getName(), $ranges);
  488.     }
  489. }