vertilia/recif

规则引擎的代码生成器

v2.5.1 2023-05-16 16:09 UTC

This package is auto-updated.

Last update: 2024-09-16 19:08:37 UTC


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

复杂条件:上下文是一个包含countrycurrency条目的关联数组,必须与以下矩阵匹配

如果条件匹配,它将返回上下文的大陆名称(北美洲欧洲)而不是简单的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'] 构造。