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

Php Expression Laravel Package

kitano/php-expression

kitano/php-expression is a lightweight PHP library for parsing and evaluating expressions safely without eval. It supports variables, operators, and functions, making it useful for dynamic formulas, rules, and configurable logic in PHP applications.

View on GitHub
Deep Wiki
Context7

Getting Started

There is nothing Laravel-specific here — no service provider, no facade, no config file, no auto-discovery. It's a plain Composer package, so installation is a single line and everything after that is your responsibility:

composer require kitano/php-expression

Where to look first: the src/ directory. The library is small enough to read end-to-end in a sitting, and you should — with ~30 stars and no dependents, the source is the documentation. Expect three layers:

  1. Parser — takes a source string and produces an AST.
  2. Compiler — takes the AST and emits a PHP code fragment (a string).
  3. An Expression-style wrapper — parse + compile + evaluate behind one call, for when you just want a value back.

Confirm the exact class names and signatures against src/ before you write anything; the README is thin and the API surface may differ slightly from what the assessments describe.

Do this before you evaluate anything. Dump the compiled output — it is the single fastest way to understand what the library actually does with your input, and it tells you immediately whether you're comfortable with what ends up in eval:

use Kitano\PhpExpression\Parser;
use Kitano\PhpExpression\Compiler;

$ast  = (new Parser)->parse('order.total >= 100 and order.country == "FR"');
$code = (new Compiler)->compile($ast);

var_dump($code); // read this carefully before running it

First use case: the one this package is genuinely good for — business rules stored as data rather than code. A shipping eligibility condition, a discount formula, or a notification trigger that an admin edits in a text field, persists to a rules table, and that your application evaluates against an order or user.

Keep the first slice deliberately small:

  • one migration with expression (source) and compiled (code) columns,
  • one RuleEvaluator service that loads, caches, and evaluates,
  • one admin form that saves a rule and returns a 422 if it doesn't parse.

Do not start with a rule-builder UI, a DSL spec, or a generic "conditions" model. Ship one hardcoded rule as an expression, prove the plumbing, and then decide whether to generalize.

Implementation Patterns

Compile once, evaluate many. Parsing is the expensive step; the compiled fragment is a plain string you can cache indefinitely. Key the cache on the source plus the package version so that a composer update silently invalidates every stale fragment:

$code = Cache::rememberForever('expr:'.sha1($source.'|'.self::VERSION), function () use ($source) {
    return (new Compiler)->compile((new Parser)->parse($source));
});

Persist expression and compiled side by side on the rule row. The source is for humans and recompilation; compiled is the runtime artifact. Your read path then never touches the parser.

Wrap evaluation in a dedicated service — never call eval from a controller, model, job, or Blade template. One class, one method, one place to add logging, timeout guards, and error mapping:

final class RuleEvaluator
{
    public function evaluate(string $compiled, array $context): mixed
    {
        return (static function (array $context) use ($compiled) {
            extract($context, EXTR_SKIP);
            return eval("return {$compiled};");
        })($context);
    }
}

The static closure matters: it isolates the eval scope so context keys become locals, and the service's own $this/$compiled can't be shadowed or leaked into the expression.

Bind it behind an application-owned interface. RuleEvaluator (or ExpressionEvaluator) should be an interface bound in a service provider, with the package behind it. This is what lets you swap in symfony/expression-language later without touching call sites — and given the adoption signals here, assume you may want to.

Pass an explicit context, never a whole model. Build the context array by hand from an allowlist:

$context = [
    'order' => [
        'total'    => (float) $order->total,
        'country'  => $order->shipping_country,
        'items'    => $order->items_count,
    ],
    'customer' => [
        'vip'      => $customer->is_vip,
        'age'      => $customer->age,
    ],
];

An Eloquent model handed straight to extract turns every attribute, relation, and method into expression surface area. Flatten to scalars and arrays.

