ruler

repository·main·Indexed 22 days ago

https://github.com/bobthecow/ruler

A simple, stateless production rules engine for PHP 5.3+ featuring a fluent DSL for defining logical, mathematical, and set-based rules. It includes a RuleBuilder for constructing complex propositions, a Context class for managing facts with support for lazy evaluation, and the ability to define custom operators.

Tokens
7.3K
Snippets
25
Records
33
Agent score
77%

What's inside ruler

  1. Combine multiple rules using logical operators

    main

    Rules are also propositions, meaning you can combine them using logical operators to create complex logic (often called "MEGARULES").

    Logical Operators:

    • logicalNot($rule): Negates a rule.
    • logicalAnd($rule1, $rule2, ...): True if all rules are true.
    • logicalOr($rule1, $rule2, ...): True if any rule is true.
    • logicalXor($rule1, $rule2): True if exactly one rule is true.
  2. Access properties and methods on Context variables

    main

    Ruler allows you to access properties, methods, and offsets of context variables directly within the RuleBuilder syntax using VariableProperties.

    If a variable resolves to an object, $rb['var']['prop'] performs a prioritized lookup:

    1. A method named prop
    2. A public property named prop
    3. ArrayAccess offset prop

    If the variable is an array, it returns the array index prop.

    You can also define default values for these properties on the RuleBuilder itself.

    // Set a default value for a property
    $rb['user']['roles'] = ['anonymous'];
    
    // Use it in a rule
    $rb->create(
        $rb->logicalAnd(
            $userIsLoggedIn,
            $rb['user']['roles']->contains('admin')
        ),
        function() use ($context, $logger) {
            $logger->info("Admin user {$context['user']['fullName']} did a thing!");
        }
    );
  3. Create rules using the RuleBuilder DSL

    main

    The RuleBuilder provides a fluent Domain Specific Language (DSL) to construct complex rules easily. You can create rules by combining logical operators (like logicalAnd, logicalOr) and propositions (comparisons) on variables. A rule can optionally include an action (a callback function) that executes if the rule evaluates to true.

    Variables are accessed via the RuleBuilder using array access (e.g., $rb['variableName']).

    $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!"
  4. Configure the evaluation Context

    main

    The Context acts as a ViewModel for rule evaluation. You can provide static values or closures for lazy evaluation. Closures allow you to fetch data from external sources (like databases or sessions) only when a rule actually needs that specific variable.

    Variables in the context can be accessed by name.

    $context = new Context;
    
    // Static value
    $context['reallyAnnoyingUsers'] = ['bobthecow', 'jwage'];
    
    // Lazy evaluation via closure
    $context['userName'] = fn() => $_SESSION['userName'] ?? null;
    
    $context['user'] = function() use ($em, $context) {
        if ($userName = $context['userName']) {
            return $em->getRepository('Users')->findByUserName($userName);
        }
        return null;
    };
  5. Use the RuleBuilder Variable for fluent rule construction

    main

    The Ruler\RuleBuilder\Variable class is a specialized version of a base variable designed for use within the RuleBuilder DSL. It acts as a placeholder in propositions and comparison operators. During evaluation, these variables are replaced with terminal values from either the variable's default value or the current evaluation Context.

    Key features include:

    • Fluent Interface: Allows for chaining operators (math, comparison, set operations) without manual object instantiation.
    • Variable Properties: Supports accessing nested properties, indexes, or methods of a variable using array-like syntax.
    • Dynamic Operators: Supports custom operators registered via the RuleBuilder through magic method calls.
    // Example of using the fluent interface to build a rule
    $variable->greaterThan(10)->equalTo($otherVariable);
  6. Use the Context class to manage evaluation facts

    main

    The Ruler\Context class acts as a container for the facts (variables) used to evaluate Rules or Propositions. It supports lazy evaluation, allowing you to define facts as callables (Closures or invokable objects) that are only resolved when accessed.

    Key behaviors:

    • Lazy Evaluation: If a value is a callable, accessing it via array syntax ($context['name']) will execute the callable and return the result.
    • Shared Facts: Using share() ensures a lazy fact is evaluated only once and the result is cached (frozen) for the lifetime of the Context instance.
    • Protected Callables: If you want to store a callable as a literal value rather than having it executed as a lazy fact, use protect().
    • Array Access: The class implements ArrayAccess, so you can interact with it using standard PHP array syntax.
    use Ruler\Context;
    
    // Initialize with existing facts
    $context = new Context(['user_id' => 42]);
    
    // Define a lazy fact using a Closure
    $context['is_admin'] = function (Context $ctx) {
        return $ctx['user_id'] === 1;
    };
    
    // Accessing the fact triggers evaluation
    if ($context['is_admin']) {
        // ...
    }
  7. Evaluate and Execute rules

    main

    There are two primary ways to interact with a Rule:

    1. evaluate(Context $context): bool: Determines if the rule's condition is met. Use this when you only need to check a condition without triggering an action.
    2. execute(Context $context): void: Evaluates the rule and, if the condition is met, executes the associated action (callback).

    To manage and execute a collection of rules, use a RuleSet.

    // Evaluate
    if ($userIsLoggedIn->evaluate($context)) {
        // logic
    }
    
    // Execute
    $hiJustin->execute($context);
    
    // Execute a set
    $rules = new RuleSet([$rule1, $rule2]);
    $rules->executeRules($context);
  8. Create custom Operators

    main

    You can extend Ruler by implementing your own operators. To do this, create a class that extends VariableOperator and implements the Proposition interface.

    After defining your operator, register its namespace with the RuleBuilder to make it available in the DSL.

    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;
        }
    }
    
    // Usage:
    $rb->registerOperatorNamespace('My\Ruler\Operators');
    $rb->create($rb['a']->aLotGreaterThan(10));
  9. Compare values with Propositions

    main

    Propositions are the building blocks of rules used to compare variables. When using the RuleBuilder, you can call these methods on a variable to create a comparison.

    Comparison Operators:

    • greaterThan($b): $a > $b
    • greaterThanOrEqualTo($b): $a >= $b
    • lessThan($b): $a < $b
    • lessThanOrEqualTo($b): $a <= $b
    • equalTo($b): $a == $b
    • notEqualTo($b): $a != $b
    • sameAs($b): $a === $b
    • notSameAs($b): $a !== $b

    String Operators:

    • stringContains($b): true if strpos($b, $a) is not false
    • stringDoesNotContain($b): true if strpos($b, $a) is false
    • stringContainsInsensitive($b): true if stripos($b, $a) is not false
    • stringDoesNotContainInsensitive($b): true if stripos($b, $a) is false
    • startsWith($b): true if strpos($b, $a) === 0
    • startsWithInsensitive($b): true if stripos($b, $a) === 0
    • endsWith($b): true if strpos($b, $a) === len($a) - len($b)
    • endsWithInsensitive($b): true if stripos($b, $a) === len($a) - len($b)
  10. Perform mathematical operations in rules

    main

    Mathematical operators allow you to manipulate values before comparing them with a proposition. These are not propositions themselves but return a value that can be used in a comparison.

    Arithmetic Operators:

    • add($other): addition
    • subtract($other): subtraction
    • multiply($other): multiplication
    • divide($other): division
    • modulo($other): modulo
    • exponentiate($other): exponentiation
    • negate(): unary negation
    • ceil(): ceiling
    • floor(): floor
    $rb['price']
      ->add($rb['shipping'])
      ->greaterThanOrEqualTo(50)
  11. Reason about sets with set operators

    main

    If variables resolve to arrays, you can use set operators to manipulate them or use set propositions to include them in rules.

    Set Manipulation:

    • union($other)
    • intersect($other)
    • complement($other)
    • symmetricDifference($other)
    • min()
    • max()

    Set Propositions:

    • containsSubset($other)
    • doesNotContainSubset($other)
    • setContains($item)
    • setDoesNotContain($item)
  12. Register and find operator namespaces

    main

    You can extend the DSL by registering custom operator namespaces. This allows the RuleBuilder to locate and instantiate custom operator classes.

    1. Register: Use registerOperatorNamespace(string $namespace) to add a namespace (e.g., "MyCustomNamespace"). Note that namespaces are typically case-sensitive depending on your filesystem.
    2. Find: Use findOperator(string $name) to resolve an operator name to its fully qualified class name. The builder will attempt to find a class named \<Namespace>\<CapitalizedName>.

    If no matching class is found in any registered namespace, a \LogicException is thrown.

    $builder->registerOperatorNamespace('App\\Operators');
    
    // If App\\Operators\\GreaterThan exists, this returns it
    $operatorClass = $builder->findOperator('greaterThan');