clqms-be/app/Services/RuleExpressionService.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);
}
}