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 Bridge Laravel Package

frontastic/rulerz-bridge

Integrates the RulerZ rule engine with Frontastic, giving PHP applications a clean way to define and evaluate configurable business rules against objects and query builders. Ideal for promotions, eligibility checks, dynamic pricing, and other rule-driven logic.

View on GitHub
Deep Wiki
Context7

Getting Started

Know what you are installing. This is not a rules engine — it is Symfony DI glue over rulerz/core (historically a re-publication of kp-rulerz-bundle). Ten minutes spent confirming which of those two things you actually have will save you a day.

  1. Read the wiring before the README.

    • composer.json — PHP/Symfony constraints, and whether it requires rulerz/core directly or pins an old hoa/ruler.
    • DependencyInjection/ — the extension class tells you the config tree keys (rulerz: ...) and, critically, whether operators/compilers are declared in YAML or via tagged services.
    • *Bundle.php + Resources/config/services.xml — the service IDs you will actually inject. If there is no Bundle class, it is a plain config package and you wire it manually.
    • CompilerPass/ — anything tagged (rulerz.operator, rulerz.compiler, rulerz.target) is your extension point.
  2. Install and confirm container resolution.

    composer require frontastic/rulerz-bridge
    php bin/console debug:container rulerz          # service alias?
    php bin/console debug:container --tag=rulerz.operator
    

    If rulerz does not resolve, stop and hand-wire core in services.yaml — that is usually a 20-line block and removes the dependency entirely.

  3. First use case — the one that proves the value. A config-stored eligibility rule, evaluated twice: in-memory for a preview/admin screen, and pushed down to Doctrine for the real listing query. That dual execution is the only reason to accept a DSL; if you only ever need one target, symfony/expression-language is cheaper to own.

// app/config/rules.yaml (or a DB row with a version column)
promotions:
  free_shipping:
    rule: "tier in ['gold','silver'] and cart_total >= :threshold"
    params: { threshold: 5000 }

// src/Checker/ShippingEligibility.php
final class ShippingEligibility
{
    public function __construct(private \RulerZ\RulerZ $rulerz) {}

    /** @param array<string,mixed> $params */
    public function applies(Customer $customer, string $rule, array $params = []): bool
    {
        // satisfies() evaluates a single subject in memory
        return $this->rulerz->satisfies($customer, $rule, $params);
    }
}
  1. Then the query path — same rule string, different target:
    $qb = $repo->createQueryBuilder('p');
    $this->rulerz->apply($qb, $rule, $params);   // mutates the QueryBuilder
    

Implementation Patterns

Wrap it, immediately. Nothing outside one namespace should import RulerZ\*. The package is unmaintained (0 stars, 0 dependents); the wrapper is what makes forking or swapping to ExpressionLanguage a contained change.

interface RuleEngine
{
    /** @param array<string,mixed> $params */
    public function matches(object $subject, Rule $rule, array $params = []): bool;

    /** @param array<string,mixed> $params */
    public function filter(object $target, Rule $rule, array $params = []): iterable;
}

Rule is a value object carrying ruleString, version, params, target. Persist version — you will need it for cache keys and audit.

Always bind parameters, never interpolate. Rules that reach the engine as assembled strings are an injection surface the moment a non-developer can edit them. Keep the DSL constant and vary $params:

// good
$engine->filter($qb, 'total > :min and country in :countries', ['min' => 100, 'countries' => $codes]);
// bad: "total > {$userInput}"

Dual-execution / preview workflow. The pattern that sells this package internally: an admin edits a rule, you evaluate it against a sample of 50 entities in PHP (satisfies() in a loop, or filter() over an ArrayIterator), show the delta vs. the current rule, and only then save. Production filtering goes through the Doctrine/ES target. Same string, two compilers — which is exactly the risk covered in Gotchas.

Register custom operators deliberately. Domain operators (within_radius, has_tag, stock_at_warehouse) go in as tagged services and — critically — into an allow-list consumed by your validator. RulerZ will happily compile whatever it can parse.

services:
  App\Rules\Operator\WithinRadius:
    tags: [{ name: rulerz.operator }]   # verify tag name in the bridge's CompilerPass

Cache compiled rules, not evaluations. Compilation is the expensive part; it produces a PHP class. Key on sha256(target . compilerVersion . ruleString . parameterKeys) — not parameter values, otherwise you get one cache entry per customer. Put this behind PSR-6 and warm it on deploy for rules in config.

Test rules as data, not as code. A fixture table of (rule, subject, expected) run against both the array target and the query target is the only thing that catches compiler divergence. Treat a rule change as a schema change: bump version, run the fixture set, diff results before activation.

Integration tips.

  • Inject the Rule collection via a tagged iterator or a config tree, not by reading YAML in a service — you want validation to fail at container build time, not at runtime.
  • Feature-flag rule evaluation per use case so a bad rule is a config rollback, not a deploy.
  • Log (rule_version, params, target, cache_hit) on every evaluation failure. Without it, a compile error is an unreadable stack trace.

Gotchas and Tips

The same rule can legitimately produce different results per target. Array evaluation uses PHP comparison semantics; the Doctrine compiler emits DQL where '5' = 5-style coercion, NULL ordering, and IN semantics differ. Your preview screen will lie to you if it only runs the array target. Assert parity in fixtures for every rule you ship.

apply() mutates the QueryBuilder. Reusing a pre-configured QB across rules (pagination + filter + sort) silently accumulates constraints. clone before every apply(), or build a fresh QB per rule.

Compile errors surface at execution, not parse. An unknown operator or a field that does not exist in the target fails when the rule is first evaluated — i.e. on the customer's request. Validate rules on save by compiling them ($compiler->compile($rule, $targetType)) and reject at authoring time.

Debugging a failed compile. RulerZ compiles the DSL into a generated PHP class. When a rule misbehaves, get the generated source and read it — that is faster than reasoning about the DSL. Wrap compilation in try/catch and include the rule string and the generated source (or its hash) in the log context.

Field naming is not portable. The DSL addresses properties (customer.tier), but Doctrine wants DQL aliases (c.tier) and Elasticsearch wants index fields with possibly different names. You need a name-mapping layer or a strict convention; do not assume the bridge translates them. Verify with one rule per target before authoring twenty.

Parameters must be typed, not stringly. status = 'active' and status = "active" and status = active are three different parses. Pin quoting conventions in your internal rule-authoring guide; there is no linting UX here.

Version drift is the dominant risk. Check constraints against your runtime before the pilot, not during it. If it fails, the decision is binary: vendor a patched fork, or drop the bridge and wire rulerz/core in services.yaml. Do not defer — a bridge that blocks a Symfony upgrade is a platform problem, not a library problem.

Vendor or fork on day one. Zero dependents means no downstream validation: you will be the first to hit any bug, and no one is coming to fix it. Commit a fork or replace it in Composer; either way, record an owner and a re-evaluation date.

Empty vs. exception is config-dependent. With no matching compiler, behaviour ranges from a CompilerException to a silent passthrough depending on how the bridge registers targets. Write one test per target that asserts the failure mode, so an unsupported rule cannot quietly return all rows.

Extension points, ranked by usefulness: tagged operators (adds domain vocabulary) → custom compiler/target (adds a backend) → the compiler cache (perf). Everything else in this package is boilerplate you can read in an afternoon and replace if you have to.

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.
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core