kevintherm / exprc
Convert expression string to in-memory evaluation, SQL syntax, or something else.
Requires
- php: ^8.2
Requires (Dev)
- illuminate/database: ^10.0 || ^11.0 || ^12.0
- phpunit/phpunit: ^10.5
Suggests
- illuminate/database: Use QueryBuilderEvaluator to apply rules directly to Laravel query builders.
README
Exprc is a tiny, decoupled rule engine for PHP
It parses rule strings into an AST once, then lets you evaluate that AST with different backends:
- in-memory records
- Laravel query builders
- custom callback handlers
Prerequisites
- PHP 8.2+
Install
composer require kevintherm/exprc
For Laravel QueryBuilder support:
composer require illuminate/database
Architecture
Rule string -> Lexer -> Parser -> AST -> Evaluator
Core parser and AST are framework-agnostic. Evaluators are swappable adapters.
Supported Operators
=/!=>/>=/</<=LIKE/NOT LIKEIN/NOT INCONTAINS/NOT CONTAINSIS NULL/IS NOT NULL
Logical Operators & Precedence
Exprc uses SQL-style keywords for logical operations. Symbols like && or || are not supported.
NOT: Inverts the following expression (Highest precedence)AND: Matches only if both sides are trueOR: Matches if either side is true (Lowest precedence)
Use parentheses ( ) to group expressions and override default precedence:
status = 'active' AND (priority = 'high' OR is_urgent = true)
Comparison Values
- Literals:
'strings',"strings",42,10.5,true,false,null - Identifiers: Unquoted words on the right side are treated as field paths (e.g.,
verified = original_verified) - Arrays:
['a', 'b']or(1, 2, 3)
<?php use Kevintherm\Exprc\Exprc; $exprc = new Exprc(); $rule = "user.status = 'active' AND score >= 42"; $record = [ 'user' => ['status' => 'active'], 'score' => 100, ]; $matches = $exprc->matches($rule, $record); // true
Parse Once, Evaluate Many
<?php use Kevintherm\Exprc\Exprc; use Kevintherm\Exprc\Evaluators\InMemoryEpvaluator; use Kevintherm\Exprc\Evaluators\CallbackEvaluator; $exprc = new Exprc(); $ast = $exprc->parse("status IN ['active','pending'] AND tags CONTAINS 'beta'"); $inMemory = InMemoryEvaluator::for([ 'status' => 'active', 'tags' => ['beta', 'pro'], ])->evaluate($ast); $callback = CallbackEvaluator::for(function ($comparison): bool { return $comparison->field === 'status' && $comparison->value === 'active'; })->evaluate($ast);
Query Builder Usage (Laravel)
Exprc uses Laravel query methods for JSON paths and containment. No raw SQL is generated.
<?php use Kevintherm\Exprc\Exprc; use Kevintherm\Exprc\Resolvers\CollectionFieldResolver; $exprc = new Exprc(); $ast = $exprc->parse("metadata['version'] = 'v2' AND tags CONTAINS 'beta'"); $resolver = new CollectionFieldResolver( tableAlias: 'r', fieldMap: [ 'status' => 'state', ], ); $query = DB::table('rules as r'); QueryBuilderEvaluator::for($query, $resolver)->evaluate($ast); $results = $query->get();
Field Access
Supported syntax for parser and in-memory backend:
- dot notation:
user.profile.name - bracket notation:
tags[0],metadata['key'] - wildcard notation:
items[*]
Array Literals
Use either brackets or parentheses:
['active', 'pending']('active', 'pending')[1, 'two', true, null][]or()
Extension & Customization
Exprc is designed to be highly extensible via composition and visitors.
AST Node Visitors
You can traverse the AST before evaluation for static analysis or transformation using the VisitorInterface.
use Kevintherm\Exprc\Ast\VisitorInterface; use Kevintherm\Exprc\Ast\ComparisonNode; class RelationshipFinder implements VisitorInterface { public array $relationships = []; public function visitComparisonNode(ComparisonNode $node): mixed { if (str_contains($node->field, '.')) { $this->relationships[] = explode('.', $node->field)[0]; } return null; } // ... implement other methods } $finder = new RelationshipFinder(); $ast->accept($finder); // $finder->relationships now contains all base relations found in the rule
Custom AST Nodes & Evaluator Hooks
All core classes are non-final. You can extend ComparisonNode to create specialized nodes (like VeloquentComparisonNode) or use evaluator hooks:
class MyEvaluator extends InMemoryEvaluator { public function beforeProcessNode(Node $node): void { // Intercept node before evaluation } public function afterProcessNode(Node $node, mixed $result): void { // Log or transform result } }
Extensible Parsing
The Lexer and Parser classes can be extended. Use the TokenRegistry to prepare for custom symbols (like @ for system variables or -> for JSON paths).
Notes
- AST nodes are
readonlyvalue objects (PHP 8.2+). - Metadata-aware nodes can be created by extending base node classes.
- Evaluators support standard hooks for plugin-like behavior.
IS NULLandIS NOT NULLare treated as first-classNullComparisonNodeobjects.