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

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