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

intaro/rule-engine-bundle

A Symfony bundle that adds a configurable rule engine to your app: define conditions and actions, evaluate them against domain objects, and run business logic declaratively. Extendable, expression-driven, and easy to wire into existing services.

View on GitHub
Deep Wiki
Context7

Getting Started

The first thing to internalise: this is a Symfony bundle, not a Laravel package. It has no service provider, no facade, no config file in config/. Composer will happily install it, but nothing boots. Treat it as a library whose two moving parts — symfony/expression-language and an event dispatcher — you wire into Laravel yourself.

Minimal steps:

  1. composer require intaro/rule-engine-bundle:^1.0 symfony/expression-language
  2. Read the bundle's Resources/config/ (or DependencyInjection/Configuration.php) first — that is the authoritative schema for what a rule looks like. Then read tests/; for a 2018-era bundle with 6 stars, the test suite is the real documentation.
  3. Accept the mental model before writing code: a Rule = N Conditions (ExpressionLanguage strings) + M Actions (things executed when all conditions pass). Rules are triggered by events flowing through an EventDispatcher.
  4. Pick one narrow first use case, e.g. "when an OrderPlaced event fires, if order.total > 500 and customer.tier == 'gold', dispatch ApplyLoyaltyDiscount". Implement it by hand with ExpressionLanguage first, then decide whether the bundle's Rule/Action plumbing earns its keep.

A minimal integration sketch in Laravel:

use Symfony\Component\ExpressionLanguage\ExpressionLanguage;

$el = new ExpressionLanguage();

$rules = [
    [
        'conditions' => ['order.total > 500', "customer.tier == 'gold'"],
        'actions'    => [ApplyLoyaltyDiscount::class],
    ],
];

Event::listen(OrderPlaced::class, function ($event) use ($el, $rules) {
    $context = ['order' => $event->order, 'customer' => $event->customer];

    foreach ($rules as $rule) {
        foreach ($rule['conditions'] as $expr) {
            if (! $el->evaluate($expr, $context)) {
                continue 2; // all conditions must pass
            }
        }
        event(new $rule['actions'][0]($event->order));
    }
});

Implementation Patterns

Config-driven vs. code-driven rules. The bundle's natural mode is declarative: rules defined in YAML and loaded once at container build time. In Laravel the analogue is a config/rules.php array (or a database table + cache) loaded by a RuleRepository. Keep the declarative shape — it is the main reason to consider this bundle at all — but own the loader, because the bundle's loader is a Symfony Extension you cannot use.

Event-first triggering. Don't call the engine from controllers. Register one wildcard-ish listener (or a dedicated EventServiceProvider entry) per domain event and hand the event payload to the engine. This keeps business rules out of your HTTP layer and makes them testable by dispatching events in a unit test.

Build a context array deliberately. ExpressionLanguage only sees what you pass it. Write an explicit toRuleContext() on your events or a small context factory — never (array) $event or get_object_vars(). This is your security boundary and your decoupling point: renaming a model property should break one factory method, not 40 expression strings.

Actions that dispatch events, not actions that do work. The composable pattern is: a rule's action emits a domain event; normal Laravel listeners do the work (mail, webhook, DB write). Rules then compose — rule A's action can satisfy rule B's trigger. Keep actual side effects in listeners where they are retryable, queued, and observable.

Rule ordering and match semantics. Decide up-front whether your engine is first-match (like a firewall / state machine) or all-match (like validation). The bundle leans toward evaluate-all-and-execute-matching; if you want first-match, sort by an explicit priority key rather than relying on array order — YAML/PHP array order is too fragile for business logic.

Testing. ExpressionLanguage expressions are pure functions of the context — assert them directly with $el->evaluate($expr, $fixtureContext) in fast tests, and reserve full-engine tests for wiring. This is far cheaper than booting the app to test a condition.

Gotchas and Tips

  • It is effectively abandoned. Last release April 2018, zero dependents, six stars. Expect Symfony 2/3-era code: ContainerAwareInterface, old EventDispatcherInterface signatures, possibly PHP 7.0 constraints. Assume you will not get upstream fixes; budget for vendoring or forking if you adopt it.
  • Laravel's dispatcher is not Symfony's. Illuminate\Contracts\Events\Dispatcher does not implement Symfony\Component\EventDispatcher\EventDispatcherInterface. If the bundle type-hints the Symfony interface anywhere, you need an adapter or you need to run a standalone symfony/event-dispatcher instance alongside Laravel's. Verify this before committing — it is the most likely blocker.
  • ExpressionLanguage is remote code execution if you feed it untrusted input. Anyone who can write a rule string can potentially call methods on objects you expose (order.delete(), config('app.key') via a registered function, static calls). Rules must be developer-authored and code-reviewed, or you must run expressions through a hardened evaluation path with a function whitelist. Never expose a "user defines rules in the admin panel" feature over raw ExpressionLanguage.
  • Cache parsed expressions. ExpressionLanguage::parse() returns a ParsedExpression; evaluating a string re-parses and re-compiles it every time. For rules evaluated on a hot event, cache parsed nodes (Laravel cache or an in-process array keyed by the expression string).
  • Guard against event loops. Rule actions that dispatch events which re-trigger rules are the classic infinite recursion. Add a per-request depth counter or a "seen rules" set, and make it a hard failure with a clear log line.
  • Debug by logging the decision, not just the outcome. Log rule_name, the rendered expression, the context keys (not full payloads — PII), and the boolean result. When a rule "doesn't fire" the cause is almost always a missing context key or a type mismatch (string "500" vs int 500), and only the expression-plus-context pair tells you which.
  • Extension points to use, not fight: custom ExpressionLanguage providers (register your own domain functions like has_role(), in_business_hours()) and custom rule sources (DB-backed rules behind a cache). Both are cheap to implement yourself and give you the bundle's ergonomics without its container coupling.
  • Honest alternative: for most Laravel apps, symfony/expression-language plus ~80 lines of rule repository + listener glue reproduces the useful 90% of this bundle, with no abandoned dependency and full control over the dispatcher. Reach for the bundle only if you want its specific Rule/Condition/Action serialization format — for example to persist rules authored elsewhere.
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.
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle