Skip to content

Instantly share code, notes, and snippets.

@yann-eugone
Last active December 15, 2020 10:39
Show Gist options
  • Save yann-eugone/7ce529aaedba245ab35453066778be8e to your computer and use it in GitHub Desktop.
Save yann-eugone/7ce529aaedba245ab35453066778be8e to your computer and use it in GitHub Desktop.
Formula calculation using symfony/expression-language
<?php
require __DIR__ . '/vendor/autoload.php';
use Symfony\Component\ExpressionLanguage\ExpressionFunction;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
use Symfony\Component\ExpressionLanguage\Node\NameNode;
use Symfony\Component\ExpressionLanguage\Node\Node;
class Formula
{
private ExpressionLanguage $language;
public function __construct(ExpressionLanguage $language = null)
{
$this->language = $language ?? new ExpressionLanguage();
$this->language->addFunction(ExpressionFunction::fromPhp('round'));
}
public function calculate(array $data, array $formulas): array
{
foreach ($this->sort($formulas, array_keys($data)) as $var => $formula) {
$data[$var] = $this->language->evaluate($formula, $data);
}
return $data;
}
private function sort(array $formulas, array $dataVars): array
{
$vars = [...$dataVars, ...array_keys($formulas)];
$items = array_fill_keys($dataVars, []);
foreach ($formulas as $var => $formula) {
$items[$var] = array_unique($this->vars($this->language->parse($formula, $vars)->getNodes()));
}
$total = count($items);
$count = 0;
$sorted = [];
$done = [];
// while not all items are resolved
while ($total > $count) {
$resolvedItem = false;
foreach ($items as $id => $deps) {
if (isset($done[$id])) {
// item already in resultset
continue;
}
$resolved = true;
foreach ($deps as $dep) {
if (!isset($done[$dep])) {
// there is a dependency that is not met
$resolved = false;
break;
}
}
if ($resolved) {
//all dependencies are met
$done[$id] = true;
$resolvedItem = true;
$count++;
if (isset($formulas[$id])) {
$sorted[$id] = $formulas[$id];
}
}
}
if (!$resolvedItem) {
throw new Exception('unresolvable dependency');
}
}
return $sorted;
}
private function vars(Node $node): array
{
if ($node instanceof NameNode) {
return [$node->toArray()[0]];
}
$vars = [];
foreach ($node->toArray() as $item) {
if (!$item instanceof Node) {
continue;
}
$vars[] = $this->vars($item);
}
return array_merge(...$vars);
}
}
$formulas = [
'total' => 'round(amount + shipping, 2)',
'shipping' => 'amount > 100 ? 0 : (weight > 5 ? 3 : weight * 0.5)',
'weight' => '(qty * unit_weight)',
'amount' => 'unit_amount * qty',
];
$data = [
'unit_amount' => 10,
'qty' => 3,
'unit_weight' => 0.75,
];
var_dump((new Formula())->calculate($data, $formulas));
/*
array [
"unit_amount" => 10
"qty" => 3
"unit_weight" => 0.75
"weight" => 2.25
"amount" => 30
"shipping" => 1.125
"total" => 31.13
]
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment