Skip to main content

Creating Aggregate Functions

To create an aggregate function, you need to create an implementation of the AggregateFunction or SummarizableAggregateFunction interface.

Interfaces

Aggregate function Aggregate function

AggregateFunction Interface

namespace Rekalogika\Analytics\Contracts\Summary;

use Rekalogika\Analytics\Contracts\Context\SummaryQueryContext;

interface AggregateFunction
{
public function getAggregateToResultExpression(
string $inputExpression,
SummaryQueryContext $context,
): string;
}

Implement AggregateFunction if the function does not read any data from the source directly, but gets its input from other aggregate functions. Example: the Average aggregate function, it takes the sum and count from other aggregate functions and uses them to calculate the average.

SummarizableAggregateFunction Interface

namespace Rekalogika\Analytics\Contracts\Summary;

use Rekalogika\Analytics\Contracts\Context\SourceQueryContext;

interface SummarizableAggregateFunction extends AggregateFunction
{
public function getSourceToAggregateExpression(
SourceQueryContext $context,
): string;

public function getAggregateToAggregateExpression(
string $inputExpression,
): string;

/**
* @return list<string>
*/
public function getInvolvedProperties(): array;
}

Implement SummarizableAggregateFunction if the function reads data from the source entity. You need to supply these logics using DQL expressions:

  • getSourceToAggregateExpression() - How to transform a field from multiple source entities into a single aggregate value.
  • getAggregateToAggregateExpression() - How to combine multiple aggregate values into a single aggregate value.
  • getAggregateToResultExpression() - How to transform an aggregate value into the final result value.

Division Consideration

When creating an aggregate function that involves division, you should consider the case when the denominator is zero. To avoid division by zero, use the DQL expression NULLIF(<denominator>, 0). This will return NULL if the denominator is zero, which is a safe way to handle division in DQL.

User Value Transformer

The aggregate function can provide a way to transform the raw result value (as returned by Doctrine) into a user-friendly object. This is done by implementing the UserValueTransformer interface. The following is an example done for the CountDistinct aggregate function.

use Rekalogika\Analytics\Contracts\Context\ValueTransformerContext;
use Rekalogika\Analytics\Contracts\Summary\SummarizableAggregateFunction;
use Rekalogika\Analytics\Contracts\Summary\UserValueTransformer;

/**
* @implements UserValueTransformer<int,ApproximateCount>
*/
final readonly class CountDistinct implements
SummarizableAggregateFunction,
UserValueTransformer
{
// ...

#[\Override]
public function getUserValue(
mixed $rawValue,
ValueTransformerContext $context,
): mixed {
if ($rawValue === null) {
return null;
}

return new ApproximateCount($rawValue);
}
}

Then in the summary class, the user will be able to do something like this:

use Doctrine\ORM\Mapping as ORM;
use Doctrine\ORM\Types\Types;
use Rekalogika\Analytics\Core\Metadata as Analytics;
use Rekalogika\Analytics\Core\Entity\BaseSummary;

#[ORM\Entity()]
#[Analytics\Summary(
sourceClass: Order::class,
)]
class OrderSummary extends BaseSummary
{
#[ORM\Column(type: 'rekalogika_hll')]
#[Analytics\Measure(
function: new CountDistinct(new IdentifierValue('customer')),
)]
private ?int $uniqueCustomers = null;

public function getUniqueCustomers(): ?ApproximateCount
{
return $this->getContext()->getUserValue(
property: 'uniqueCustomers',
class: ApproximateCount::class,
);
}
}