Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Ruler Laravel Package

hoa/ruler

Hoa/ruler is a PHP rule engine for defining and evaluating business rules with a dedicated, expressive language. It supports stateless production rules, rule manipulation, proposition evaluation, and integration into PHP applications. Use it to model conditions and make declarative decisions.

View on GitHub
Deep Wiki
Context7

Getting Started

Install it like any Composer package — there is no Laravel service provider, config file, or facade:

composer require hoa/ruler

The smallest working unit is Ruler (the asserter) + Context (the variable bag):

use Hoa\Ruler\Ruler;
use Hoa\Ruler\Context;

$ruler = new Ruler();

$context = new Context();
$context['plan']   = 'pro';
$context['seats']  = 12;
$context['region'] = 'eu';

$ruler->assert('plan == "pro" and seats > 10 and region in ["eu", "uk"]', $context);
// true

As of 2.17.05.16 the asserter can also resolve array-like dimensions, so bracket access into arrays (and ArrayAccess values) works directly in a rule:

$context['cart']    = ['total' => 82.5, 'items' => 3];
$context['country'] = 'DE';

$ruler->assert('cart["total"] > 75 and cart["items"] > 0 and country in ["FR", "DE"]', $context);
// true

Where to look first, in this order:

  1. README.md — it contains the whole grammar and the default operator table; this is effectively the documentation.
  2. Hoa\Ruler\Ruler and Hoa\Ruler\Visitor\Asserter — the entry point and the operator registry you extend.
  3. Hoa\Ruler\Context — an ArrayAccess variable bag; everything the rule can see must be put in here.
  4. Hoa\Ruler\Visitor\Compiler / Printer / Dumper — only when you need to inspect or explain a parsed rule.

First real use case in a Laravel app: a per-tenant eligibility rule stored in the database instead of hard-coded if statements — e.g. "free shipping if cart.total > 75 and country in ["FR", "DE"] and not customer.is_flagged", editable by an admin, evaluated at checkout.

Implementation Patterns

Bind once, reuse. Parsing is the expensive part, so register the asserter as a singleton and never new Ruler() inside a loop:

// AppServiceProvider::register()
$this->app->singleton(\Hoa\Ruler\Ruler::class, fn () => new \Hoa\Ruler\Ruler());

Wrap it in one small service. Do not let Hoa\Ruler\* classes leak across the app — that is what makes migrating off this archived package painful later:

final class RuleEngine
{
    public function __construct(private \Hoa\Ruler\Ruler $ruler) {}

    public function allows(string $expression, array $facts): bool
    {
        $context = new \Hoa\Ruler\Context();
        foreach ($facts as $key => $value) {
            $context[$key] = $value;
        }

        return $this->ruler->assert($expression, $context);
    }
}

Cache the parsed model, not the boolean. The asserter accepts either a raw string or a parsed Hoa\Ruler\Model, so parse once per rule and keep it:

$model = Cache::rememberForever(
    "rule:{$rule->id}:{$rule->updated_at}",
    fn () => $this->ruler->parse($rule->expression)
);

$this->ruler->assert($model, $context);

(Check the version you land on: parse() lives on Ruler in the recent 3.x/date-tagged releases, and the Model is a plain object graph, so it serializes fine.)

Validate expressions on save, not on evaluate. Wrap the parse in a Laravel validation rule so an editor can't persist a broken expression:

public function passes($attribute, $value): bool
{
    try {
        app(Ruler::class)->parse($value);
        return true;
    } catch (\Hoa\Compiler\Exception $e) {
        return false; // message carries the offset of the syntax error
    }
}

Build the context from your domain objects deliberately. Map Eloquent models to plain arrays ($order->only([...]), $user->toRuleFacts()) rather than passing models in and relying on property access — it keeps the rule language stable when your schema changes and avoids lazy-loading surprises mid-assertion. With the 2.17.05.16 asserter change, a nested fact map is now fully addressable from a rule:

$context['cart'] = ['total' => 82.5, 'lines' => [['sku' => 'A-1', 'qty' => 2]]];

