symftony/xpression
PHP expression language that parses and compiles dynamic string expressions, letting you safely evaluate configurable logic with custom functions, variables, and operators—ideal for business rules, configs, and validation.
Install it with Composer:
composer require symftony/xpression
Before writing any business logic, spend ten minutes reading the package source and tests in vendor/symftony/xpression/. This is a small library (17 stars, MIT), so the entire surface area is likely a handful of classes: a specification contract, one or more composite specifications, and maybe a base/abstract class. Confirm three things first:
isSatisfiedBy($candidate): bool.AndSpecification, OrSpecification, NotSpecification, or fluent and()/or()/not() methods. Do not assume; check the tests.A minimal first use case is an eligibility check that currently lives as a tangle of if statements — for example, "can this user check out?":
final class HasVerifiedEmail implements Specification
{
public function isSatisfiedBy($user): bool
{
return $user->email_verified_at !== null;
}
}
final class HasActiveSubscription implements Specification
{
public function isSatisfiedBy($user): bool
{
return $user->subscription?->active() === true;
}
}
Then compose at the edge:
$canCheckout = (new HasVerifiedEmail())->and(new HasActiveSubscription());
if (! $canCheckout->isSatisfiedBy($user)) {
abort(403, 'Account not eligible for checkout.');
}
The first win is not architectural purity — it is that each rule gets a name, a test, and a single place to change.
One spec per business rule, named as a question. OrderExceedsFreeShippingThreshold, CustomerIsInGoodStanding, CouponIsWithinValidityWindow. Keep isSatisfiedBy() pure: input in, boolean out, no writes, no events, no logging side effects.
Compose at the application edge, not inside specs. Atomic specs should not know about each other. Build composites in controllers, form requests, policies, or dedicated "eligibility" services so the composition is visible and easy to change. If you find a spec constructing another spec in its constructor, that is a sign the rule should be split.
Wrap the package behind your own contract if you plan to use it widely. A one-method interface in your domain (App\Specifications\Specification) with an adapter to the package costs little and protects you from a small, low-traffic dependency changing or disappearing. It also lets you add explain() later without touching every rule.
Use it in Laravel's validation and authorization layers. Specs compose naturally into FormRequest::passes() overrides, Gate::define() callbacks, and job middleware. For example, a Gate can delegate directly to a composite spec, keeping the rule testable outside HTTP tests.
Two-track approach for Eloquent. This package evaluates in memory against a candidate object. It will not translate to SQL. Use specs for already-loaded models and small collections; use query scopes or dedicated query objects for database filtering. Do not try to make one class serve both unless the package explicitly supports it — the mismatch (hydration, N+1, memory) is where most pain comes from.
// In-memory: fine for a single $order or a small collection.
$eligible = $orders->filter(fn ($o) => $spec->isSatisfiedBy($o));
// Database: use a scope, not the spec.
Order::whereNotNull('paid_at')->where('total', '>', 5000)->get();
Test atomics individually, then composites as truth tables. Data providers work well: verified/unverified × active/inactive, asserting the composite returns the expected boolean. This is where the pattern pays for itself — the truth table documents the business rule.
Bind frequently used composites in a service provider. If the same CanCheckout composition is used in a controller, a policy, and a job, register it as a singleton and inject it. This avoids drift where three copies of the composition slowly diverge.
In-memory evaluation is the default constraint. Filtering a large Eloquent collection through specs hydrates everything first. For anything more than a few hundred rows, push the filter into SQL and reserve specs for the final decision on a loaded model.
Avoid hidden queries inside isSatisfiedBy(). It is tempting to lazy-load a relation inside a spec. That converts a cheap boolean into an N+1 and makes the spec untestable without a database. Prefer requiring the needed data via constructor arguments or eager loading.
Composites return only a boolean. When a composite says "no," you often need to know which child failed. Add a small diagnostic wrapper (firstFailing($candidate)) or log at the atomic level during development. Do not bake logging into the specs themselves.
Keep specs stateless and cheap to construct. Constructor arguments are fine (thresholds, dates, IDs), but mutable state shared across calls will cause confusing bugs in long-running workers and queued jobs.
Watch null and type handling. isSatisfiedBy() may accept a loosely typed candidate. Use strict comparisons and explicit null checks; PHP's truthiness will happily turn 0, '', and null into the same answer.
Serialization matters if specs go into queues or cache keys. Closures and un-serializable dependencies inside specs will break queue payloads. Prefer plain scalar constructor arguments.
Pin the version. With 17 stars and an unspecified upstream repository, treat this as a dependency you control: commit the lockfile, and consider forking or vendoring if the package becomes load-bearing. The pattern is more valuable than the implementation — if the package stalls, a 100-line internal version is a realistic fallback.
Don't over-apply it. A rule used once, in one place, is fine as a private method. The Specification pattern earns its keep when rules are reused, composed, or need to be explained to non-developers; using it for every conditional creates indirection without payoff.
Check PHP version and autoload constraints early. Small packages often target older PHP versions; confirm compatibility with your Laravel version before adopting it broadly.
How can I help you explore Laravel packages today?