184 lines
5.8 KiB
PHP
184 lines
5.8 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
|
|
use Symfony\Component\ExpressionLanguage\ParsedExpression;
|
|
|
|
class RuleExpressionService
|
|
{
|
|
protected ExpressionLanguage $language;
|
|
|
|
/** @var array<string, ParsedExpression> */
|
|
protected array $parsedCache = [];
|
|
|
|
public function __construct()
|
|
{
|
|
$this->language = new ExpressionLanguage();
|
|
}
|
|
|
|
public function evaluate(string $expr, array $context = [])
|
|
{
|
|
$expr = trim($expr);
|
|
if ($expr === '') {
|
|
return null;
|
|
}
|
|
|
|
$names = array_keys($context);
|
|
sort($names);
|
|
$cacheKey = md5($expr . '|' . implode(',', $names));
|
|
|
|
if (!isset($this->parsedCache[$cacheKey])) {
|
|
$this->parsedCache[$cacheKey] = $this->language->parse($expr, $names);
|
|
}
|
|
|
|
return $this->language->evaluate($this->parsedCache[$cacheKey], $context);
|
|
}
|
|
|
|
public function evaluateBoolean(?string $expr, array $context = []): bool
|
|
{
|
|
if ($expr === null || trim($expr) === '') {
|
|
return true;
|
|
}
|
|
|
|
return (bool) $this->evaluate($expr, $context);
|
|
}
|
|
|
|
/**
|
|
* Compile DSL expression to engine-compatible JSON structure.
|
|
*
|
|
* Supported DSL:
|
|
* - if(condition ? action : action)
|
|
* - sex('F'|'M') -> order["Sex"] == 'F'
|
|
* - set_result(value) -> {"value": value} or {"valueExpr": "value"}
|
|
*
|
|
* @param string $expr The raw DSL expression
|
|
* @return array The compiled structure with valueExpr
|
|
* @throws \InvalidArgumentException If DSL is invalid
|
|
*/
|
|
public function compile(string $expr): array
|
|
{
|
|
$expr = trim($expr);
|
|
if ($expr === '') {
|
|
return [];
|
|
}
|
|
|
|
// Remove outer parentheses from if(...)
|
|
if (preg_match('/^if\s*\(\s*(.+?)\s*\)$/s', $expr, $m)) {
|
|
$expr = trim($m[1]);
|
|
}
|
|
|
|
// Parse: condition ? thenAction : elseAction
|
|
if (!preg_match('/^(.+?)\s*\?\s*(.+?)\s*:\s*(.+)$/s', $expr, $parts)) {
|
|
throw new \InvalidArgumentException('Invalid DSL: expected "if(condition ? action : action)" format');
|
|
}
|
|
|
|
$condition = trim($parts[1]);
|
|
$thenAction = trim($parts[2]);
|
|
$elseAction = trim($parts[3]);
|
|
|
|
// Compile condition
|
|
$compiledCondition = $this->compileCondition($condition);
|
|
|
|
// Compile actions
|
|
$thenCompiled = $this->compileAction($thenAction);
|
|
$elseCompiled = $this->compileAction($elseAction);
|
|
|
|
// Build valueExpr combining condition and actions
|
|
$thenValue = $thenCompiled['valueExpr'] ?? json_encode($thenCompiled['value'] ?? null);
|
|
$elseValue = $elseCompiled['valueExpr'] ?? json_encode($elseCompiled['value'] ?? null);
|
|
|
|
// Handle string vs numeric values
|
|
if (is_string($thenCompiled['value'] ?? null)) {
|
|
$thenValue = '"' . addslashes($thenCompiled['value']) . '"';
|
|
}
|
|
if (is_string($elseCompiled['value'] ?? null)) {
|
|
$elseValue = '"' . addslashes($elseCompiled['value']) . '"';
|
|
}
|
|
|
|
$valueExpr = "({$compiledCondition}) ? {$thenValue} : {$elseValue}";
|
|
|
|
return [
|
|
'conditionExpr' => $compiledCondition,
|
|
'valueExpr' => $valueExpr,
|
|
'then' => $thenCompiled,
|
|
'else' => $elseCompiled,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Compile DSL condition to ExpressionLanguage expression
|
|
*/
|
|
private function compileCondition(string $condition): string
|
|
{
|
|
$condition = trim($condition);
|
|
|
|
// sex('F') -> order["Sex"] == 'F'
|
|
if (preg_match("/^sex\s*\(\s*['\"]([MF])['\"]\s*\)$/i", $condition, $m)) {
|
|
return 'order["Sex"] == "' . $m[1] . '"';
|
|
}
|
|
|
|
// sex == 'F' (alternative syntax)
|
|
if (preg_match('/^\s*sex\s*==\s*[\'"]([MF])[\'"]\s*$/i', $condition, $m)) {
|
|
return 'order["Sex"] == "' . $m[1] . '"';
|
|
}
|
|
|
|
// priority('S') -> order["Priority"] == 'S'
|
|
if (preg_match("/^priority\s*\(\s*['\"]([SR])['\"]\s*\)$/i", $condition, $m)) {
|
|
return 'order["Priority"] == "' . $m[1] . '"';
|
|
}
|
|
|
|
// priority == 'S' (alternative syntax)
|
|
if (preg_match('/^\s*priority\s*==\s*[\'"]([SR])[\'"]\s*$/i', $condition, $m)) {
|
|
return 'order["Priority"] == "' . $m[1] . '"';
|
|
}
|
|
|
|
// age > 18 -> patient["Age"] > 18 (if available) or order["Age"] > 18
|
|
if (preg_match('/^\s*age\s*([<>]=?)\s*(\d+)\s*$/i', $condition, $m)) {
|
|
return 'order["Age"] ' . $m[1] . ' ' . $m[2];
|
|
}
|
|
|
|
// If already valid ExpressionLanguage, return as-is
|
|
return $condition;
|
|
}
|
|
|
|
/**
|
|
* Compile DSL action to action params
|
|
*/
|
|
private function compileAction(string $action): array
|
|
{
|
|
$action = trim($action);
|
|
|
|
// set_result(value) -> SET_RESULT action
|
|
if (preg_match('/^set_result\s*\(\s*(.+?)\s*\)$/i', $action, $m)) {
|
|
$value = trim($m[1]);
|
|
|
|
// Check if it's a number
|
|
if (is_numeric($value)) {
|
|
return [
|
|
'type' => 'SET_RESULT',
|
|
'value' => strpos($value, '.') !== false ? (float) $value : (int) $value,
|
|
'valueExpr' => $value,
|
|
];
|
|
}
|
|
|
|
// Check if it's a quoted string
|
|
if (preg_match('/^["\'](.+)["\']$/s', $value, $vm)) {
|
|
return [
|
|
'type' => 'SET_RESULT',
|
|
'value' => $vm[1],
|
|
'valueExpr' => '"' . addslashes($vm[1]) . '"',
|
|
];
|
|
}
|
|
|
|
// Complex expression
|
|
return [
|
|
'type' => 'SET_RESULT',
|
|
'valueExpr' => $value,
|
|
];
|
|
}
|
|
|
|
throw new \InvalidArgumentException('Unknown action: ' . $action);
|
|
}
|
|
}
|