vendor/shopware/core/Framework/DataAbstractionLayer/Dbal/EntityAggregator.php line 89

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