Ruler: A simple stateless production rules engine for PHP 5.3+

What is ruler?

Ruler is a simple stateless production rules engine for PHP 5.3+ written by Justin Hileman (@bobthecow). Justin was previously employed at OpenSky but these days you will find him hacking on a new startup named @presentate.

What is a rules engine?

From martinfowler.com:

A rules engine is all about providing an alternative computational model. Instead of the usual imperative model, commands in sequence with conditionals and loops, it provides a list of production rules. Each rule has a condition and an action - simplistically you can think of it as a bunch of if-then statements.

From wikipedia:

A business rules engine is a software system that executes one or more business rules in a runtime production environment. The rules might come from legal regulation (“An employee can be fired for any reason or no reason but not for an illegal reason”), company policy (“All customers that spend more than $100 at one time will receive a 10% discount”), or other sources. A business rule system enables these company policies and other operational decisions to be defined, tested, executed and maintained separately from application code.

What does Ruler usage look like?

Ruler has a nice and convenient DSL that is provided by RuleBuilder:

$rb = new RuleBuilder;
$rule = $rb->create(
    $rb->logicalAnd(
        $rb['minAge']->greaterThan($rb['age']),
        $rb['maxAge']->lessThan($rb['age'])
    ),
    function() {
        echo 'Congratulations! You are between the ages of 18 and 25!';
    }
);

$context = new Context(array(
    'minAge' => 18,
    'maxAge' => 25,
    'age' => function() {
        return 20;
    },
));

$rule->execute($context); // "Congratulations! You are between the ages of 18 and 25!"

The full API is quite simple:

// 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

You can combine things to create more complex rules:

// 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(array(
    'a' => rand(),
    'b' => rand(),
));

// Hint: this is always true!
$eitherOne->evaluate($context);

More complex examples:

$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

Full Examples

Check if user is logged in:

$context = new Context(array('username', function() {
    return isset($_SESSION['username']) ? $_SESSION['username'] : null;
}));

$userIsLoggedIn = $rb->create($rb['username']->notEqualTo(null));

if ($userIsLoggedIn->evaluate($context)) {
    // Do something special for logged in users!
}

If a Rule has an action, you can execute() it directly and save yourself a couple of lines of code.

$hiJustin = $rb->create(
    $rb['userName']->equalTo('bobthecow'),
    function() {
        echo "Hi, Justin!";
    }
);

$hiJustin->execute($context);  // "Hi, Justin!"

What does OpenSky use Ruler for?

OpenSky makes heavy use of Ruler. Below is a list of some of the conditions we have available in our application:

  • Joins OpenSky

    • Is Facebook Connected
    • Number of friends is >= n
    • Number of friends is <= n
    • With certain origination parameters existing in URL
  • Makes a Purchase

    • Within x days of joining
    • Is first purchase
    • Order amount is >= n
  • Loves an offer

    • Is first love of the day
  • Visits OpenSky

    • Is Facebook Connected
    • Number of friends is >= n
    • Number of friends is <= n
    • Users points are >= n

These are just some of the conditions we have available. Our application is setup in a way that we can easily create new rules via a backend GUI. We can mix and match conditions and rewards. Some of the rewards we have available are:

  • Issue n points
  • New member level
  • Credit
  • Free shipping

The benefit of this abstract setup is it allows us to combine different conditions, tweak the parameters of the conditions and issue rewards depending on the outcome of the condition all without requiring code changes and a deploy. You can imagine our business and marketing teams love this because they can change things all day long and without having to bother the tech team.