We recently adopted the Symfony Expression Language in the rules engine at OpenSky. It has brought a new level of flexibility to our system and creating new logic has never been easier.

Installing the expression language in your application is easy with composer. Just add the following to your composer.json:

"symfony/expression-language": "2.5.*@dev"

The expression language allows you to perform expressions that get evaluated with raw PHP code and return a single value. It can be any type of value and is not limited to boolean values. Here is a simple example:

use Symfony\Component\ExpressionLanguage\ExpressionLanguage;

$language = new ExpressionLanguage();

$expression = 'user["isActive"] == true and product["price"] > 20';
$context = array(
    'user' => array(
        'isActive' => true
    ),
    'product' => array(
        'price' => 30
    ),
);

$return = $language->evaluate($expression, $context);

var_export($return); // true

That is a very simple example on how to use the raw expression language. Now I will try to demonstrate how you could model a real implementation using Doctrine to persist your rules to a database, the Symfony Event Dispatcher to evaluate your rules and execute actions when your expressions evaluate to true.

To get started create a new Rule class and map it to a database using one of the Doctrine object mappers. For this example we will map it using the MongoDB ODM:

use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;

/**
 * @ODM\Document
 */
class Rule
{
    /**
     * @ODM\Id
     */
    private $id;

    /**
     * @ODM\Collection
     */
    private $eventNames = array();

    /**
     * @ODM\String
     */
    private $expression;

    /**
     * @ODM\Collection
     */
    private $actionEvents = array();

    // ...
}

Now imagine you already have an event named user.add_to_cart being notified in your application. It looks something like this:

use Symfony\Component\EventDispatcher\Event;

class UserAddToCartEvent extends Event
{
    const onUserAddToCart = 'user.add_to_cart';

    private $user;
    private $product;

    // ...
}

class AddToCartController
{
    // ...

    public function addToCartAction($productId)
    {
        // ...

        $this->dispatcher->dispatch(UserAddToCartEvent::onUserAddToCart, new UserAddToCartEvent($user, $product));
    }
}

Say you want to give a reward to users who add items to their cart when they have loved more than 20 items and the price of the product is greater than 50 dollars. The Rule model we created earlier allows us to define a rule that will be executed when UserAddToCart::onUserAddToCart is dispatched:

$rule = new Rule();

// set the events this rule should be executed on.
$rule->setEventNames(array(
    UserAddToCartEvent::onUserAddToCart
));

// set the expression to evaluate when the rule is executed.
// if the user has loved more than 20 items and the price of the product is more than 50 dollars.
// the expression string will be evaluated by the Symfony expression language.
$rule->setExpression('event.getUser().getNumLoves() > 20 and event.getProduct().getPrice() > 50');

// set the action events to dispatch when the expression evaluates to true.
$rule->setActionEvents(array(
    array(
        'eventName' => UserCreditRewardEvent::onUserCreditReward,
        'recipientExpression' => 'event.getUser()',
        'attributes' => array(
            'amount' => 50
        ),
    )
));

$dm->persist($rule);
$dm->flush();

The above example assumes you have a UserCreditRewardEvent setup and a listener setup to process the event to give the user a credit. Here is what the event would look like:

use Symfony\Component\EventDispatcher\Event;

class UserCreditRewardEvent extends Event
{
    const onUserCreditReward = 'user.credit_reward';

    private $user;
    private $amount;

    // ...
}

And here is what the listener would look like to give the user the credit. This example assumes you already have a CreditManager service with an issueCredit() method you can use to give a user a credit for a dollar amount:

class UserCreditRewardListener
{
    private $creditManager;

    // ...

    public function onUserCreditReward(UserCreditRewardEvent $event)
    {
        $this->creditManager->issueCredit(
            $event->getUser(),
            $event->getAmount()
        );
    }
}

Now to bring it all together we need a RuleSubcriber to lookup the Rule objects from the database when events occur in our application. This class will evaluate the rules and then dispatch the resulting action events when the expressions return true.

use Symfony\Component\EventDispatcher\Event;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class RuleSubcriber implements EventSubscriberInterface
{
    private $dm;
    private $expressionLanguage;
    private $actionEventFactory;

    // ...

    public static function getSubscribedEvents()
    {
        return array(
            UserAddToCartEvent::onUserAddToCart => array('handleEvent', 0),
        );
    }

    public function handleEvent(Event $event)
    {
        $rules = $this->findRulesByEventName($event->getName());

        foreach ($rules as $rule) {
            if ($this->evaluateRule($rule, $event)) {
                $this->dispatchActionEvents($rule, $event);
            }
        }
    }

    private function findRulesByEventName($eventName)
    {
        return $this->dm->createQueryBuilder()
            ->field('eventNames')->equals($eventName)
            ->getQuery()
            ->execute();
    }

    private function evaluateRule(Rule $rule, Event $event)
    {
        return $this->expressionLanguage->evaluate($rule->getExpression(), array(
            'event' => $event,
        ));
    }

    private function dispatchActionEvents(Rule $rule, Event $event)
    {
        foreach ($rule->getActionEvents() as $action) {
            $this->dispatchActionEvent($action, $rule, $event);
        }
    }

    private function dispatchActionEvent(array $action, Rule $rule, Event $event)
    {
        $recipientUser = $this->expressionLanguage($action['recipientExpression'], array(
            'event' => $event,
        ));

        $actionEvent = $this->actionEventFactory->createActionEvent(
            $action,
            $recipientUser,
            $rule
        );

        $this->dispatcher->dispatch($action['eventName'], $actionEvent);
    }
}

The ActionEventFactory used in the above RuleSubcriber is a simple service used to create the action events we dispatch for our rules.

class ActionEventFactory
{
    public function createActionEvent(array $action, User $user, Rule $rule)
    {
        switch ($action['eventName']) {
            // ...

            case UserCreditRewardEvent::onUserCreditReward:
                return new UserCreditRewardEvent($user, $action['attributes']['amount']);
        }
    }
}

That is it! Now you have the ability to define rules that can be created with a user interface in your application and stored in a database. These rules get evaluated when certain events are dispatched within your application. When those rules evaluate to true you can dispatch other events that can give out credits, give free shipping, send e-mails, or do anything you can possibly imagine. Build up a repository of common actions as events and allow your business people to define new rules and rewards for promotional campaigns without having to involve a software engineer.