// all valid now that dimensions are visitable
$ruler->assert('cart["total"] > 75', $context);
$ruler->assert('cart["lines"][0]["qty"] >= 2', $context);

Register domain operators instead of expressing business logic as long boolean strings. The asserter exposes an operator registry; register pure, side-effect-free callables:

$ruler->getDefaultAsserter()->setOperator('has_role', function ($user, $role) {
    return in_array($role, $user['roles'] ?? [], true);
});

$ruler->assert('has_role(user, "admin") and account.age_days > 30', $context);

Test table-driven. A Pest/PHPUnit dataset of [expression, facts, expected] is the only sane way to keep a rule DSL trustworthy, and it doubles as documentation for whoever edits rules later. Add a case per dimension-access shape you rely on (cart["total"], lines[0]["qty"]) since that path is the freshest code in this release.

Gotchas and Tips

  • It is archived (last release 2.17.05.16, May 2017) and has zero dependents in this dataset. Treat it as a maintenance liability: it must be audited against your PHP version (the hoa/* stack around hoa/stream, hoa/file, hoa/compiler is where PHP 8 warnings usually surface), and it should not be introduced into a new greenfield project. Note this latest tag is the tail of the project — an asserter-only fix, no new maintenance.
  • The asserter compiles rules into PHP and evals them. That is fine for developer-authored expressions, but it means rule authoring is close to code execution. Never ship a UI that lets untrusted users type arbitrary expressions; if you must, first whitelist the operator names present in the parsed Model and reject everything else. The new dimension-visiting path is worth a review if you expose rules to non-developers.
  • Two different exception families. Syntax errors come from Hoa\Compiler\Exception (with the character offset in the message), runtime errors from Hoa\Ruler\Exception. Catch both, and translate them into something a Laravel log or form can show. Bad dimension access (missing key, non-array value) surfaces as a runtime exception rather than a parse error, so it will only blow up at evaluate time.
  • Undefined variables are the #1 source of silent bugs. Missing keys tend to resolve to null, and null > 0, null in [...], and null == "" all behave in confusing ways with loose comparison. Define defaults on the Context (or pre-validate that every identifier appearing in the model exists in your facts array) before asserting. The same applies to missing dimensions: cart["total"] on a cart without that key behaves like any other missing fact — decide up front whether that is null-tolerant or an error.
  • The grammar uses double-quoted strings and word operators. plan == 'pro' (single quotes) is a parse error; use and / or / xor / not, not && / ||. This matters more now that bracket dimensions are usable — cart["total"] is the working form; bare-word dimensions and single-quoted keys are not. Rules stored in a DB by non-developers will hit this constantly — give the editor a syntax check on the server and a good error message.
  • in wants a literal list (country in ["fr", "de"]). Passing a PHP array as a context value and using it on the right-hand side of in is version-dependent — verify on your install, or expose a in_list(x, list) custom operator and stop worrying.
  • Dot access (user.group) and bracket access (user["group"], lines[0]["qty"]) both resolve over arrays and objects, and how it walks a value is easy to get wrong; flatten your facts to a predictable shape rather than mixing arrays, Eloquent models, and DTOs. Prefer one access style per rule set so the stored expressions don't depend on which of the two the asserter happens to support cleanly.
  • Do not use setCache() with Hoa\File\ReadWrite on ephemeral filesystems. It writes compiled PHP to disk; on Vapor, Lambda, or read-only containers it fails. Prefer wrapping compiled models in Laravel's cache (Redis/array) as shown above.
  • Debugging: when a rule "just returns false", dump the AST (Hoa\Ruler\Visitor\Dumper or the Printer visitors) and log both the expression and the serialized context. Nine times out of ten the bug is a missing fact (or a missing dimension) — not the rule. This release changed the asserter only, so when in doubt re-parse the expression and re-run it against the exact array shape your rule expects.
  • Honest build-vs-buy: if you're starting fresh, symfony/expression-language gives you the same "user-editable boolean expression" capability with active maintenance, a compile/cache step, and custom functions — and it plays fine inside Laravel. laravel/pennant covers the feature-flag slice of this problem with far less surface area. Keep hoa/ruler around only where it already works.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle