vertilia / recif
规则引擎的代码生成器
Requires
- php: >=7.4
- ext-json: *
Requires (Dev)
- phpunit/phpunit: ^9.5
README
将规则文件中的条件从json或yaml格式转换为本地代码的规则引擎解析器。
描述
当企业表达新的或更新的规则集时,这些规则集可能由程序员编码到现有信息系统中,或者可能使用系统可以解析和评估的特殊方言声明。在这种情况下,不需要程序员干预,但解析和评估的时间可能很长。每次系统需要评估规则集中特定上下文的条件(例如,定义产品细分)时,系统都需要解析规则文件,从顶级条件开始处理条件,直到找到匹配的规则,所有这些操作都递归调用规则集中提到的每个操作的方法。
使用recif,当规则文件中的规则集更新时,我们调用转换器将规则转换为本地代码。该代码代表文件中的规则集条件。所有操作都已转换为本地结构,如OR、AND等,因此完整规则集的执行是最佳的。要在项目中使用该类,请实例化对象并调用其evaluate()方法,将上下文作为其参数。如果找到匹配项,则evaluate()将返回true(或相应的值);如果传递的上下文不匹配任何条件,则返回false(是的,您可以提供自己的返回值)。
示例1
单个条件。输入上下文是文数字值,如果大于10则返回true
{
"gt": [{"cx":""}, 10]
}
示例2
使用AND运算符组合多个条件。输入上下文是文数字值。如果上下文值在0..100范围内则返回true
{
"and": [
{"ge": [{"cx":""}, 0]},
{"le": [{"cx":""}, 100]}
]
}
示例3
复杂条件:上下文是一个包含country和currency条目的关联数组,必须与以下矩阵匹配
如果条件匹配,它将返回上下文的大陆名称(北美洲或欧洲)而不是简单的true值。
{
"or": [
{
"and": [
{"in": [{"cx":"country"}, {"_": ["DE", "ES", "FR", "IT"]}]},
{"eq": [{"cx":"currency"}, "EUR"]}
],
"return": "Europe"
},
{
"and": [
{"eq": [{"cx":"country"}, "US"]},
{"eq": [{"cx":"currency"}, "USD"]}
],
"return": "North America"
}
]
}
注意:要表示数组,您需要使用特殊的{"_": ...}内联运算符。
安装
使用composer要求
$ composer require vertilia/recif
命令行使用
使用composer安装后,recif二进制文件作为vendor/bin/recif可用。
$ vendor/bin/recif
Usage: recif [options] <ruleset.json >Ruleset.php
Options:
-C COMMENT add comments text, should not contain open / close comment sequences (default: not set)
-n NAMESPACE use namespace (default: not set)
-s generate static evaluate() method (default: not set)
-c CLASS use class name (default: Ruleset)
-e CLASS extends class (default: not set)
-i INTERFACE implements interfaces, comma separated list (default: not set)
-x TYPE define context parameter type (default: not set)
-r TYPE define method return type (default: not set)
-S VALUE return on success default value (default: true)
-F VALUE return on fail default value (default: false)
-d declare static_types (default: not set)
-5 generate php5-compatible code
-y input stream in YAML format (needs yaml PECL extension)
最简单的规则集:评估true语句
echo 'true' | recif
同一级别:评估1 > 0
echo '{"gt":[1,0]}' | recif
做有意义的事:检查上下文,向生成的类添加命名空间并指定evaluate()方法的返回类型
echo '{"gt":[{"cx":""},0]}' | recif -n MyNamespace\\Level2 -r bool
规则文件语法
规则文件包含一个以根条件开始的条件树。
每个条件是一个对象,包含一个单一必需条目,其中包含操作名称作为键和参数作为值。可选的return条目可以包含操作匹配时的返回值,否则将返回默认的true。
如or之类的运算符必须包含一个参数数组。一元运算符如not对一个值操作,而不是数组。有关可用操作的全列表,请参阅操作参考。
每个参数可能具有以下形式之一
- 文数字值或数组(通常用于将值与上下文字段进行比较),如
true或"US"; - 对象(表示上下文或另一个操作),如
{"cx":"currency"}或{"eq": [{"cx":"country"}, "US"]}或{"_": ["DE", "ES", "FR", "IT"]}。
返回值表示当相应的条件评估为 true 时,返回给外部操作的数据值。它可以声明为任何类型。如果值是对象,它可能表示上下文或其属性之一,如 {"cx":""} 或 {"cx":"currency.iso_name"}。在这种情况下,将返回此元素或属性的值。
操作符参考
下面的示例使用 json 语法表示,含义以 PHP 代码的形式给出。
上下文操作符
cx - 上下文值(参数:如果上下文是字面量,则为空字符串;如果为数组,则为点分隔的键列表)
示例: {"cx":""}, {"cx":"url.path"}
含义: $context, $context['url']['path']
比较操作符
eq - 等于
示例: {"eq": [{"cx":""}, 0]}
含义: $context == 0
=== - 全等(等于且类型相同)
示例: {"===": [{"cx":""}, '15h']}
含义: $context === '15h'
ne - 不等于
示例: {"ne": [{"cx":""}, 0]}
含义: $context != 0
!== - 不全等(不相等或类型不同)
示例: {"!==": [{"cx":""}, '15h']}
含义: $context !== '15h'
lt - 小于
示例: {"lt": [{"cx":""}, 0]}
含义: $context < 0
le - 小于等于
示例: {"le": [{"cx":""}, 0]}
含义: $context <= 0
gt - 大于
示例: {"gt": [{"cx":""}, 0]}
含义: $context > 0
ge - 大于等于
示例: {"ge": [{"cx":""}, 0]}
含义: $context >= 0
逻辑操作符
or - 或(两个或更多参数)
示例: {"or": [{"eq": [{"cx":""}, 0]}, {"eq": [{"cx":""}, 10]}]}
含义: ($context == 0) or ($context == 10)
and - 与(两个或更多参数)
示例: {"and": [{"gt": [{"cx":""}, 0]}, {"lt": [{"cx":""}, 10]}]}
含义: ($context > 0) and ($context < 10)
not - 非(一元操作符,单个参数)
示例: {"not": {"lt": [{"cx":""}, 0]}}
含义: ! ($context < 0)
数学操作
mod - 取模(参数:值,模数)
示例: {"mod": [{"cx":""}, 10]}
含义: $context % 10
rnd - 在指定范围内生成随机整数(参数:最小值,最大值)
示例: {"rnd": [1, 10]}
含义: rand(1, 10)
数组操作
in - 元素存在于数组中(参数:元素,数组)
示例: {"in": [{"cx":""}, {"_": [1, 2, 3, 5, 8, 13]}]}
含义: in_array($context, [1, 2, 3, 5, 8, 13])
inx - 通过索引从哈希表获取元素(参数:索引,哈希表)
示例: {"inx": [{"cx":""}, {"_": ["FR": "Europe", "US": "North America"]}]}, {"inx": [{"cx":""}, {"_": ["US": "North America", "default": "Europe"]}]}
含义: (["FR" => "Europe", "US" => "North America"])[$context], (["US" => "North America"])[$context] ?? "Europe"
字符串操作
sub - 针对目标字符串的子字符串存在(参数:针,目标字符串)。在 Unicode 不区分大小写的模式下搜索。
示例: {"sub": ["word", {"cx":""}]}
含义: mb_stripos($context, "word") !== false
re - 字符串与正则表达式匹配(参数:正则表达式,文本)。
示例: {"re": ["/^\w+$/u", {"cx":""}]}
含义: preg_match('/^\w+$/u', $context)
spf - 格式化字符串(参数:格式字符串,零个或多个值)。
示例: {"spf": ["%.2f", {"cx":""}]}
含义: sprintf('%.2f', $context)
内联操作符
_ (下划线) - 直接使用参数值(任何类型的参数)。
示例: {"_": true}, {"_": ["a": "b"]}, {"_": [1, {"_": []}, 3]}
含义: true, ['a' => 'b'], [1, [], 3]
原生函数调用
fn - 使用提供的参数调用原生函数(参数:函数名,参数1,...)。
示例: {"fn": ["Currencies::getRateForCountry", {"cx":"country"}]}
含义: Currencies::getRateForCountry($context['country'])
代码示例
示例1
规则文件
{
"gt": [{"cx":""}, 10]
}
生成的代码
<?php class Ruleset { public function evaluate($context) { // return on success $success = true; // rules if ($context > 10) { return $success; } // not found return false; } }
以下示例中生成的代码将创建在 Ruleset::evaluate() 方法内部,就像上面的示例一样,所以只展示相应的 // rules 部分。
示例2
规则文件
{
"and": [
{"ge": [{"cx":""}, 0]},
{"le": [{"cx":""}, 100]}
]
}
生成的代码
// rules if ( ($context >= 0) and ($context <= 100) ) { return $success; }
示例3
规则文件
{
"or": [
{
"and": [
{"eq": [{"cx":"country"}, "US"]},
{"eq": [{"cx":"currency"}, "USD"]}
],
"return": "North America"
},
{
"and": [
{"in": [{"cx":"country"}, {"_": ["DE", "ES", "FR", "IT"]}]},
{"eq": [{"cx":"currency"}, "EUR"]}
],
"return": "Europe"
}
]
}
生成的代码
// rules if ( ( ( ($context['country'] ?? null) === "US" and ($context['currency'] ?? null) === "USD" ) and is_string($success = "North America") ) or ( ( in_array($context['country'] ?? null, ["DE", "ES", "FR", "IT"]) and ($context['currency'] ?? null) === "EUR" ) and is_string($success = "Europe") ) ) { return $success; }
注意 在 php-5 兼容模式下, ($context['currency'] ?? null) 和类似的构造将替换为 isset($context['currency']) and $context['currency'] 构造。