Last active
March 26, 2020 14:28
-
-
Save devster/753aad5f43e47f4a31408ca615b6bc2a to your computer and use it in GitHub Desktop.
Report Aggregator POC
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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(); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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)); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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