Skip to content

Instantly share code, notes, and snippets.

@devster
Last active March 26, 2020 14:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save devster/753aad5f43e47f4a31408ca615b6bc2a to your computer and use it in GitHub Desktop.
Save devster/753aad5f43e47f4a31408ca615b6bc2a to your computer and use it in GitHub Desktop.
Report Aggregator POC
<?php
declare(strict_types=1);
namespace App\Reporting\Core\ReportAggregator;
interface AggregatorInterface
{
/**
* @param $value int|float
*/
public function add($value): void;
/**
* @return int|float
*/
public function getResult();
}
<?php
declare(strict_types=1);
namespace App\Reporting\Core\ReportAggregator;
class AvgAggregator implements AggregatorInterface
{
/** @var int|float */
private $total;
private int $counter = 0;
public function add($value): void
{
$this->counter++;
$this->total += $value;
}
/**
* @return float|int
*/
public function getResult()
{
return $this->total / $this->counter;
}
}
<?php
declare(strict_types=1);
namespace App\Reporting\Core\ReportAggregator;
abstract class ReportAggregator implements \IteratorAggregate
{
private array $segmentFields;
private array $sumFields = [];
private array $avgFields = [];
private array $aggregates = [];
public function __construct()
{
$this->configure();
}
protected function configure(): void
{
// Configure report here: segments, sum, avg, etc
// Ex: $this->setSegmentFields(['field_1',...]);
}
public function setSegmentFields(array $fields): self
{
$this->segmentFields = $fields;
return $this;
}
public function setSumFields(array $sumFields): self
{
$this->sumFields = $sumFields;
return $this;
}
public function setAvgFields(array $avgFields): self
{
$this->avgFields = $avgFields;
return $this;
}
public function add(object $object): void
{
$data = $this->normalize($object);
$segments = [];
foreach ($this->segmentFields as $field) {
$segments[$field] = $data[$field] ?? null;
}
$key = implode('~', $segments);
// If the aggregate doesn't exist yet, we create one with the common fields: segments
if (!$aggregate = $this->aggregates[$key] ?? false) {
$aggregate = $segments;
}
// SUM aggregation
foreach ($this->sumFields as $field) {
$aggregate[$field] ??= 0;
$aggregate[$field] += $data[$field] ?? 0;
}
// AVG aggregation
foreach ($this->avgFields as $field) {
$aggregate[$field] ??= new AvgAggregator();
$aggregate[$field]->add($data[$field] ?? 0);
}
$this->aggregates[$key] = $aggregate;
}
/**
* @param object $object
* @return array<string, mixed>
*/
abstract protected function normalize(object $object): array;
public function getIterator(): \Generator
{
while (\count($this->aggregates) > 0) {
yield $this->computeAggregate(array_shift($this->aggregates));
}
}
/**
* @param array<string, mixed> $aggregate
* @return array<string, mixed>
*/
protected function format(array $aggregate): array
{
// Override this method to format final aggregate.
// Ex: $aggregate['my_field'] = round($aggregate['my_field']);
return $aggregate;
}
/**
* @param array<string, mixed> $aggregate
* @return array<string, mixed>
*/
private function computeAggregate(array $aggregate): array
{
return $this->format(array_map(function ($value) {
if (is_object($value) && $value instanceof AggregatorInterface) {
return $value->getResult();
}
return $value;
}, $aggregate));
}
}
<?php
declare(strict_types=1);
namespace App\Tests\Reporting\Core\ReportAggregator;
use App\Reporting\Core\ReportAggregator\ReportAggregator;
use PHPUnit\Framework\TestCase;
class ReportAggregatorTest extends TestCase
{
public function test()
{
$objects = [
(object) [
'date' => '2020-01-01',
'product' => 'toilet paper',
'sells' => 100,
'price' => 79.99,
],
(object) [
'date' => '2020-01-01',
'product' => 'toilet paper',
'sells' => 50,
'price' => 9,
],
(object) [
'date' => '2020-01-02',
'product' => 'toilet paper',
'sells' => 10,
'price' => 9,
],
(object) [
'date' => '2020-01-02',
'product' => 'hydro gel',
'sells' => 1000,
'price' => 99.9,
],
];
$report = new class() extends ReportAggregator {
protected function configure(): void
{
$this
->setSegmentFields([
'date',
'product',
])
->setSumFields([
'sells',
])
->setAvgFields([
'price',
]);
}
protected function normalize(object $object): array
{
return (array) $object;
}
protected function format(array $aggregate): array
{
$aggregate['price'] = round($aggregate['price'], 2);
return $aggregate;
}
};
foreach ($objects as $object) {
$report->add($object);
}
$results = iterator_to_array($report);
$this->assertSame([
[
'date' => '2020-01-01',
'product' => 'toilet paper',
'sells' => 150,
'price' => 44.50,
],
[
'date' => '2020-01-02',
'product' => 'toilet paper',
'sells' => 10,
'price' => 9.0,
],
[
'date' => '2020-01-02',
'product' => 'hydro gel',
'sells' => 1000,
'price' => 99.9,
],
], $results);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment