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

Rule Laravel Package

swp/rule

A lightweight rules engine for Laravel: define reusable rule classes, compose conditions into groups, and evaluate them against models or plain data. Keeps decision logic out of controllers and makes complex business rules testable and easy to maintain.

View on GitHub
Deep Wiki
Context7

Getting Started

Reality check first: this is a read-only subtree split of an internal Superdesk Web Publisher component, last released in 2018, with no independent dependents. It is not a general-purpose rules engine — it is the expression-evaluation layer that SWP's content-routing workflow sits on top of. If you're not working inside SWP (or a fork of it), you will spend more time wiring it than writing symfony/expression-language calls by hand. Read this section as "how to evaluate whether it fits" before "how to install."

Install and inspect

composer require swp/rule

The package has no DI container config, no Doctrine mappings, and no bundle. You get interfaces plus a couple of concrete classes. Read these files in order — together they are essentially the whole public surface:

  1. src/Model/RuleInterface.php — the data contract (name, expression, priority, and depending on version a configuration array).
  2. src/Evaluator/RuleEvaluator.php — wraps ExpressionLanguage::evaluate(); this is the only thing that actually "runs" a rule.
  3. src/Processor/RuleProcessor.php — the orchestration layer: takes a subject, finds applicable rules, evaluates them, returns the match.
  4. src/Repository/RuleRepositoryInterface.php and src/Validator/RuleValidatorInterface.php — the two seams you must implement yourself.

Minimal first use case

The canonical SWP scenario: "if this article is in the sport category, route it to the sport destination." Expressed as a rule:

use SWP\Component\Rule\Model\Rule;
use SWP\Component\Rule\Evaluator\RuleEvaluator;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;

$rule = new Rule();
$rule->setName('sport-to-sport-route');
$rule->setExpression("article.getMetadataByKey('category') == 'sport'");
$rule->setPriority(10);

// Verify the constructor against the tag you installed — it takes the
// ExpressionLanguage instance in the versions published on Packagist.
$evaluator = new RuleEvaluator(new ExpressionLanguage());

$matched = $evaluator->evaluate($rule, ['article' => $article]);

That's the entire happy path. Note what the package did not do: it did not define article, it did not load the rule, it did not decide what "matched" means for your app, and it did not persist anything. Everything below is about filling those gaps.

Implementation Patterns

1. Rule + ExpressionLanguage + parameter bag — the core loop. The evaluator is a thin adapter. All of the domain knowledge lives in the $parameters array you pass in. Deliberately keep that array small and explicit: article, route, tenant, now. Every key you add is a method surface you're exposing to user-authored strings (see Gotchas). In SWP the parameter set is assembled per-operation by the calling workflow, which is why the package itself has no opinion.

2. Rule collections with explicit priority semantics. Rules are ordered by priority, and the two viable strategies are first match wins (routing) and apply all matches (tagging/notifications). Do not leave this implicit — sort the collection once at load time and document the tie-break, because two rules with equal priority and overlapping expressions is the most common source of "why did this publish to the wrong channel" bugs.

$rules = $repository->findBySubject($tenant);   // your implementation
usort($rules, fn ($a, $b) => $b->getPriority() <=> $a->getPriority());

foreach ($rules as $rule) {
    if ($evaluator->evaluate($rule, $params)) {
        return $rule;                            // first-match
    }
}

3. Processor pattern for multi-step workflows. RuleProcessor is what the SWP workflow actually calls, because it combines "load rules for this subject" + "evaluate" + "act." Mirror that structure in your own application service rather than scattering RuleEvaluator calls through controllers — one service that takes (subject, parameters) and returns a decision is far easier to test and to log against. If the bundled processor fits, use it; if it's too coupled to SWP's subject types, copy its shape rather than its imports.

4. Use configuration as the side-channel. An expression can only return a boolean; it cannot tell you which route to use. Newer versions of RuleInterface carry an arbitrary configuration array alongside the expression — put the destination, template ID, or channel there, and use the expression purely as the predicate. This is the single most useful design decision in the package and the one people miss, because they try to encode outcomes in the expression string.