Validate on write, not on read. When a rule is saved, parse it synchronously and reject with a 422 carrying the parser's message. A rule that fails to compile at 3am in a queue worker is far worse than a red form field. Compile and persist in the same transaction.

Fail closed. Any parse failure, undefined variable, or exception during evaluation should return false (or throw a domain exception your caller handles) — never true by default. A silent error on a discount rule is a revenue leak.

Test at two levels. Unit-test your RuleEvaluator with a handful of representative expressions. Separately keep a table-driven test of (expression, context, expected) fixtures for your actual business rules — that dataset becomes your safety net if you ever swap the engine.

Gotchas and Tips

The compiler emits PHP that goes through eval. That's the whole game. The library is safe against its own grammar but is not a sandbox for arbitrary PHP — you must verify yourself whether function calls, static access, or new-object syntax survive parsing. Test it explicitly: phpinfo(), system("ls"), getenv("APP_KEY") as expressions. If any of them compile, you cannot expose authoring to untrusted users without an allowlist layer in front.

Restrict authoring to admins, at minimum. With no sandbox guarantees, treat expression authoring as a privileged capability — the same trust level as editing code. If non-engineers must author rules, budget for a whitelist pass over the compiled output (reject (, $, ::, ->, and backticks if your rule grammar doesn't need them) before you even consider it.

PHP string semantics leak into your expressions. "FR" and 'FR' are not the same thing after a round-trip through the compiler, and a value the user typed as FR (unquoted) becomes a constant lookup — likely null with a warning. Document quoting rules in your admin UI's help text, or pre-quote string literals on the way in.

Undefined variables are silently null-ish. With loose PHP comparison, order.totl (typo) typo'd against null >= 100 doesn't blow up the way you'd hope. Populate every declared variable on every evaluation — including falsy defaults — so typos and genuine nulls are distinguishable in your logs.

Loose comparison bites on money and IDs. == on a float total or a string promo code compared to an int will do what PHP does. Cast aggressively when building the context, and prefer explicit comparison helpers in your rule documentation ("always compare money as >= against a literal with two decimals").

Expression cost is unbounded. There's no complexity limit in the library. A rule with thousands of or clauses, or a pathological nested expression, burns CPU on your hot path. Enforce a max source length and an operator count at write time; cache compiled output; call set_time_limit-style guards around evaluation in queue workers if you accept user input at all.

Cache invalidation must include the package version. Compiled fragments are tied to the compiler's output shape. A composer update that changes codegen, and your persisted compiled column quietly becomes wrong. Either version-key the cache (as above) or recompile every rule in a deploy step and record the version on the row.

Errors arrive as raw PHP, not Laravel exceptions. ParseError-like exceptions and eval warnings won't map to validation errors on their own. Translate once, in the adapter: parser exception → ValidationException, evaluation exception → a domain RuleEvaluationFailed exception with the rule id, source, and context attached for the log. Never let a raw eval warning reach a request cycle.

Check PHP/Laravel constraints before you commit. There is no framework-level compatibility guarantee and no CI matrix you can lean on. Run your app's oldest and newest supported PHP versions against it in your own CI before you depend on it in a critical path.

Pin the version and assume you may fork. With 0 dependents and modest activity, treat this as a vendored component: composer require kitano/php-expression:1.x with an exact constraint, a named owner, and a short note in your repo about where to patch if upstream stalls. MIT makes forking easy — the small source makes it practical too.

Debugging recipe. When a rule misbehaves, log three things together: the source, the compiled fragment, and the context array. Reproduce by dumping the compiled string and hand-evaluating it against the logged context — nine times out of ten the bug is a quoting issue or a missing context key, not the parser.

Compare before you build on it. Before you commit to the rules-table architecture, run the same five expressions through symfony/expression-language. It's maintained, widely used, sandboxed, and has a documented function/constant registration API. If it handles your grammar, it is the safer default — this package's edge is that it's tiny, dependency-free, and readable, which matters most if you intend to fork and own it.

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.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings