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

Rulerz Laravel Package

frontastic/rulerz

frontastic/rulerz is a PHP rule engine and specification library for writing business rules once as reusable predicates and applying them to arrays, query builders, and other data sources. Use its concise DSL or PHP rules for filtering, validation, and dynamic access control.

View on GitHub
Deep Wiki
Context7

Getting Started

Install (and vendor) it first. This is a zero-star single-vendor fork; treat installation as a supply-chain decision, not a composer require.

# Prefer a pinned commit over a branch or a floating version
composer config repositories.rulerz vcs https://github.com/FrontasticGmbH/rulerz
composer require "frontastic/rulerz:dev-master#<commit-sha>"

Before writing code, check composer.lock for hoa/compiler / hoa/ruler. If they're still pulled in, that's the unmaintained transitive dependency that historically breaks on PHP 8.x — confirm the fork actually patched it, and check whether upstream k-phoen/rulerz is healthier.

Where to look first, in this order:

Class / namespace Why it matters
RulerZ\RulerZ The whole public API: satisfies(), filter(), matching() plus the *Specification() variants
RulerZ\Spec\AbstractSpecification Where your named rules live
RulerZ\Spec\{AndX, OrX, Not, Rule} Composition and the "rule loaded from DB" wrapper
RulerZ\Target\Native\Native The in-memory target — the only one that works with zero extra work in Laravel
RulerZ\Compiler\Compiler Rule → AST → compiled PHP; controls the rule cache

Bind it once, behind your own interface. Do this on day one, not later:

// app/Rules/SpecificationEngine.php
namespace App\Rules;

use RulerZ\Spec\Specification;

interface SpecificationEngine
{
    public function satisfies(object|array $subject, Specification $spec): bool;
    public function filter(iterable $items, Specification $spec): iterable;
}
// app/Providers/RuleServiceProvider.php
use RulerZ\Compiler\Compiler;
use RulerZ\RulerZ;
use RulerZ\Target\Native\Native;

public function register(): void
{
    $this->app->singleton(SpecificationEngine::class, function () {
        return new class(
            new RulerZ(Compiler::create(), [new Native()])
        ) implements SpecificationEngine {
            public function __construct(private RulerZ $rulerz) {}

            public function satisfies(object|array $s, \RulerZ\Spec\Specification $spec): bool
            {
                return $this->rulerz->satisfiesSpecification($s, $spec);
            }

            public function filter(iterable $items, \RulerZ\Spec\Specification $spec): iterable
            {
                return $this->rulerz->filterSpecification($items, $spec);
            }
        };
    });
}

Controllers and models then depend on SpecificationEngine, never on RulerZ. That is what makes the library a replaceable detail.

First real use case: a rule currently buried in an if block. Free-shipping eligibility is a good one.

// app/Rules/Shipping/FreeShippingEligible.php
use RulerZ\Spec\AbstractSpecification;

final class FreeShippingEligible extends AbstractSpecification
{
    public function __construct(private float $threshold) {}

    public function getRule(): string
    {
        return 'cart_total >= :threshold and customer_tier != :excluded';
    }

    public function getParameters(): array
    {
        return ['threshold' => $this->threshold, 'excluded' => 'blocked'];
    }
}
$engine->satisfies($cart->toArray(), new FreeShippingEligible(50.00)); // bool
$eligible = iterator_to_array($engine->filter($carts, new FreeShippingEligible(50.00)));

Write the rule string, the params, and a unit test asserting satisfies() is true/false for three fixtures. That's the whole loop.

Implementation Patterns

One class per rule, named in the business's language. GoldTierCustomer, InStock, NotAlreadyDiscounted. This is the pattern's entire payoff: a rule change becomes a diff in one small file with one failing test, instead of an archaeology expedition through a service.

Rules from the database via RulerZ\Spec\Rule. Store the DSL string and a JSON params column on an Eloquent model, then hydrate:

$spec = new \RulerZ\Spec\Rule($ruleModel->expression, $ruleModel->parameters);

This is where the "no more deploys for commercial logic" story actually comes from — but you own persistence, versioning, and audit (created_by, superseded_at, a rule_versions table). The package ships none of it.

Validate at write time, never at read time. A malformed rule should fail when an admin saves it, not during checkout. Compile it once in the save path:

// throws on a syntax error / unknown operator
$rulerz->satisfiesSpecification(['cart_total' => 0], new Rule($expression, $params));

Wrap that in a form request rule and return a friendly validation error.

Compose specifications instead of growing rule strings.

use RulerZ\Spec\{AndX, OrX, Not};

$spec = new AndX(
    new InStock(),
    new OrX(new GoldTier(), new CartOver(100.0)),
    new Not(new AlreadyDiscounted()),
);

Keep parameter names globally unique. AbstractSpecification composition merges parameter arrays; two specs both using :threshold will silently collide and one wins. Prefix by concern (:shipping_threshold, :promo_threshold) or suffix with a hash. This is the single most likely production bug in a composed-rule codebase.

