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.
Architecture fit. frontastic/rulerz is a fork of K-Phoen's RulerZ implementing the Specification pattern for PHP. It gives you a runtime expression DSL (points > 30 and gender = "f"), closure-based predicates, and a Specification interface, with the same rule evaluated in-memory, compiled to SQL/DQL via Doctrine, or translated into Elasticsearch DSL via target-specific visitors. For a Laravel/PHP product this sits precisely at the domain boundary between user-defined filter input and data access — the right layer for reusable, testable, serializable business rules shared across admin UI, API query params, and persistence. Fit is strong on pattern, weak on out-of-the-box Laravel plumbing: there is no first-party Eloquent or Scout target, so the two headline integrations (Doctrine, Elasticsearch) are adjacent to, not native to, the Laravel stack.
Integration feasibility. Mechanically trivial — Composer package, MIT license, no services. The consuming surface is small: construct a RulerZ instance with a compiler, operator registry, and targets, then call satisfies(), filter(), or matching(). The real feasibility questions are economic and supply-chain:
hoa/ruler/hoa/compiler, effectively unmaintained with known PHP 8.x friction. The likely reason this fork exists is to patch exactly that. Verify what changed before duplicating the work.matching() compiles against Doctrine QueryBuilders; Illuminate\Database\Query\Builder needs a custom Target/visitor (~200–400 LOC) or a DBAL bridge. In-memory filter() over Collections works today.Technical risk. Ranked:
| Risk | Severity | Mitigation |
|---|---|---|
Abandoned transitive dependency (hoa/*) |
High | Swap compiler for symfony/expression-language behind the Compiler interface |
| Single-maintainer fork, no release cadence | High | Vendor internally; pin commit SHA; own CI |
| No PHP/Laravel compatibility matrix | Medium | Add PHPUnit + PHPStan matrix on adoption |
| Behavioral drift across targets | Medium | Contract tests asserting identical result sets per target |
| User-supplied DSL as injection/DoS surface | Medium | Operator allowlist; verify parameter binding; cap expression complexity |
| Limited per-target operator coverage | Low–Medium | Document supported subset; fail loudly on untranslatable specs |
The dominant risk is supply chain and longevity, not code quality — the codebase is small and well-shaped, but it has zero stars, zero dependents, and zero known consumers.
Key questions.
K-Phoen/rulerz, and does it still pull hoa/*?symfony/expression-language or a hand-rolled predicate tree?Stack fit. Assume Laravel 10/11/12 on PHP 8.2+. Two postures:
Specification objects and the DSL compiler over arrays/Collections for eligibility, segmentation, and pricing rules in the domain layer. Zero persistence coupling.matching() compiles to Illuminate\Database\Query\Builder, mirroring the Doctrine target. Budget for it; do not assume it exists.Bind the engine in a service provider as a singleton exposing a narrow interface (SpecificationEngine::filter(iterable $items, Specification $spec): iterable) so controllers and models never reference the library directly. This is the single highest-leverage decision — it caps migration cost regardless of upstream fate.
Migration path. Realistically greenfield adoption with exits pre-planned:
internal/rulerz) so we control tags, PHP constraints, and patches. Do not depend on Packagist availability of a zero-star fork.hoa/* is present, replace the compiler with symfony/expression-language (mature, maintained, Laravel-adjacent) behind the existing Compiler interface — confirm the fork hasn't already done this.SpecificationEngine interface plus contract tests makes a future swap to symfony/expression-language or a hand-rolled specification tree an internal refactor, not a cross-cutting change.Compatibility. Pin conservative constraints and widen only with evidence: PHP ^8.2, Laravel ^10.0|^11.0|^12.0, and require that the package's own constraints not force a downgrade. Treat any version bump as a reviewed change, not a composer update side effect.
Sequencing. (1) One-week timeboxed spike converting three real production rules against Collections; (2) define the internal interface and contract tests; (3) vendor and pin; (4) build the Eloquent target only if a second consumer justifies it; (5) revisit self-service authoring as a separate roadmap item — this package is the engine, not the UI.
Maintenance. With this adoption profile, assume we inherit maintenance. A named owner and a vendored fork are prerequisites, not nice-to-haves. Ongoing cost is low in volume (small codebase) but non-zero in attention: PHP/Laravel upgrade compatibility checks, security advisory monitoring on a package nobody else watches, and periodic audit of the hoa/* (or replacement) surface. Budget a recurring slice of one engineer's time, not a project.
Support. No vendor, no community, no Stack Overflow corpus. Support is internal: the owning team becomes the escalation path for DSL questions, operator semantics, and target-specific behavior. Mitigate with documentation of the supported DSL subset and a deliberately loud failure mode for untranslatable specs — silent divergence between targets is the expensive failure.
Scaling. In-memory filtering scales with collection size and is unsuitable for large catalogs; pushdown to SQL or the search index is the scaling story, and that is exactly where the Eloquent gap and ES version coupling bite. There is no index-awareness or caching story out of the box — parsed-rule caching exists, but query planning does not. Benchmark against real catalog size before committing, and explicitly rule out hot-path (>1k RPS) and real-time bidding use cases.
Failure modes. Ranked by cost: (a) semantic drift — the same spec returns different results in-memory vs SQL vs ES, producing "works in preview, wrong in production" incidents; contract tests across targets are the control; (b) dependency rot — a PHP upgrade forces an emergency compiler swap; the interface boundary and a rehearsed exit contain it; (c) injection/DoS via user-supplied DSL — allowlist operators, verify parameter binding, cap complexity; (d) abandonment — the fork stops receiving patches; vendoring plus a pinned SHA makes this survivable rather than fatal.
Ramp-up. The Specification pattern is familiar to most PHP engineers, but the DSL, operator registry, and visitor/compilation model are not. Expect a genuine learning curve for anyone writing custom targets or operators, and a shallow one for anyone only composing specs. Onboarding cost is lowest when rules are plain PHP classes behind our interface and the DSL is an implementation detail — that containment is also what makes the package replaceable. For product and commercial stakeholders the gain is a shared, precise rule vocabulary and a shorter path from "change the eligibility criteria" to "it's live"; the non-gain, which must be stated plainly, is that self-service rule authoring, versioning, audit trails, and governance remain separate roadmap items requiring either a translation layer or a BRMS.
Recommendation. Fund a two-week spike, not a commitment. The defensible decision now is "encode volatile commercial rules as composable, individually tested Specification objects" — that holds regardless of whether this specific fork survives. Adopt the package only behind a thin internal interface, with a named owner, a pinned vendored copy, a written exit plan, and a due-diligence checklist covering license (MIT — fine), security advisory history, PHP/Laravel matrix, adapter coverage for our datastores, three real rules converted, and a comparison against upstream K-Phoen/rulerz.
How can I help you explore Laravel packages today?