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

frontastic/rulerz-bundle

Symfony bundle integrating the RulerZ rule engine, letting you define business rules as readable specifications and apply them to arrays, Doctrine query builders, and other collections for flexible, reusable filtering.

View on GitHub
Deep Wiki
Context7

Getting Started

First, a reality check that changes how you start. frontastic/rulerz-bundle is a Symfony bundle. It declares a Symfony\Component\HttpKernel\Bundle\Bundle subclass, ships a DependencyInjection\RulerZExtension, and wires services through Symfony service definitions. It targets symfony/framework-bundle ^2.7/^3.x. On a Laravel 10/11 app (which pulls Symfony components ^6.2/^7.x), composer require frontastic/rulerz-bundle will fail dependency resolution outright. There is no service provider, no config publish, no facade. So "get started" means one of two honest paths:

  • Read the bundle, don't install it. Treat the repo as reference wiring: look at DependencyInjection/RulerZExtension.php and the Resources/config/services.xml to see exactly which classes the compiler, targets, and operators are bound to. That's ~a few hundred lines and is the actual valuable thing here.
  • Install the core library it wraps — kphoen/rulerz — and wire it into Laravel yourself. This is the day-to-day path a Laravel team will actually take.
composer require kphoen/rulerz
php artisan make:provider RulerzServiceProvider

Bind the compiler and façade as singletons:

// app/Providers/RulerzServiceProvider.php
public function register(): void
{
    $this->app->singleton(Compiler::class, fn () => new Compiler\EvalCompiler());

    $this->app->singleton(RulerZ::class, function ($app) {
        return new RulerZ($app->make(Compiler::class));
    });

    $this->app->alias(RulerZ::class, 'rulerz');
}

Where to look first in the bundle repo. In order:

  1. DependencyInjection/ — learn the service ids and which compiler it instantiates by default.
  2. Resources/config/ — the target list (Array, Doctrine, Elasticsearch). Copy the intent, not the XML.
  3. The test directory — the shortest concrete example of registering an operator and evaluating a rule.

First use case. Start small and evaluable: an eligibility check over a PHP array (a cart's line items, a user's entitlements), never against a database on day one. Rules live in config or a rules table; values are bound as parameters.

$rulerz = app(RulerZ::class);

$cart = ['total' => 120.00, 'items' => [...], 'customer_tier' => 'gold'];

$eligible = $rulerz->satisfies(
    $cart,
    'total > :threshold and customer_tier in :tiers',
    ['threshold' => 100, 'tiers' => ['gold', 'platinum']]
);

Once that works, you have proven the DSL, the parameter binding, and your DI wiring — the three things everything else depends on.

Implementation Patterns

Wrap the façade behind your own contract. Don't let RulerZ\RulerZ leak into controllers. Define a small interface so you can swap the engine later without touching call sites:

interface RuleEvaluator
{
    public function satisfies(mixed $subject, string $rule, array $params = []): bool;
    public function filter(iterable $subjects, string $rule, array $params = []): iterable;
}

The RuleEvaluator is what services inject; the RulerZ façade lives inside one implementation class. This is the single most valuable pattern because it converts a dormant dependency into a replaceable one.

Store rules as data, versioned, with an explicit target.

Schema::create('rules', function (Blueprint $t) {
    $t->id();
    $t->string('key')->unique();       // 'promo.free_shipping'
    $t->text('expression');            // RulerZ DSL
    $t->string('target')->default('array'); // array|eloquent|es
    $t->unsignedInteger('version')->default(1);
    $t->timestamps();
});

Never inline rule strings in controllers. The whole value proposition — "define once, apply consistently" — collapses the moment rules live in code at call sites. Rules in the DB, code in the repo, both under review.

Compile once, cache by hash. Compilation is the expensive part; evaluation is cheap. Cache compiled rules keyed by hash(expression . target . version):

public function compiled(Rule $rule): CompiledRule
{
    return Cache::rememberForever(
        "rule:{$rule->key}:{$rule->version}",
        fn () => $this->compiler->compile($rule->expression, $this->targetFor($rule))
    );
}

Bump version in Rule on every edit; never invalidate by key alone. This makes cache busting trivial and auditable.

Parameterize everything. RulerZ supports bound values — use them. String concatenation into the DSL is the equivalent of SQL injection and will bite you on user-supplied thresholds, tiers, or date ranges:

// Good
$rulerz->satisfies($subject, 'amount > :min', ['min' => $threshold]);

// Bad — user value ends up in the rule text
$rulerz->satisfies($subject, "amount > {$threshold}");

Use the array target for anything small; write a custom target for Eloquent. RulerZ's Array target builds a PHP anonymous function — perfect for a collection already in memory, dangerous for a large table. For Eloquent/QueryBuilder, implement a Target\DoctrineORM-style class over Illuminate\Database\Query\Builder so the rule pushes down to SQL:

