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

Expression Laravel Package

webmozart/expression

A lightweight PHP library for building and evaluating expressions as objects. Compose comparisons and logical operators (and, or, not), then evaluate them against values for reusable validation, filtering, and rule logic without hard-coded conditionals.

Deep Wiki
Context7

Getting Started

Install and pin. The package is abandoned (last tagged release 1.x, December 2015), so treat it as a fixed dependency: composer require webmozart/expression:^1.3 and commit the lock file. Do not expect upstream fixes — budget for reading the source.

Look here first, in this order:

  1. src/Expression.php — the two-method contract everything implements:
    interface Expression {
        public function evaluate($value);   // bool
        public function toString();         // human-readable rule
    }
    
  2. src/Expr.php — the static facade. It is the actual catalogue of what exists (constraints, logic combinators, selectors). Read it top to bottom once; it takes five minutes and saves you guessing class names.
  3. src/Constraint/, src/Logic/, src/Selector/ — the concrete node types. AndX / OrX are named with an X because and/or are reserved words.

First use case — a reusable business rule instead of a boolean soup:

use Webmozart\Expression\Expr;

$eligible = Expr::greaterThanOrEqual(18)
    ->andLessThan(65)
    ->andEquals('active');          // note: Equals is loose (==); Same is strict (===)

$eligible->evaluate(30);            // true
$eligible->evaluate(17);            // false

(array) $eligible;                  // or: $eligible->toString()
// greaterThanOrEqual(18) && lessThan(65) && equals("active")

Because it implements Expression, you can type-hint business rules in signatures: function canCheckout(User $u, Expression $rule): bool. That is the whole value proposition — a rule stops being a closure buried in a controller and becomes a named, composable, printable object.

Implementation Patterns

1. Compose rules, don't chain-construct them inline. Build small named factories once and combine at the call site. This is what makes the package pay off over array_filter callbacks:

final class UserRules
{
    public static function adult(): Expression { return Expr::greaterThanOrEqual(18); }
    public static function active(): Expression { return Expr::same('active'); }
}

$rule = Expr::andX(UserRules::adult(), UserRules::active(), Expr::not(Expr::isNull()));
array_filter($users, fn ($u) => $rule->evaluate($u));   // or use ->evaluate as callable

2. Filtering collections. evaluate() takes exactly one argument, so it is compatible with array_filter / Iterator filters when wrapped: array_filter($rows, [$rule, 'evaluate']).

3. Selectors for nested data. Do not flatten DTOs into arrays just to test them. Webmozart\Expression\Selector\Key, Property, and Method extract a sub-value before applying the inner expression:

use Webmozart\Expression\Selector\Key;

$rule = new Key('address', new Key('country', Expr::same('CH')));
$rule->evaluate(['address' => ['country' => 'CH']]);   // true

Use Property for objects (public props / getters) and Method for computed values.

4. Config-driven rules. The single highest-leverage integration: persist rules as arrays/JSON and hydrate them into an expression tree in one factory. That turns "editable business logic" into a data problem rather than a deploy.

function build(array $node): Expression {
    return match ($node['op']) {
        'and'    => Expr::andX(...array_map('build', $node['of'])),
        'or'     => Expr::orX(...array_map('build', $node['of'])),
        'not'    => Expr::not(build($node['of'])),
        'eq'     => Expr::same($node['value']),
        'gt'     => Expr::greaterThan($node['value']),
        'in'     => Expr::in($node['values']),
        default  => throw new InvalidArgumentException($node['op']),
    };
}

5. Walking the tree — the extension point that matters. Traversal\ExpressionWalker + ExpressionVisitor let you convert an expression tree into something else (SQL WHERE, a Doctrine QueryBuilder, an Elasticsearch filter, an audit-log string). Write one visitor per target rather than one giant if on expression class names:

final class SqlVisitor implements ExpressionVisitor
{
    public function visitSame(Same $expr): string  { return $this->col.' = :p'.(++$this->i); }
    public function visitGreaterThan(GreaterThan $e): string { /* ... */ }
    // implement enterExpression()/leaveExpression() to wrap children in parentheses
}

This is the pattern that justifies the package over closures: a closure can only answer yes/no, an expression tree can be translated.

6. Validation / guard clauses. Use expressions to validate command and DTO input, and emit toString() in the exception message so the failure is self-describing: throw new DomainException('Rejected: '.$rule->toString()); That beats "constraint violation" messages for debuggability.

7. Testing. Table-drive against evaluate(): [[17, false], [18, true], [64, true], [65, false]]. Assert on toString() only for stable, hand-built trees — the output format is not a contract.

Gotchas and Tips

  • Equals is loose (==), Same is strict (===). This is the number-one source of silent bugs: Expr::equals(0)->evaluate(null) is true pre-PHP-8-ish semantics and '1' == '01' surprises abound. Default to Expr::same() for scalars unless you specifically want juggling.
  • Immutability means chaining returns new objects. $expr->andX($other); alone does nothing useful — the combinator returns a new tree. Assign the result. This bites every first-time user coming from mutable builders.
  • Argument direction. Expr::equals('foo') builds "the evaluated value equals 'foo'" — the expected value is the constructor argument, the actual value is the evaluate() argument. Reversing them still "works" for symmetric constraints and fails confusingly for StartsWith, Contains, GreaterThan.
  • Missing keys/selectors. Decide and document what a missing Key yields (null vs. exception) and prefer KeyExists/KeyNotExists in front of a deeper selector rather than relying on null propagation.
  • Vacuous truth. An empty AndX evaluates true and an empty OrX evaluates false (standard logic). A config-driven builder that silently produces an empty AndX from a malformed rule therefore allows everything. Validate your rule config before building.
  • PHP version drift. This was written for PHP 5.x-era signatures. On PHP 8 expect untyped params (fine), but be alert to deprecation notices and to any use of dynamic properties or each()-era helpers. Wrap in your own test suite — the package's own CI is dead and will not catch platform regressions for you.
  • Performance. Object graphs are far heavier than closures. Don't build expressions inside hot loops — build once, reuse. If a rule is evaluated millions of times, cache a compiled closure alongside it (fn ($v) => $rule->evaluate($v)), or bypass the package on the hot path.
  • Not round-trippable. toString() is for humans; there is no parser to read it back, and not every constraint prints unambiguously. Serialize your config (arrays), never the toString() output.
  • Combining precedence. AndX/OrX nest, so a && (b || c) must be built as andX($a, orX($b, $c)). The tree — not operator precedence — is the semantics; draw it before you ship a complex rule.
  • Prefer the facade. Newing up Constraint\* classes couples you to internal names that won't change (the package is frozen) but will read worse. Expr::* is the intended surface.
  • Exit strategy. Because the API is tiny (two methods on Expression), you can vendor a 200-line replacement later or wrap the package behind your own Rule interface now. Do the wrap if this is load-bearing: it costs an hour and makes the abandonment a non-event.
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.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons