jaspr / expression
Rich expression builder allows create complex closure expression tree to filter array of objects
Installs: 13 834
Dependents: 1
Suggesters: 0
Security: 0
Stars: 0
Forks: 0
pkg:composer/jaspr/expression
Requires
- php: ^8.1
Requires (Dev)
- ext-pdo: *
- ext-pdo_sqlite: *
- doctrine/collections: ^1.6
- doctrine/orm: ^2.7
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^9.4
- rector/rector: 2.0.0-rc2
- squizlabs/php_codesniffer: ^3.5
Suggests
- doctrine/collections: If you want use DoctrineCriteriaResolver
- doctrine/orm: If you want use DoctrineQueryResolver
README
A PHP library for creating rich expression trees to filter collections of objects and generate database queries. This library provides a fluent API for building complex filter expressions that can be resolved into closures for array filtering or SQL/DQL queries for database operations.
Table of Contents
Installation
composer require jaspr/expression
Requirements
- PHP ^8.1
Optional Dependencies
doctrine/collections- For DoctrineCriteriaResolverdoctrine/orm- For DoctrineQueryResolver
Core Concepts
The library is built around three main concepts:
- Expressions: Immutable objects representing operations (comparisons, functions, literals, fields)
- Expression Builder (Ex): Factory class for creating expressions using a fluent API
- Resolvers: Components that convert expressions into executable code or query strings
Expression Tree
Expressions form a tree structure where:
- Leaf nodes are literals (
Ex::literal()) or field references (Ex::field()) - Internal nodes are operations (comparisons, functions, arithmetic)
Type System
Every expression has a type:
TString- String valuesTNumeric- Integer and float valuesTBoolean- Boolean valuesTDateTime- DateTime valuesTArray- Array values
Quick Start
Filtering PHP Arrays
use JSONAPI\Expression\Ex;
use JSONAPI\Expression\Dispatcher\ClosureResolver;
// Create test data
$obj1 = new stdClass();
$obj1->name = "John";
$obj1->age = 25;
$obj2 = new stdClass();
$obj2->name = "Jane";
$obj2->age = 30;
$objects = [$obj1, $obj2];
// Build expression: age > 20 AND name starts with "J"
$expression = Ex::and(
Ex::gt(Ex::field('age', 'integer'), Ex::literal(20)),
Ex::startsWith(Ex::field('name'), Ex::literal('J'))
);
// Resolve to closure and filter
$resolver = new ClosureResolver();
$filter = $expression->resolve($resolver);
$result = array_filter($objects, $filter);
// Result: both objects match
Generating SQL WHERE Clauses
use JSONAPI\Expression\Ex;
use JSONAPI\Expression\Dispatcher\SQLiteResolver;
// Build expression
$expression = Ex::and(
Ex::eq(Ex::field('status'), Ex::literal('active')),
Ex::gt(Ex::field('price', 'double'), Ex::literal(100.0))
);
// Resolve to SQL
$resolver = new SQLiteResolver();
$whereClause = $expression->resolve($resolver);
$params = $resolver->getParams();
// Use in query
$sql = "SELECT * FROM products WHERE " . $whereClause;
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
Generating Doctrine DQL Expressions
use JSONAPI\Expression\Ex;
use JSONAPI\Expression\Dispatcher\DoctrineQueryResolver;
$expression = Ex::and(
Ex::eq(Ex::field('user.email'), Ex::literal('test@example.com')),
Ex::ge(Ex::field('user.age', 'integer'), Ex::literal(18))
);
$resolver = new DoctrineQueryResolver();
$dqlExpression = $resolver->dispatch($expression);
// Use with Doctrine QueryBuilder
$qb->where($dqlExpression);
API Reference
Expression Builder (Ex)
The Ex class provides static factory methods for building expressions.
Creating Values
Ex::literal($value): TString|TNumeric|TBoolean|TDateTime|TArray
Creates a literal value expression.
Parameters:
$value- string, int, float, bool, array, DateTimeInterface, or null
Returns: Expression matching the type of input value
Examples:
Ex::literal('hello') // TString
Ex::literal(42) // TNumeric
Ex::literal(3.14) // TNumeric
Ex::literal(true) // TBoolean
Ex::literal([1, 2, 3]) // TArray
Ex::literal(new DateTime()) // TDateTime
Ex::literal(null) // NullValue
Ex::field(string $name, string $type = 'string'): TString|TNumeric|TBoolean|TDateTime|TArray
Creates a field reference expression for accessing object properties.
Parameters:
$name- Property name$type- Type hint: 'string', 'integer', 'int', 'float', 'double', 'boolean', 'bool', 'datetime', or array variants with '[]' suffix
Returns: Expression of specified type
Examples:
Ex::field('name') // TString (default)
Ex::field('age', 'integer') // TNumeric
Ex::field('price', 'double') // TNumeric
Ex::field('isActive', 'boolean') // TBoolean
Ex::field('createdAt', 'datetime') // TDateTime
Ex::field('tags', 'string[]') // TArray
Ex::field('scores', 'integer[]') // TArray
Comparison Operators
All comparison operators return TBoolean expressions.
Ex::eq($left, $right): TBoolean
Equal comparison (=).
Examples:
Ex::eq(Ex::field('status'), Ex::literal('active'))
Ex::eq(Ex::field('count', 'integer'), Ex::literal(0))
Ex::ne($left, $right): TBoolean
Not equal comparison (!=, <>).
Examples:
Ex::ne(Ex::field('status'), Ex::literal('deleted'))
Ex::lt($left, $right): TBoolean
Less than comparison (<).
Examples:
Ex::lt(Ex::field('age', 'integer'), Ex::literal(18))
Ex::le($left, $right): TBoolean
Less than or equal comparison (<=).
Examples:
Ex::le(Ex::field('price', 'double'), Ex::literal(100.0))
Ex::gt($left, $right): TBoolean
Greater than comparison (>).
Examples:
Ex::gt(Ex::field('score', 'integer'), Ex::literal(90))
Ex::ge($left, $right): TBoolean
Greater than or equal comparison (>=).
Examples:
Ex::ge(Ex::field('age', 'integer'), Ex::literal(21))
Ex::in(TNumeric|TString $left, TArray $right): TBoolean
IN comparison - checks if value exists in array.
Examples:
Ex::in(
Ex::field('status'),
Ex::literal(['active', 'pending', 'approved'])
)
Ex::in(
Ex::field('id', 'integer'),
Ex::literal([1, 2, 3, 4, 5])
)
Ex::has(TArray $array, TString|TNumeric $value): TBoolean
HAS comparison - checks if array contains value (reverse of IN).
Examples:
Ex::has(
Ex::field('tags', 'string[]'),
Ex::literal('important')
)
Ex::be(TNumeric|TDateTime $value, TNumeric|TDateTime $from, TNumeric|TDateTime $to): TBoolean
BETWEEN comparison - checks if value is between two values (inclusive).
Examples:
Ex::be(
Ex::field('age', 'integer'),
Ex::literal(18),
Ex::literal(65)
)
Ex::be(
Ex::field('createdAt', 'datetime'),
Ex::literal(new DateTime('2020-01-01')),
Ex::literal(new DateTime('2020-12-31'))
)
Logical Operators
Ex::and(TBoolean $left, TBoolean $right): TBoolean
Logical AND - both conditions must be true.
Examples:
Ex::and(
Ex::eq(Ex::field('status'), Ex::literal('active')),
Ex::gt(Ex::field('age', 'integer'), Ex::literal(18))
)
Ex::or(TBoolean $left, TBoolean $right): TBoolean
Logical OR - at least one condition must be true.
Examples:
Ex::or(
Ex::eq(Ex::field('role'), Ex::literal('admin')),
Ex::eq(Ex::field('role'), Ex::literal('moderator'))
)
Ex::not(TBoolean $expression): TBoolean
Logical NOT - negates the expression.
Examples:
Ex::not(Ex::eq(Ex::field('status'), Ex::literal('deleted')))
Ex::not(Ex::startsWith(Ex::field('email'), Ex::literal('spam')))
String Functions
Ex::length(TString $subject): TNumeric
Returns the length of a string.
Examples:
Ex::eq(Ex::length(Ex::field('name')), Ex::literal(5))
Ex::concat(TString $subject, TString $append): TString
Concatenates two strings.
Examples:
Ex::concat(Ex::field('firstName'), Ex::literal(' '))
Ex::concat(Ex::field('firstName'), Ex::field('lastName'))
Ex::contains(TString $haystack, TString $needle): TBoolean
Checks if string contains substring.
Examples:
Ex::contains(Ex::field('email'), Ex::literal('@example.com'))
Ex::contains(Ex::field('description'), Ex::literal('urgent'))
Ex::startsWith(TString $haystack, TString $needle): TBoolean
Checks if string starts with substring.
Examples:
Ex::startsWith(Ex::field('name'), Ex::literal('John'))
Ex::startsWith(Ex::field('sku'), Ex::literal('PRD-'))
Ex::endsWith(TString $haystack, TString $needle): TBoolean
Checks if string ends with substring.
Examples:
Ex::endsWith(Ex::field('email'), Ex::literal('.com'))
Ex::endsWith(Ex::field('filename'), Ex::literal('.pdf'))
Ex::indexOf(TString $haystack, TString $needle): TNumeric
Returns the position of substring in string (0-based), or -1 if not found.
Examples:
Ex::eq(Ex::indexOf(Ex::field('text'), Ex::literal('error')), Ex::literal(0))
Ex::substring(TString $string, TNumeric $start, ?TNumeric $length = null): TString
Extracts substring from string.
Examples:
Ex::substring(Ex::field('code'), Ex::literal(0), Ex::literal(3))
Ex::substring(Ex::field('text'), Ex::literal(5)) // From position 5 to end
Ex::matchesPattern(TString $subject, TString $pattern): TBoolean
Checks if string matches regex pattern.
Examples:
Ex::matchesPattern(Ex::field('email'), Ex::literal('/^[a-z]+@[a-z]+\.[a-z]+$/'))
Ex::matchesPattern(Ex::field('phone'), Ex::literal('/^\+?[0-9]{10,15}$/'))
Ex::toLower(TString $subject): TString
Converts string to lowercase.
Examples:
Ex::eq(Ex::toLower(Ex::field('status')), Ex::literal('active'))
Ex::toUpper(TString $subject): TString
Converts string to uppercase.
Examples:
Ex::eq(Ex::toUpper(Ex::field('code')), Ex::literal('USA'))
Ex::trim(TString $subject): TString
Removes whitespace from beginning and end of string.
Examples:
Ex::eq(Ex::trim(Ex::field('name')), Ex::literal('John'))
Numeric Functions
Ex::ceiling(TNumeric $value): TNumeric
Rounds number up to nearest integer.
Examples:
Ex::eq(Ex::ceiling(Ex::field('price', 'double')), Ex::literal(100))
Ex::floor(TNumeric $value): TNumeric
Rounds number down to nearest integer.
Examples:
Ex::eq(Ex::floor(Ex::field('rating', 'double')), Ex::literal(4))
Ex::round(TNumeric $value): TNumeric
Rounds number to nearest integer.
Examples:
Ex::eq(Ex::round(Ex::field('average', 'double')), Ex::literal(5))
DateTime Functions
Ex::date(TDateTime $datetime): TDateTime
Extracts date part from datetime (time set to 00:00:00).
Examples:
Ex::eq(
Ex::date(Ex::field('createdAt', 'datetime')),
Ex::date(Ex::literal(new DateTime('2020-12-31')))
)
Ex::time(TDateTime $datetime): TDateTime
Extracts time part from datetime.
Examples:
Ex::eq(
Ex::time(Ex::field('scheduledAt', 'datetime')),
Ex::time(Ex::literal(new DateTime('15:30:00')))
)
Ex::year(TDateTime $datetime): TNumeric
Extracts year from datetime.
Examples:
Ex::eq(Ex::year(Ex::field('createdAt', 'datetime')), Ex::literal(2020))
Ex::month(TDateTime $datetime): TNumeric
Extracts month from datetime (1-12).
Examples:
Ex::eq(Ex::month(Ex::field('createdAt', 'datetime')), Ex::literal(12))
Ex::day(TDateTime $datetime): TNumeric
Extracts day of month from datetime (1-31).
Examples:
Ex::eq(Ex::day(Ex::field('createdAt', 'datetime')), Ex::literal(25))
Ex::hour(TDateTime $datetime): TNumeric
Extracts hour from datetime (0-23).
Examples:
Ex::ge(Ex::hour(Ex::field('createdAt', 'datetime')), Ex::literal(9))
Ex::minute(TDateTime $datetime): TNumeric
Extracts minute from datetime (0-59).
Examples:
Ex::eq(Ex::minute(Ex::field('scheduledAt', 'datetime')), Ex::literal(30))
Ex::second(TDateTime $datetime): TNumeric
Extracts second from datetime (0-59).
Examples:
Ex::eq(Ex::second(Ex::field('timestamp', 'datetime')), Ex::literal(0))
Mathematical Operations
All mathematical operations return TNumeric.
Ex::add(TNumeric $x, TNumeric $y): TNumeric
Addition.
Examples:
Ex::eq(Ex::add(Ex::field('quantity', 'integer'), Ex::literal(5)), Ex::literal(10))
Ex::sub(TNumeric $x, TNumeric $y): TNumeric
Subtraction.
Examples:
Ex::gt(Ex::sub(Ex::field('stock', 'integer'), Ex::literal(10)), Ex::literal(0))
Ex::mul(TNumeric $x, TNumeric $y): TNumeric
Multiplication.
Examples:
Ex::eq(
Ex::mul(Ex::field('price', 'double'), Ex::literal(1.1)),
Ex::literal(110.0)
)
Ex::div(TNumeric $x, TNumeric $y): TNumeric
Division.
Examples:
Ex::eq(
Ex::div(Ex::field('total', 'integer'), Ex::field('count', 'integer')),
Ex::literal(5)
)
Ex::mod(TNumeric $x, TNumeric $y): TNumeric
Modulo (remainder).
Examples:
Ex::eq(Ex::mod(Ex::field('id', 'integer'), Ex::literal(2)), Ex::literal(0)) // Even IDs
Resolvers
Resolvers convert expression trees into executable code or query strings.
ClosureResolver
Converts expressions to PHP closures for filtering arrays.
Usage:
use JSONAPI\Expression\Dispatcher\ClosureResolver;
$resolver = new ClosureResolver();
$closure = $expression->resolve($resolver);
$filtered = array_filter($array, $closure);
Features:
- Full support for all expression types
- Direct PHP execution
- No external dependencies
Best for: Filtering in-memory collections
SQLiteResolver
Converts expressions to SQLite-compatible SQL WHERE clauses.
Usage:
use JSONAPI\Expression\Dispatcher\SQLiteResolver;
$resolver = new SQLiteResolver();
$whereClause = $expression->resolve($resolver);
$params = $resolver->getParams();
$sql = "SELECT * FROM table WHERE " . $whereClause;
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
Features:
- Parameterized queries (SQL injection safe)
- SQLite-specific functions
- Get parameters via
getParams()
Best for: SQLite database queries
PostgresSQLResolver
Converts expressions to PostgreSQL-compatible SQL WHERE clauses.
Usage:
use JSONAPI\Expression\Dispatcher\PostgresSQLResolver;
$resolver = new PostgresSQLResolver();
$whereClause = $expression->resolve($resolver);
$params = $resolver->getParams();
$sql = "SELECT * FROM table WHERE " . $whereClause;
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
Features:
- PostgreSQL-specific functions
- Parameterized queries
- Get parameters via
getParams()
Best for: PostgreSQL database queries
DoctrineQueryResolver
Converts expressions to Doctrine Query Language (DQL) expressions.
Usage:
use JSONAPI\Expression\Dispatcher\DoctrineQueryResolver;
$resolver = new DoctrineQueryResolver();
$dqlExpr = $resolver->dispatch($expression);
$queryBuilder->where($dqlExpr);
Features:
- Returns Doctrine Expr objects
- Compatible with QueryBuilder
- Partial implementation (some methods throw NotImplemented)
Supported:
- Comparison operators (eq, ne, lt, le, gt, ge, in, between)
- Logical operators (and, or, not)
- String functions (contains, startsWith, endsWith, concat, length, trim, toLower, toUpper, substring)
- Math operations (add, sub, mul, div)
Not Supported:
- DateTime extraction functions (year, month, day, hour, minute, second, date, time)
- Numeric functions (ceiling, floor, round)
- String function (indexOf, matchesPattern)
- Math operation (mod)
- Array comparison (has)
Best for: Doctrine ORM queries
DoctrineCriteriaResolver
Converts expressions to Doctrine Criteria for collection filtering.
Usage:
use JSONAPI\Expression\Dispatcher\DoctrineCriteriaResolver;
$resolver = new DoctrineCriteriaResolver();
$criteria = $expression->resolve($resolver);
$filtered = $collection->matching($criteria);
Best for: Filtering Doctrine collections
Usage Examples
Complex Filtering Example
use JSONAPI\Expression\Ex;
use JSONAPI\Expression\Dispatcher\ClosureResolver;
// Find active users aged 18-65 whose email ends with company domain
$expression = Ex::and(
Ex::and(
Ex::eq(Ex::field('status'), Ex::literal('active')),
Ex::be(Ex::field('age', 'integer'), Ex::literal(18), Ex::literal(65))
),
Ex::or(
Ex::endsWith(Ex::field('email'), Ex::literal('@company.com')),
Ex::endsWith(Ex::field('email'), Ex::literal('@company.org'))
)
);
$resolver = new ClosureResolver();
$filter = $expression->resolve($resolver);
$result = array_filter($users, $filter);
Database Query with Complex Conditions
use JSONAPI\Expression\Ex;
use JSONAPI\Expression\Dispatcher\SQLiteResolver;
// Find products: (category is electronics OR computers) AND price between 100-1000 AND in stock
$expression = Ex::and(
Ex::and(
Ex::or(
Ex::eq(Ex::field('category'), Ex::literal('electronics')),
Ex::eq(Ex::field('category'), Ex::literal('computers'))
),
Ex::be(Ex::field('price', 'double'), Ex::literal(100.0), Ex::literal(1000.0))
),
Ex::gt(Ex::field('stock', 'integer'), Ex::literal(0))
);
$resolver = new SQLiteResolver();
$where = $expression->resolve($resolver);
$params = $resolver->getParams();
$sql = "SELECT * FROM products WHERE " . $where;
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$products = $stmt->fetchAll(PDO::FETCH_ASSOC);
String Manipulation Example
use JSONAPI\Expression\Ex;
// Find users whose trimmed uppercase username starts with 'ADMIN'
$expression = Ex::startsWith(
Ex::toUpper(Ex::trim(Ex::field('username'))),
Ex::literal('ADMIN')
);
// Find posts where title contains 'important' and has minimum length
$expression = Ex::and(
Ex::contains(Ex::toLower(Ex::field('title')), Ex::literal('important')),
Ex::ge(Ex::length(Ex::field('title')), Ex::literal(10))
);
Date/Time Filtering Example
use JSONAPI\Expression\Ex;
// Find records created in December 2020
$expression = Ex::and(
Ex::eq(Ex::year(Ex::field('createdAt', 'datetime')), Ex::literal(2020)),
Ex::eq(Ex::month(Ex::field('createdAt', 'datetime')), Ex::literal(12))
);
// Find events scheduled between 9 AM and 5 PM
$expression = Ex::and(
Ex::ge(Ex::hour(Ex::field('scheduledAt', 'datetime')), Ex::literal(9)),
Ex::lt(Ex::hour(Ex::field('scheduledAt', 'datetime')), Ex::literal(17))
);
// Find records created today
$expression = Ex::eq(
Ex::date(Ex::field('createdAt', 'datetime')),
Ex::date(Ex::literal(new DateTime()))
);
Mathematical Operations Example
use JSONAPI\Expression\Ex;
// Find products where discounted price (price * 0.9) is less than 50
$expression = Ex::lt(
Ex::mul(Ex::field('price', 'double'), Ex::literal(0.9)),
Ex::literal(50.0)
);
// Find orders where average item price (total / quantity) exceeds 100
$expression = Ex::gt(
Ex::div(Ex::field('total', 'double'), Ex::field('quantity', 'integer')),
Ex::literal(100.0)
);
// Find even-numbered records
$expression = Ex::eq(
Ex::mod(Ex::field('id', 'integer'), Ex::literal(2)),
Ex::literal(0)
);
Array Operations Example
use JSONAPI\Expression\Ex;
// Find users with specific role
$expression = Ex::in(
Ex::field('role'),
Ex::literal(['admin', 'moderator', 'editor'])
);
// Find products with 'featured' tag
$expression = Ex::has(
Ex::field('tags', 'string[]'),
Ex::literal('featured')
);
Type System
The library uses a strict type system to ensure type safety:
Type Classes
TString- String values and expressionsTNumeric- Numeric values (integers and floats)TBoolean- Boolean values and comparison resultsTDateTime- DateTime values and date/time operationsTArray- Array values
Type Checking
Operations verify type compatibility at runtime:
// Valid - comparing same types
Ex::eq(Ex::field('name'), Ex::literal('John'))
// Valid - numeric comparison
Ex::gt(Ex::field('age', 'integer'), Ex::literal(18))
// Invalid - will throw IncomparableExpressions
Ex::eq(Ex::field('name'), Ex::literal(123))
Field Type Hints
Specify field types to ensure correct handling:
Ex::field('id', 'integer') // Integer field
Ex::field('price', 'double') // Float field
Ex::field('active', 'boolean') // Boolean field
Ex::field('createdAt', 'datetime') // DateTime field
Ex::field('tags', 'string[]') // String array field
License
MIT License
Author
Tomas Benedikt (tomas.benedikt@gmail.com)
Contributing
This library follows PSR standards and includes comprehensive test coverage. See tests directory for usage examples.