A rule engine following the condition -> action paradigm
A rule is build up by three elements:
Every rule has a boolean condition. If the condition is satisfied (i.e, its value is not false) then the rule is processed.
Every rule has some associated actions. The actions could be performed at different rule's stages:
- Before the rule's condition being evaluated (regardless being satisfied or not)
- After its condition has been satisfied but before its subrules have been executed
- After both its condition has been satisfied and its subrules have been executed.
- Before leave the rule's, regardless the condition has been satisfied or not.
Each rule could contain subrules. Each subrule is executed only if the parent rule's condition has been satisfied.
When executes, the flow a rule follows could be described as:
Perform pre-rule actions
If the rule's condition is satisfied then
Perform pre-subrules actions
Execute subrules
Perform post-subrules actions
Perform post-rule actions
$ctx = new Context();
$ctx['name'] = 'Philip J.';
$ctx['surname'] = 'Fry';
// If the context's name is 'Philip J.' AND the context's surname is 'Fry'...
$condition = new AndOp(
new EqualTo('{{ name }}', 'Philip J.'),
new EqualTo('{{ surname }}', 'Fry')
);
// Then print the message
$action = new RunCallback(function (Context $c) {
echo 'Hello, my name is ' . $c['name'] . $c['surname'];
});
$rule = new Rule();
$rule
->setCondition($condition)
->setAction($action)
->execute($ctx);
// Hello, my name is Philip J. Fry
Conditions use boolean expressions to check whether the rule must be processed.
By default, a rule has a true
as a condition.
There are four predefined primitives:
- String
- Integer
- Float
- Boolean
Operators take one, two or more values (primitives or other operator's output) and return some other value
AndOp()
operator takes one or more arguments and returns true
if all of its arguments are evaluated as true
:
$and = new AndOp(true, false, false, true);
echo $and->evaluate(); // <-- false
OrOp()
operator takes one or more arguments and returns true
if any of its arguments are evaluated as true
:
$or = new OrOp(true, false, false, true);
echo $or->evaluate(); // <-- true
NotOp()
operator takes one or more arguments and returns true
if all of its arguments are evaluated as false
.
This is indeed a Nand operation.
$not = new Not(true, false, false, true);
echo $not->evaluate(); // <-- false
EqualTo()
operator takes two or more arguments and returns true
if all of its arguments are the same:
$equalTo = new EqualTo("foo", "bar", "baz");
echo $equalTo->evaluate(); // <-- false
EqualTo()
operator takes two or more arguments and returns true
if any of its arguments is different from aonther one:
$notEqualTo = new EqualTo("foo", "bar", "baz");
echo $notEqualTo->evaluate(); // <-- true
This action updates/extends the current context:
$ctx = new Context();
$ctx['name'] = 'Philip J.';
$overrideCtx['surname'] = 'Fry';
$action = new OverrideContext($overrideCtx);
$action->perform($ctx);
var_dump($ctx);
// array(
// 'name' => 'Philip J.',
// 'surname' => 'Fry'
// );
This action allows to render the current context as a template by feeding itself as the variable replacements:
$ctx = new Context();
$ctx['name'] = 'Philip J.';
$ctx['surname'] = 'Fry';
$ctx['fullname'] = '{{ name }} {{ surname }}';
$action = new InterpolateContext();
$action->perform($ctx);
var_dump($ctx);
// array(
// 'name' => 'Philip J.',
// 'surname' => 'Fry'
// 'fullname' => 'Philip J. Fry'
// );
This action filter (either by preserving or discarding) entries in a context:
$ctx = new Context();
$ctx['name'] = 'Philip J.';
$ctx['surname'] = 'Fry';
$ctx['fullname'] = 'Philip J. Fry';
$action = new FilterContext();
$action
->setKeys('fullname')
->setMode(FilterContext::ALLOW_KEYS)
->perform($ctx);
var_dump($ctx);
// array(
// 'fullname' => 'Philip J. Fry'
// );
This actions calls a custom user function passing the current context as argument:
$ctx = new Context();
$ctx['name'] = 'Philip J.';
$ctx['surname'] = 'Fry';
$callback = function(Context $ctx) {
$ctx['fullname'] = $ctx['name'] . ' ' . $ctx['surname'];
unset($ctx['surname'];
}
$action = new RunCallback($callback);
$action->perform($ctx);
var_dump($ctx);
// array(
// 'name' => 'Philip J.',
// 'fullname' => 'Philip J. Fry'
// );
This actions does nothing :)
$ctx = new Context();
$ctx['name'] = 'Philip J.';
$ctx['surname'] = 'Fry';
$action = new NoAction($callback);
$action->perform($ctx);
var_dump($ctx);
// array(
// 'name' => 'Philip J.',
// 'surname' => 'Fry'
// );
This actions allows to define a list of actions to be performed sequentially:
$ctx = new Context();
$ctx['fullname'] = '{{ name }} {{ surname }}';
$override = new OverrideContext(new Context(array('name' => 'Philip J.'))); // Adds name
$callback = new RunCallback(function (Context $c) { $c['surname'] = 'Fry'; } ); // Adds surname
$interpolate = new InterpolateContext(); // Interpolates fullname
$filter = new FilterContext(FilterContext::ALLOW_KEYS, 'fullname'); // Filters all except fullname
$sequence = new Sequence($override, $callback);
$sequence->appendAction($interpolate)->appendAction($filter);
$sequence->perform($ctx);
var_dump($ctx);
// array(
// 'fullname' => 'Philip J. Fry'
// );