A rules engine is a piece of software in which you can define business rules on runtime. This is very convenient for, for example, discount codes. This way you do not have to create a bunch of if-statements, but you can dynamically add them to a collection, and the rules engine will do the validation for you.

In this post, I am going to show how you can implement a simple rules engine.

What is a rules engine?

A rules engine usually works with rules and facts. A rule is a piece of business information for example “x has to be larger than 3”. A ‘fact’ is the data you want to check your rules against, this can be anything, a product, a user, you name it.

The Problem

Let’s say we have a webshop, in this webshop we have the following rules for applying a discount to the cart:

  1. If 3 or more are in the cart.
  2. If the total amount in the cart is more than €50,-.
  3. If a product called “Cheese” is in the cart.

When any of these rules are true, then a discount will be applied. You could do that by using some if-statements, but that can get out of hand quite fast. A solution to this problem could be to implement a rules engine.

The implementation

Our rules engine will work with callables this means you can use anonymous functions, arrow-functions, or invokable classes. Every rule will check a fact on its own. And if one fact passes the validateAny method will return true if none passes it will return false

final class RuleEngine 
{
    private array $rules;
    
    public function addRule(callable $rule): void
    {
        $this->rules[] = $rule;
    }
    
    public function validateAny($fact): bool
    {
        foreach ($this->rules as $rule) {
            if ($rule($fact)) {
                return true;
            }
        }    
        return false;
    }
}

Example

In our webshop, we need to have a cart and a product.

final class Product 
{
    public function __construct(
        public string $name,
        public int $price,
    ) {}
    
}

final class Cart 
{
    public array $products;
    
    public function getTotal(): int
    {
        return array_sum($this->products);
    }
}

We have to configure our rules engine we can easily do this by adding new rules via the addRule method.

$rulesEngine = new RulesEngine();
$rulesEngine->addRule(fn($fact) => count($fact->products) > 3);
$rulesEngine->addRule(fn($fact) => $fact->getTotal() > 50);
$rulesEngine->addRule(fn($fact) => !empty(array_filter($fact->products, fn($product) => $product->name === "Cheese")));

After we’ve added the rules to the engine we can call the validateAny method with a cart as fact. So let’s create a cart and check if the cart can retrieve a discount.

$cart = new Cart();
$cart->products = [
    new Product("Cheese", 10),
    new Product("Chips", 25),
];

var_dump($rulesEngine->validateAny($cart));

Sometimes we want to check if a fact passes all the rules instead of one of the rules in our rules engine. We can easily implement a validateAll method, this method will return false as soon a rule fails to pass.

public function validateAll($fact): bool {
    foreach ($this->rules as $rule) {
        if (!$rule($fact)) {
            return false;
        }
    }
    
    return true;
}

Conclusion

A rules engine can be quite a powerful tool to make your application more flexible and easier customizable.

This simple rules engine can be extended by creating rules with invokable classes which implement more complex logic. Or you can add Symfony’s ExpressionLanguage Component to a rule, as I did in my example project, this way you could write the business rules in a human-readable language.

You could even create a rule that implements another rules engine, so you can have rules like:

  1. the cart has to have 3 or more products and a total higher than 50.
  2. or the customer has already ordered 5 times.

For more information about a rules engine, I refer to the article made by Martin Fowler about RulesEngines