2026-03-16 07:24:50 +07:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Models\Rule;
|
|
|
|
|
|
|
|
|
|
use App\Models\BaseModel;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* RuleDef Model
|
|
|
|
|
*
|
|
|
|
|
* Rule definitions that can be linked to multiple tests via testrule mapping table.
|
|
|
|
|
*/
|
|
|
|
|
class RuleDefModel extends BaseModel
|
|
|
|
|
{
|
|
|
|
|
protected $table = 'ruledef';
|
|
|
|
|
protected $primaryKey = 'RuleID';
|
|
|
|
|
protected $allowedFields = [
|
|
|
|
|
'RuleCode',
|
|
|
|
|
'RuleName',
|
|
|
|
|
'Description',
|
|
|
|
|
'EventCode',
|
|
|
|
|
'ConditionExpr',
|
|
|
|
|
'ConditionExprCompiled',
|
|
|
|
|
'CreateDate',
|
|
|
|
|
'StartDate',
|
|
|
|
|
'EndDate',
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
protected $useTimestamps = true;
|
|
|
|
|
protected $createdField = 'CreateDate';
|
|
|
|
|
protected $updatedField = 'StartDate';
|
|
|
|
|
|
|
|
|
|
protected $useSoftDeletes = true;
|
|
|
|
|
protected $deletedField = 'EndDate';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Fetch active rules for an event scoped by TestSiteID.
|
|
|
|
|
*
|
|
|
|
|
* Rules are standalone and only apply when explicitly linked to a test
|
|
|
|
|
* via the testrule mapping table.
|
|
|
|
|
*
|
|
|
|
|
* @param string $eventCode The event code to filter by
|
|
|
|
|
* @param int|null $testSiteID The test site ID to filter by
|
|
|
|
|
* @return array Array of matching rules
|
|
|
|
|
*/
|
|
|
|
|
public function getActiveByEvent(string $eventCode, ?int $testSiteID = null): array
|
|
|
|
|
{
|
|
|
|
|
if ($testSiteID === null) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $this->select('ruledef.*')
|
|
|
|
|
->join('testrule', 'testrule.RuleID = ruledef.RuleID', 'inner')
|
|
|
|
|
->where('ruledef.EventCode', $eventCode)
|
|
|
|
|
->where('ruledef.EndDate IS NULL')
|
|
|
|
|
->where('testrule.TestSiteID', $testSiteID)
|
|
|
|
|
->where('testrule.EndDate IS NULL')
|
|
|
|
|
->orderBy('ruledef.RuleID', 'ASC')
|
|
|
|
|
->findAll();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get all tests linked to a rule
|
|
|
|
|
*
|
|
|
|
|
* @param int $ruleID The rule ID
|
|
|
|
|
* @return array Array of test site IDs
|
|
|
|
|
*/
|
|
|
|
|
public function getLinkedTests(int $ruleID): array
|
|
|
|
|
{
|
|
|
|
|
$db = \Config\Database::connect();
|
|
|
|
|
$result = $db->table('testrule')
|
|
|
|
|
->where('RuleID', $ruleID)
|
|
|
|
|
->where('EndDate IS NULL')
|
|
|
|
|
->select('TestSiteID')
|
|
|
|
|
->get()
|
|
|
|
|
->getResultArray();
|
|
|
|
|
|
2026-03-16 15:58:56 +07:00
|
|
|
return array_map('intval', array_column($result, 'TestSiteID'));
|
2026-03-16 07:24:50 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Link a rule to a test
|
|
|
|
|
*
|
|
|
|
|
* @param int $ruleID The rule ID
|
|
|
|
|
* @param int $testSiteID The test site ID
|
|
|
|
|
* @return bool Success status
|
|
|
|
|
*/
|
|
|
|
|
public function linkTest(int $ruleID, int $testSiteID): bool
|
|
|
|
|
{
|
|
|
|
|
$db = \Config\Database::connect();
|
|
|
|
|
|
|
|
|
|
// Check if already linked (and not soft deleted)
|
2026-03-16 15:58:56 +07:00
|
|
|
$existing = $db->table('testrule')
|
|
|
|
|
->where('RuleID', $ruleID)
|
|
|
|
|
->where('TestSiteID', $testSiteID)
|
|
|
|
|
->where('EndDate IS NULL')
|
|
|
|
|
->get()
|
|
|
|
|
->getRowArray();
|
2026-03-16 07:24:50 +07:00
|
|
|
|
|
|
|
|
if ($existing) {
|
|
|
|
|
return true; // Already linked
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if soft deleted - restore it
|
2026-03-16 15:58:56 +07:00
|
|
|
$softDeleted = $db->table('testrule')
|
|
|
|
|
->where('RuleID', $ruleID)
|
|
|
|
|
->where('TestSiteID', $testSiteID)
|
|
|
|
|
->where('EndDate IS NOT NULL')
|
|
|
|
|
->get()
|
|
|
|
|
->getRowArray();
|
2026-03-16 07:24:50 +07:00
|
|
|
|
|
|
|
|
if ($softDeleted) {
|
|
|
|
|
return $db->table('testrule')
|
|
|
|
|
->where('TestRuleID', $softDeleted['TestRuleID'])
|
|
|
|
|
->update(['EndDate' => null]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create new link
|
|
|
|
|
return $db->table('testrule')->insert([
|
|
|
|
|
'RuleID' => $ruleID,
|
|
|
|
|
'TestSiteID' => $testSiteID,
|
|
|
|
|
'CreateDate' => date('Y-m-d H:i:s'),
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Unlink a rule from a test (soft delete)
|
|
|
|
|
*
|
|
|
|
|
* @param int $ruleID The rule ID
|
|
|
|
|
* @param int $testSiteID The test site ID
|
|
|
|
|
* @return bool Success status
|
|
|
|
|
*/
|
|
|
|
|
public function unlinkTest(int $ruleID, int $testSiteID): bool
|
|
|
|
|
{
|
|
|
|
|
$db = \Config\Database::connect();
|
|
|
|
|
|
|
|
|
|
return $db->table('testrule')
|
|
|
|
|
->where('RuleID', $ruleID)
|
|
|
|
|
->where('TestSiteID', $testSiteID)
|
|
|
|
|
->where('EndDate IS NULL')
|
|
|
|
|
->update(['EndDate' => date('Y-m-d H:i:s')]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Validate that a DSL expression compiles successfully
|
|
|
|
|
*
|
|
|
|
|
* @param string $expr The DSL expression to validate
|
|
|
|
|
* @return array ['valid' => bool, 'error' => string|null, 'compiled' => array|null]
|
|
|
|
|
*/
|
|
|
|
|
public function validateExpression(string $expr): array
|
|
|
|
|
{
|
|
|
|
|
$service = new \App\Services\RuleExpressionService();
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
$compiled = $service->compile($expr);
|
|
|
|
|
return [
|
|
|
|
|
'valid' => true,
|
|
|
|
|
'error' => null,
|
|
|
|
|
'compiled' => $compiled,
|
|
|
|
|
];
|
|
|
|
|
} catch (\Throwable $e) {
|
|
|
|
|
return [
|
|
|
|
|
'valid' => false,
|
|
|
|
|
'error' => $e->getMessage(),
|
|
|
|
|
'compiled' => null,
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Compile a DSL expression to JSON
|
|
|
|
|
*
|
|
|
|
|
* @param string $expr The DSL expression to compile
|
|
|
|
|
* @return array The compiled structure
|
|
|
|
|
* @throws \InvalidArgumentException If compilation fails
|
|
|
|
|
*/
|
|
|
|
|
public function compileExpression(string $expr): array
|
|
|
|
|
{
|
|
|
|
|
$service = new \App\Services\RuleExpressionService();
|
|
|
|
|
return $service->compile($expr);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check if a rule's expression needs recompilation
|
|
|
|
|
* Returns true if the raw expression differs from the compiled version
|
|
|
|
|
*
|
|
|
|
|
* @param int $ruleID The rule ID to check
|
|
|
|
|
* @return bool True if recompilation is needed
|
|
|
|
|
*/
|
|
|
|
|
public function needsRecompilation(int $ruleID): bool
|
|
|
|
|
{
|
|
|
|
|
$rule = $this->find($ruleID);
|
|
|
|
|
if (!$rule) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$rawExpr = $rule['ConditionExpr'] ?? '';
|
|
|
|
|
$compiledJson = $rule['ConditionExprCompiled'] ?? '';
|
|
|
|
|
|
|
|
|
|
if (empty($rawExpr)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (empty($compiledJson)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Try to compile current expression and compare with stored version
|
|
|
|
|
try {
|
|
|
|
|
$compiled = $this->compileExpression($rawExpr);
|
|
|
|
|
$stored = json_decode($compiledJson, true);
|
|
|
|
|
|
|
|
|
|
return json_encode($compiled) !== json_encode($stored);
|
|
|
|
|
} catch (\Throwable $e) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Automatically compile expression if needed and update the rule
|
|
|
|
|
*
|
|
|
|
|
* @param int $ruleID The rule ID
|
|
|
|
|
* @return bool Success status
|
|
|
|
|
*/
|
|
|
|
|
public function autoCompile(int $ruleID): bool
|
|
|
|
|
{
|
|
|
|
|
if (!$this->needsRecompilation($ruleID)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$rule = $this->find($ruleID);
|
|
|
|
|
if (!$rule) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$rawExpr = $rule['ConditionExpr'] ?? '';
|
|
|
|
|
if (empty($rawExpr)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
$compiled = $this->compileExpression($rawExpr);
|
|
|
|
|
return $this->update($ruleID, [
|
|
|
|
|
'ConditionExprCompiled' => json_encode($compiled),
|
|
|
|
|
]);
|
|
|
|
|
} catch (\Throwable $e) {
|
|
|
|
|
log_message('error', 'Auto-compile failed for RuleID=' . $ruleID . ': ' . $e->getMessage());
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Compile all rules that need recompilation
|
|
|
|
|
*
|
|
|
|
|
* @return array ['success' => int, 'failed' => int]
|
|
|
|
|
*/
|
|
|
|
|
public function compileAllPending(): array
|
|
|
|
|
{
|
|
|
|
|
$rules = $this->where('EndDate IS NULL')->findAll();
|
|
|
|
|
$success = 0;
|
|
|
|
|
$failed = 0;
|
|
|
|
|
|
|
|
|
|
foreach ($rules as $rule) {
|
|
|
|
|
$ruleID = (int) ($rule['RuleID'] ?? 0);
|
|
|
|
|
if ($ruleID === 0) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($this->needsRecompilation($ruleID)) {
|
|
|
|
|
if ($this->autoCompile($ruleID)) {
|
|
|
|
|
$success++;
|
|
|
|
|
} else {
|
|
|
|
|
$failed++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
'success' => $success,
|
|
|
|
|
'failed' => $failed,
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
}
|