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
- PHP 8.2+
composer require kevintherm/exprcFor Laravel QueryBuilder support:
composer require illuminate/databaseRule string -> Lexer -> Parser -> AST -> Evaluator
Core parser and AST are framework-agnostic. Evaluators are swappable adapters.
=/!=>/>=/</<=LIKE/NOT LIKEIN/NOT INCONTAINS/NOT CONTAINSIS NULL/IS NOT NULL
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)
- 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<?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);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();Supported syntax for parser and in-memory backend:
- dot notation:
user.profile.name - bracket notation:
tags[0],metadata['key'] - wildcard notation:
items[*]
Use either brackets or parentheses:
['active', 'pending']('active', 'pending')[1, 'two', true, null][]or()
Exprc is designed to be highly extensible via composition and 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 ruleAll 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
}
}The Lexer and Parser classes can be extended. Use the TokenRegistry to prepare for custom symbols (like @ for system variables or -> for JSON paths).
- 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.