5. Persist rules as data, not code. The whole point of a DB-backed RuleRepositoryInterface is that an editor can add a rule in the admin UI without a deploy. Consequence: every expression is untrusted input. Validate at write time with RuleValidatorInterface (parse the expression without evaluating it), and again — defensively — at read time, because expressions written against an old version of your model will still be sitting in the database after a refactor.

6. Validate with a dry run before saving. ExpressionLanguage::parse() catches syntax errors, but it will happily accept article.getMetadataByKey('typo') that blows up at runtime. In the admin save path, evaluate the rule against a sample subject inside a try/catch and surface the failure to the editor. This is the difference between a rules UI that feels solid and one that produces 500s at publish time.

7. Wire it through Symfony like any other library. Register the ExpressionLanguage, evaluator, processor, and your repository as services; keep the evaluator stateless and shared. If your version supports expression configurators / variable registration, use them to declare which domain objects are legal in expressions — it doubles as documentation and as the enforcement point for pattern 1.

Gotchas and Tips

  • The repository is read-only and frozen. PRs go to the Superdesk Web Publisher monorepo, not here. Fork the monorepo if you need changes; a PR against this subtree will be discarded. Assume no upstream fixes.

  • 2018 code on modern PHP. Expect PHP 7.1-era typing and deprecation notices on PHP 8.x (implicit nullable params are the usual offender). Pin the version, run your test suite, and budget for a local patch or a shim. There is no php constraint help coming.

  • ExpressionLanguage version coupling is the top install failure. The ExpressionLanguage API changed across Symfony majors (evaluate() argument handling, ExpressionFunction registration, getNames()). A Symfony 6 ExpressionLanguage against this package's evaluator will not work cleanly. Check SWP's own composer.lock for the known-good matrix rather than resolving the latest.

  • Expressions can call arbitrary methods on whatever you pass in. ExpressionLanguage is not a sandbox by default. Never put an EntityManager, a mailer, a filesystem service, or anything with a delete/flush/send method into the parameter bag. If rules are authorable by non-developers, subclass ExpressionLanguage to whitelist methods, or restrict the parameter objects to thin read-only DTOs with no side-effecting methods at all.

  • Two different failure modes, two different catches. SyntaxError (a \LogicException) means the expression doesn't parse — that's a data/validation bug. Anything thrown during evaluation (unknown method, TypeError, null deref) is a runtime bug. Catch them separately; conflating them makes "rule didn't match" and "rule is broken" indistinguishable in logs, which is exactly the signal you need in production.

  • Object access is getter-based. article.getTitle() is a method call. article.title also works if a getTitle()/isTitle() getter exists. There is no error at parse time for a missing getter — only at evaluation. This is the #1 source of "it worked in staging."

  • No expression caching. Every evaluate() re-parses. If you evaluate a rule set per-article across a batch import (thousands of items), cache ParsedExpression keyed by expression string in a static array or a PSR-6 pool. The gain is large because parsing dominates evaluation cost.

  • Don't serialize the evaluator. If rule evaluation happens in a queue worker, put the rule ID and subject reference in the message and rehydrate — ExpressionLanguage and the repository are not meant to be serialized into job payloads.

  • RuleInterface is anemic. The base model has no validation, no normalization, and no equality semantics. Implement the interface directly in your domain layer if you already have a Rule entity; extending the provided model couples your persistence schema to a 2018 package you can't patch.

  • Extension point that does exist: the evaluator and processor are both interface-backed and constructor-injectable, so the realistic extension strategy is decorate, don't modify — wrap RuleEvaluatorInterface with a logging/caching decorator and leave the vendored code untouched. That keeps you upgradeable if a security fix ever does land.

  • Zero stars is a signal, not a verdict. The code is production-proven inside SWP, but you're adopting an unmaintained internal component. If your rules are simple predicates over one or two objects, symfony/expression-language directly plus 40 lines of your own is a strictly better trade. Adopt swp/rule when you specifically need SWP compatibility or when its Rule/RuleCollection/repository shape matches an abstraction you've already built.

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.
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
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
cgv/localized-resource