class EloquentTarget implements Target\CompilableTarget
{
    public function applyFilter($target, array $parameters, array $operators, ExecutionContext $context)
    {
        return $target->whereRaw(
            $context->getCompilationResult(),
            $parameters
        );
    }
}

This is the pattern that makes the "same rule, SQL for admin, array for tests" story real. Without a pushdown target, RulerZ evaluates in PHP and you lose the scaling property that justifies the dependency at all.

Same rule, multiple surfaces. The strongest implementation pattern is to keep one expression and evaluate it against different targets in different contexts, e.g.:

  • Admin list → pushdown to Query\Builder for pagination.
  • Unit test → Array target on fixtures.
  • Storefront search → compile to Elasticsearch query DSL.

Keep the rule text identical across targets. Divergence is where the bugs live.

Custom operators are the extension point you'll actually use. Domain concepts like is_eligible, within_budget, or has_entitlement belong as registered operators, not baked into the DSL string. Register them once in the service provider:

$compiler->registerOperator('within_budget', function ($subject, $max) {
    return $subject['spend'] <= $max;
});

The bundle's config keys (rulerz.operators, rulerz.targets) are the shape to replicate in a Laravel config file.

Test with fixture subjects, not mocks. The whole point of the Specification pattern is that rules are pure functions of subject + parameters. Golden-path fixture tests beat mock-heavy tests. Assert both the positive and the boundary case for each rule, and assert parse failure throws a specific exception type.

Gotchas and Tips

The bundle will not install in a modern Laravel app. The Symfony constraint mismatch is a hard blocker, not an inconvenience. Any tutorial that says "just require it" predates Symfony 4. Do not fight the resolver — you will end up pinning EOL components transitively.

There is no upstream maintainer absorbing upgrades. 0 stars, 0 dependents, and the core kphoen/rulerz is effectively dormant. PHP 8.x compatibility of the core needs a code audit before you commit. Pin an exact version, run PHPStan level 5+ in CI, and add an architectural test asserting no Symfony\ or RulerZ\ class names appear in your application layer (only in the adapter package).

Array-target evaluation is a full-table read waiting to happen. If a target mismatch silently falls back to the Array target while you think you're filtering in SQL, you will pull the whole result set into memory before filtering. Instrument rows-scanned and query duration per rule. If you see a rule whose execution time scales with table size, its target is wrong.

Parse failures happen at runtime by default. A malformed rule throws mid-request — in a checkout, that's a blocked transaction; in a listing, it's a broken page. Validate rules on save (compile in a queue or synchronously in the save path) and keep a safe fallback path per rule. Never let rule evaluation be the unguarded last step of a critical request.

Silent rule/datastore divergence. Rules reference columns and fields by name. A rename in a migration breaks a rule that nothing else in CI was checking. Add schema assertions in your test suite that the fields referenced by each rule's target still exist.

Compile cache stampede on deploy. A cold cache after every deploy means the first requests compile every rule simultaneously. Warm the rule cache as a deploy step (php artisan rules:warm) rather than on first hit.

The DSL is small and has sharp edges. It is not a general-purpose language. Watch for null handling (comparisons against null are not the ergonomic === null you'd write in PHP), string escaping, and operator precedence when rules get long. Prefer several small rules composed in PHP over one large expression — and document the composition in the rule row (key + expression) so a reviewer sees intent, not a wall of DSL.

Debugging: dump the compiled artifact, not the rule. When a rule behaves unexpectedly, what you want is the compiled output for the target in question. Log the compilation result and the parameter set together at the boundary of your RuleEvaluator. That one log line will answer most "why did this rule match?" questions.

Custom targets and operators are the real extension points, not configuration. The bundle config is thin; the leverage is in implementing Target and registering Operator callables. Budget your engineering effort there. If you can't write a pushdown target for your datastore, you should re-evaluate whether RulerZ is the right shape at all — the value is entirely in compile-to-datastore.

The rule-builder UI is the expensive part; don't underestimate it. The DSL is a hard blocker for product managers. If self-service authoring is the goal, the bundle (and RulerZ core) is the small part of the work — validation, simulation/dry-run, audit trail, and approval workflow are the actual product, and none of them ship here. Plan the spike against core, the adapter package next, then two domains proving the pattern, and only then consider authoring tooling.

If ecosystem constraints force this bundle on you (e.g., you're already inside a Frontastic/commercetools stack), keep the rule definitions storage-agnostic and the RuleEvaluator interface stable. That keeps a future swap to upstream k-phoen/rulerz-bundle — or a ~500-line in-house specification engine — cheap.

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.
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package