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

1.1.4 2024-12-07 22:08 UTC

This package is auto-updated.

Last update: 2026-01-11 23:15:49 UTC


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 DoctrineCriteriaResolver
  • doctrine/orm - For DoctrineQueryResolver

Core Concepts

The library is built around three main concepts:

  1. Expressions: Immutable objects representing operations (comparisons, functions, literals, fields)
  2. Expression Builder (Ex): Factory class for creating expressions using a fluent API
  3. 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 values
  • TNumeric - Integer and float values
  • TBoolean - Boolean values
  • TDateTime - DateTime values
  • TArray - 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 expressions
  • TNumeric - Numeric values (integers and floats)
  • TBoolean - Boolean values and comparison results
  • TDateTime - DateTime values and date/time operations
  • TArray - 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.