Pick the execution path deliberately.

  • In-memory over Collections (Native target): works today, no adapter code, fine up to low thousands of rows. Use for previews, admin "who matches this?" tools, and tests.
  • Pushdown to the database: requires a custom Target + visitor for Illuminate\Database\Query\Builder (Eloquent), roughly 200–400 LOC. Do not assume matching() accepts an Eloquent builder — it doesn't.
  • Elasticsearch: the bundled visitor targets a specific client version and a DSL subset. Spike against your actual cluster before roadmap commits.

Cache compiled rules. Compilation is the expensive part; parameter values are not part of the cache key, so a parameterized rule compiles once. Wire an evaluator pointed at a real cache dir and clear it on deploy the way you'd clear compiled views:

// storage/framework/rulerz must be writable and gitignored
$compiler = Compiler::create(new \RulerZ\Compiler\FileEvaluator(storage_path('framework/rulerz')));

Confirm which evaluator Compiler::create() wires by default in your pinned version — if it's the eval-based one, you're paying parse cost on every request.

Contract-test across targets. If a rule runs in more than one place, assert identical result sets:

#[DataProvider('rules')]
public function test_targets_agree(string $rule, array $params, array $fixtures): void
{
    $inMemory = array_column(iterator_to_array($this->native->filter($fixtures, $rule, $params)), 'id');
    $fromDb   = $this->ids($this->doctrine->matching($this->qb(), $rule, $params));
    $this->assertEqualsCanonicalizing($inMemory, $fromDb);
}

This is the only defence against silent drift between "what the preview showed" and "what production did".

Custom operators go through RulerZ\Compiler\CustomOperator, registered once on the engine. Keep them scalar and pure (is_even, starts_with). Anything that needs to become SQL must be implemented at the visitor level for that target — an operator that works in-memory will not automatically translate to DQL or ES. Register an allowlist of these and reject anything else at authoring time.

Log which specification matched when rules drive money (discounts, eligibility). A matched_rules array on the order is worth more than any dashboard when someone asks why a discount fired.

Gotchas and Tips

filter() returns a Generator. You can iterate it once; count() and repeated foreach will surprise you. Convert with iterator_to_array() or iterator_count() at the boundary.

The DSL is not PHP. No in_array(), no strtolower(), no &&. Use in, and, or, not, =, !=, >, >=, <, <=. Strings are quoted; booleans are true/false. Add explicit parentheses around composed and/or — precedence is easy to misread and impossible to eyeball in a stored rule.

Parameterize every value; never interpolate. A rule saved as 'points > 30' works fine until someone stores 'points > ' . $userInput. Placeholders (:min_points) go through the compiler's binding path; string interpolation becomes generated SQL. If end users can ever author rules, enforce an operator allowlist and cap expression complexity — the DSL is an injection surface, not just a filter syntax.

NOT on nullable columns silently drops rows. not (discount_code = 'X') excludes rows where discount_code IS NULL under SQL three-valued logic; the in-memory target may disagree. This is the classic in-memory-vs-SQL divergence, along with '30' vs 30 coercion and date formatting. Write fixtures with nulls and mixed types in the contract tests.

Missing properties don't fail gracefully. satisfies() over an array/object without the field the rule references produces a notice or exception depending on target. Validate rule fields against the subject shape when a rule is saved, or normalize subjects through a DTO before evaluation.

Unsupported operators fail at compile time, per target. A rule that runs in-memory may be untranslatable to DQL. Fail loudly and early rather than falling back — a rule that silently returns everything on the search path is worse than a 500.

Doctrine target assumptions. The visitor derives the query alias from the QueryBuilder's root alias. matching() on a builder with joins, multiple roots, or a non-standard alias is where it breaks. Keep the builder simple, or build the where yourself from the returned builder.

Compiled-rule cache directory must exist, be writable by the web user, and be cleared on deploy (or versioned). A stale compiled file is the "I changed the rule and nothing happened" bug.

Debugging recipe: (1) assert the rule parses with a one-line satisfies() in tinker; (2) inspect the generated PHP file in the rule cache dir to see exactly what the visitor produced; (3) for query targets, log the resulting DQL/SQL before execution. The compiled output is the fastest way to understand an unexpected result — the DSL layer is thin, the visitor is where semantics live.

Laravel has no first-party target. Illuminate\Database\Eloquent\Builder is not supported out of the box. Plan either in-memory filtering (simple, doesn't scale with catalog size) or budget a custom target. Do not discover this in sprint three.

Supply chain is the dominant risk. Zero stars, zero dependents, a fork of a dormant project. Mitigations that cost little: pin a commit SHA, mirror into an internal namespace you control, run PHPUnit/PHPStan in your own CI, and keep rules behind your SpecificationEngine interface so swapping to symfony/expression-language or a hand-rolled predicate tree is an internal refactor. Name an owner; budget a week a year for maintenance.

Extension points, ranked by usefulness: AbstractSpecification (daily), Rule + your own persistence layer (weekly), CustomOperator (occasionally — scalar only), custom Target/visitor (once, significant), replacing the Compiler (only if hoa/* forces it).

When not to reach for it. Three static conditions in one service is over-engineering — a plain method and a test is cheaper and clearer. Adopt this when rules genuinely churn monthly, or when the same condition is duplicated across a controller, a query builder, and an admin filter and the copies have already drifted.

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.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons