ruler / ruler
一个简单的无状态的PHP生产规则引擎。
Requires
- php: >=7.4
Requires (Dev)
- phpunit/phpunit: ^8.5.12 | ^9
README
Ruler是一个为PHP 5.3+设计的简单无状态生产规则引擎。
Ruler拥有一个简单直观的领域特定语言(DSL)
... 由RuleBuilder提供
$rb = new RuleBuilder; $rule = $rb->create( $rb->logicalAnd( $rb['minNumPeople']->lessThanOrEqualTo($rb['actualNumPeople']), $rb['maxNumPeople']->greaterThanOrEqualTo($rb['actualNumPeople']) ), function() { echo 'YAY!'; } ); $context = new Context([ 'minNumPeople' => 5, 'maxNumPeople' => 25, 'actualNumPeople' => fn() => 6, ]); $rule->execute($context); // "Yay!"
当然,如果你不喜欢简洁性
... 也可以不使用RuleBuilder来使用它
$actualNumPeople = new Variable('actualNumPeople'); $rule = new Rule( new Operator\LogicalAnd([ new Operator\LessThanOrEqualTo(new Variable('minNumPeople'), $actualNumPeople), new Operator\GreaterThanOrEqualTo(new Variable('maxNumPeople'), $actualNumPeople) ]), function() { echo 'YAY!'; } ); $context = new Context([ 'minNumPeople' => 5, 'maxNumPeople' => 25, 'actualNumPeople' => fn() => 6, ]); $rule->execute($context); // "Yay!"
但这听起来并不是很有趣,对吧?
你可以用Ruler做些什么
比较事物
// These are Variables. They'll be replaced by terminal values during Rule evaluation. $a = $rb['a']; $b = $rb['b']; // Here are bunch of Propositions. They're not too useful by themselves, but they // are the building blocks of Rules, so you'll need 'em in a bit. $a->greaterThan($b); // true if $a > $b $a->greaterThanOrEqualTo($b); // true if $a >= $b $a->lessThan($b); // true if $a < $b $a->lessThanOrEqualTo($b); // true if $a <= $b $a->equalTo($b); // true if $a == $b $a->notEqualTo($b); // true if $a != $b $a->stringContains($b); // true if strpos($b, $a) !== false $a->stringDoesNotContain($b); // true if strpos($b, $a) === false $a->stringContainsInsensitive($b); // true if stripos($b, $a) !== false $a->stringDoesNotContainInsensitive($b); // true if stripos($b, $a) === false $a->startsWith($b); // true if strpos($b, $a) === 0 $a->startsWithInsensitive($b); // true if stripos($b, $a) === 0 $a->endsWith($b); // true if strpos($b, $a) === len($a) - len($b) $a->endsWithInsensitive($b); // true if stripos($b, $a) === len($a) - len($b) $a->sameAs($b); // true if $a === $b $a->notSameAs($b); // true if $a !== $b
进行数学运算
$c = $rb['c']; $d = $rb['d']; // Mathematical operators are a bit different. They're not Propositions, so // they don't belong in rules all by themselves, but they can be combined // with Propositions for great justice. $rb['price'] ->add($rb['shipping']) ->greaterThanOrEqualTo(50) // Of course, there are more. $c->add($d); // $c + $d $c->subtract($d); // $c - $d $c->multiply($d); // $c * $d $c->divide($d); // $c / $d $c->modulo($d); // $c % $d $c->exponentiate($d); // $c ** $d $c->negate(); // -$c $c->ceil(); // ceil($c) $c->floor(); // floor($c)
推理集合
$e = $rb['e']; // These should both be arrays $f = $rb['f']; // Manipulate sets with set operators $e->union($f); $e->intersect($f); $e->complement($f); $e->symmetricDifference($f); $e->min(); $e->max(); // And use set Propositions to include them in Rules. $e->containsSubset($f); $e->doesNotContainSubset($f); $e->setContains($a); $e->setDoesNotContain($a);
组合规则
// Create a Rule with an $a == $b condition $aEqualsB = $rb->create($a->equalTo($b)); // Create another Rule with an $a != $b condition $aDoesNotEqualB = $rb->create($a->notEqualTo($b)); // Now combine them for a tautology! // (Because Rules are also Propositions, they can be combined to make MEGARULES) $eitherOne = $rb->create($rb->logicalOr($aEqualsB, $aDoesNotEqualB)); // Just to mix things up, we'll populate our evaluation context with completely // random values... $context = new Context([ 'a' => rand(), 'b' => rand(), ]); // Hint: this is always true! $eitherOne->evaluate($context);
组合更多规则
$rb->logicalNot($aEqualsB); // The same as $aDoesNotEqualB :) $rb->logicalAnd($aEqualsB, $aDoesNotEqualB); // True if both conditions are true $rb->logicalOr($aEqualsB, $aDoesNotEqualB); // True if either condition is true $rb->logicalXor($aEqualsB, $aDoesNotEqualB); // True if only one condition is true
evaluate
和 execute
规则
使用上下文 evaluate()
规则以确定其是否为真。
$context = new Context([ 'userName' => fn() => $_SESSION['userName'] ?? null, ]); $userIsLoggedIn = $rb->create($rb['userName']->notEqualTo(null)); if ($userIsLoggedIn->evaluate($context)) { // Do something special for logged in users! }
如果规则有一个操作,你可以直接 execute()
它,并节省几行代码。
$hiJustin = $rb->create( $rb['userName']->equalTo('bobthecow'), function() { echo "Hi, Justin!"; } ); $hiJustin->execute($context); // "Hi, Justin!"
一次执行多个规则
$hiJon = $rb->create( $rb['userName']->equalTo('jwage'), function() { echo "Hey there Jon!"; } ); $hiEveryoneElse = $rb->create( $rb->logicalAnd( $rb->logicalNot($rb->logicalOr($hiJustin, $hiJon)), // The user is neither Justin nor Jon $userIsLoggedIn // ... but a user nonetheless ), function() use ($context) { echo sprintf("Hello, %s", $context['userName']); } ); $rules = new RuleSet([$hiJustin, $hiJon, $hiEveryoneElse]); // Let's add one more Rule, so non-authenticated users have a chance to log in $redirectForAuthentication = $rb->create($rb->logicalNot($userIsLoggedIn), function() { header('Location: /login'); exit; }); $rules->addRule($redirectForAuthentication); // Now execute() all true Rules. // // Astute readers will note that the Rules we defined are mutually exclusive, so // at most one of them will evaluate to true and execute an action... $rules->executeRules($context);
动态填充评估上下文
我们上面的几个例子使用了上下文变量的静态值。这对于示例来说是好的,但在现实世界中并不那么有用。你可能会想根据所有 sorts of things 来评估规则...
你可以将上下文视为规则评估的ViewModel。你提供静态值,甚至为你的规则所需的变量提供懒加载的代码。
$context = new Context; // Some static values... $context['reallyAnnoyingUsers'] = ['bobthecow', 'jwage']; // You'll remember this one from before $context['userName'] = fn() => $_SESSION['userName'] ?? null; // Let's pretend you have an EntityManager named `$em`... $context['user'] = function() use ($em, $context) { if ($userName = $context['userName']) { return $em->getRepository('Users')->findByUserName($userName); } }; $context['orderCount'] = function() use ($em, $context) { if ($user = $context['user']) { return $em->getRepository('Orders')->findByUser($user)->count(); } return 0; };
现在你有了所有你需要的信息来根据订单数量或当前用户来创建规则,或者任何其他疯狂的事情。我不知道,这可能是一个运费计算器?
如果当前用户下了5个或更多的订单,但不是“真正烦人”,就给他们免费送货。
$rb->create( $rb->logicalAnd( $rb['orderCount']->greaterThanOrEqualTo(5), $rb['reallyAnnoyingUsers']->doesNotContain($rb['userName']) ), function() use ($shipManager, $context) { $shipManager->giveFreeShippingTo($context['user']); } );
访问变量属性
作为额外的奖励,Ruler让你可以访问上下文变量值上的属性、方法和偏移量。这真的很方便。
比如说,我们想要记录当前用户的姓名,如果他们是管理员
// Reusing our $context from the last example... // We'll define a few context variables for determining what roles a user has, // and their full name: $context['userRoles'] = function() use ($em, $context) { if ($user = $context['user']) { return $user->roles(); } else { // return a default "anonymous" role if there is no current user return ['anonymous']; } }; $context['userFullName'] = function() use ($em, $context) { if ($user = $context['user']) { return $user->fullName; } }; // Now we'll create a rule to write the log message $rb->create( $rb->logicalAnd( $userIsLoggedIn, $rb['userRoles']->contains('admin') ), function() use ($context, $logger) { $logger->info(sprintf("Admin user %s did a thing!", $context['userFullName'])); } );
这有点多。我们不需要为规则中可能需要访问的所有内容创建上下文变量,我们可以使用VariableProperties,以及它们方便的RuleBuilder接口
// We can skip over the Context Variable building above. We'll simply set our, // default roles on the VariableProperty itself, then go right to writing rules: $rb['user']['roles'] = ['anonymous']; $rb->create( $rb->logicalAnd( $userIsLoggedIn, $rb['user']['roles']->contains('admin') ), function() use ($context, $logger) { $logger->info(sprintf("Admin user %s did a thing!", $context['user']['fullName']); } );
如果父变量解析为一个对象,并且这个VariableProperty名称是“bar”,它将执行优先级查找
- 名为
bar
的方法 - 名为
bar
的公共属性 - ArrayAccess + 偏移量存在名为
bar
如果变量解析为一个数组,它将返回
- 数组索引
bar
如果上述任何一项都不为真,它将返回此VariableProperty的默认值。
添加自己的运算符
如果你需要的默认Ruler运算符不适合你的需求,你可以编写自己的!只需像这样定义额外的运算符
namespace My\Ruler\Operators; use Ruler\Context; use Ruler\Operator\VariableOperator; use Ruler\Proposition; use Ruler\Value; class ALotGreaterThan extends VariableOperator implements Proposition { public function evaluate(Context $context): bool { list($left, $right) = $this->getOperands(); $value = $right->prepareValue($context)->getValue() * 10; return $left->prepareValue($context)->greaterThan(new Value($value)); } protected function getOperandCardinality() { return static::BINARY; } }
然后你可以像这样使用它们与RuleBuilder一起使用
$rb->registerOperatorNamespace('My\Ruler\Operators'); $rb->create( $rb['a']->aLotGreaterThan(10); );
但不仅如此...
查看测试套件以获取更多示例(以及一些热门的CS 320组合逻辑操作)。
Ruler是管道。带你的瓷器来。
Ruler不会关心规则从哪里来。也许你有一个围绕ORM或ODM的RuleManager。也许你编写了一个简单的DSL并解析静态文件。
无论您选择何种口味,Ruler都会处理逻辑。