Webmozart Expression
, (*1)
Latest release: 1.0.0, (*2)
PHP >= 5.3.9, (*3)
This library implements the Specification Pattern for PHP. You can use it to
easily filter results of your domain services by evaluating logical expressions., (*4)
Conversely to rulerz, this library focuses on providing a usable and efficient
PHP API first. An expression language that converts string expressions into
Expression
instances can be built on top, but is not included in the current
release., (*5)
Visitors can be implemented that convert Expression
objects into Doctrine
queries and similar objects., (*6)
Installation
Use Composer to install the package:, (*7)
$ composer require webmozart/expression
Basic Usage
Use the [Expression
] interface in finder methods of your service classes:, (*8)
use Webmozart\Expression\Expression;
interface PersonRepository
{
public function findPersons(Expression $expr);
}
When querying persons from the repository, you can create new expressions with
the [Expr
] factory class:, (*9)
$expr = Expr::method('getFirstName', Expr::startsWith('Tho'))
->andMethod('getAge', Expr::greaterThan(35));
$persons = $repository->findPersons($expr);
The repository implementation can use the evaluate()
method to match
individual persons against the criteria:, (*10)
class PersonRepositoryImpl implements PersonRepository
{
private $persons = [];
public function findPersons(Expression $expr)
{
return Expr::filter($this->persons, $expr);
}
}
Visitors can be built to convert expressions into
other types of specifications, such as Doctrine query builders., (*11)
Domain Expressions
Extend existing expressions to build domain-specific expressions:, (*12)
class IsPremium extends Method
{
public function __construct()
{
parent::__construct('isPremium', [], Expr::same(true));
}
}
class HasPreviousBookings extends Method
{
public function __construct()
{
parent::__construct(
'getBookings',
[],
Expr::count(Expr::greaterThan(0))
);
}
}
// Check if a customer is premium
if ((new IsPremium())->evaluate($customer)) {
// ...
}
// Get premium customers with bookings
$customers = $repo->findCustomers(Expr::andX([
new IsPremium(),
new HasPreviousBookings(),
]));
The following sections describe the core expressions in detail., (*13)
Expressions
The [Expr
] class is able to create the following expressions:, (*14)
Method |
Description |
null() |
Check that a value is null
|
notNull() |
Check that a value is not null
|
isEmpty() |
Check that a value is empty (using empty() ) |
notEmpty() |
Check that a value is not empty (using empty() ) |
isInstanceOf($className) |
Check that a value is instance of a class (using instanceof ) |
equals($value) |
Check that a value equals another value (using == ) |
notEquals($value) |
Check that a value does not equal another value (using != ) |
same($value) |
Check that a value is identical to another value (using === ) |
notSame($value) |
Check that a value does not equal another value (using !== ) |
greaterThan($value) |
Check that a value is greater than another value |
greaterThanEqual($value) |
Check that a value is greater than or equal to another value |
lessThan($value) |
Check that a value is less than another value |
lessThanEqual($value) |
Check that a value is less than or equal to another value |
startsWith($prefix) |
Check that a value starts with a given string |
endsWith($suffix) |
Check that a value ends with a given string |
contains($string) |
Check that a value contains a given string |
matches($regExp) |
Check that a value matches a regular expression |
in($values) |
Check that a value occurs in a list of values |
keyExists($key) |
Check that a key exists in a value |
keyNotExists($key) |
Check that a key does not exist in a value |
true() |
Always true (tautology) |
false() |
Always false (contradiction) |
Selectors
With composite values like arrays or objects, you often want to match only a
part of that value (like an array key or the result of a getter) against an
expression. You can select the evaluated parts with a selector., (*15)
When you evaluate arrays, use the key()
selector to match the value of an
array key:, (*16)
$expr = Expr::key('age', Expr::greaterThan(10));
$expr->evaluate(['age' => 12]);
// => true
Each selector method accepts the expression as last argument that should be
evaluated for the selected value., (*17)
When evaluating objects, use property()
and method()
to evaluate the values
of properties and the results of method calls:, (*18)
$expr = Expr::property('age', Expr::greaterThan(10));
$expr->evaluate(new Person(12));
// => true
$expr = Expr::method('getAge', Expr::greaterThan(10));
$expr->evaluate(new Person(12));
// => true
The method()
selector also accepts arguments that will be passed to the
method. Pass the arguments before the evaluated expression:, (*19)
$expr = Expr::method('getParameter', 'age', Expr::greaterThan(10));
$expr->evaluate(new Person(12));
// => true
You can nest selectors to evaluate expressions for nested objects or arrays:, (*20)
$expr = Expr::method('getBirthDate', Expr::method('format', 'Y', Expr::lessThan(2000)));
$expr->evaluate(new Person(12));
// => false
The following table lists all available selectors:, (*21)
Method |
Description |
key($key, $expr) |
Evaluate an expression for a key of an array |
method($name, $expr) |
Evaluate an expression for the result of a method call |
property($name, $expr) |
Evaluate an expression for the value of a property |
count($expr) |
Evaluate an expression for the count of a collection |
The count()
selector accepts arrays and Countable
objects., (*22)
Quantors
Quantors are applied to collections and test whether an expression matches for a
certain number of elements. A famous one is the all-quantor:, (*23)
$expr = Expr::all(Expr::method('getAge', Expr::greaterThan(10)));
$expr->evaluate([new Person(12), new Person(11)]);
// => true
Quantors accept both arrays and Traversable
instances. The following table
lists all available quantors:, (*24)
Method |
Description |
all($expr) |
Check that an expression matches for all entries of a collection |
atLeast($count, $expr) |
Check that an expression matches for at least $count entries of a collection |
atMost($count, $expr) |
Check that an expression matches for at most $count entries of a collection |
exactly($count, $expr) |
Check that an expression matches for exactly $count entries of a collection |
Logical Operators
You can negate an expression with not()
:, (*25)
$expr = Expr::not(Expr::method('getFirstName', Expr::startsWith('Tho')));
You can connect multiple expressions with "and" using the and*()
methods:, (*26)
$expr = Expr::method('getFirstName', Expr::startsWith('Tho'))
->andMethod('getAge', Expr::greaterThan(35));
The same is possible for the "or" operator:, (*27)
$expr = Expr::method('getFirstName', Expr::startsWith('Tho'))
->orMethod('getAge', Expr::greaterThan(35));
You can use and/or inside selectors:, (*28)
$expr = Expr::method('getAge', Expr::greaterThan(35)->orLessThan(20));
If you want to mix and match "and" and "or" operators, use andX()
and orX()
to add embedded expressions:, (*29)
$expr = Expr::method('getFirstName', Expr::startsWith('Tho'))
->andX(
Expr::method('getAge', Expr::lessThan(14))
->orMethod('isReduced', Expr::same(true))
);
Testing
To make sure that PHPUnit compares [Expression
] objects correctly, you should
register the [ExpressionComparator
] with PHPUnit in your PHPUnit bootstrap file:, (*30)
// tests/bootstrap.php
use SebastianBergmann\Comparator\Factory;
use Webmozart\Expression\PhpUnit\ExpressionComparator;
require_once __DIR__.'/../vendor/autoload.php';
Factory::getInstance()->register(new ExpressionComparator());
Make sure the file is registered correctly in phpunit.xml.dist
:, (*31)
<phpunit bootstrap="tests/bootstrap.php" colors="true">
<!-- ... -->
</phpunit>
The [ExpressionComparator
] makes sure that PHPUnit compares different
[Expression
] instances by logical equivalence instead of by object equality.
For example, the following [Expression
] are logically equivalent, but not equal
as objects:, (*32)
// Logically equivalent
$c1 = Expr::notNull()->andSame(35);
$c2 = Expr::same(35)->andNotNull();
$c1 == $c2;
// => false
$c1->equivalentTo($c2);
// => true
// Also logically equivalent
$c1 = Expr::same(35);
$c2 = Expr::oneOf([35]);
$c1 == $c2;
// => false
$c1->equivalentTo($c2);
// => true
In some cases, you will want to transform expressions to some other
representation. A prime example is the transformation of an expression to a
Doctrine query., (*33)
You can implement a custom [ExpressionVisitor
] to do the transformation. The
visitor's methods enterExpression()
and leaveExpression()
are called for
every node of the expression tree:, (*34)
use Webmozart\Expression\Traversal\ExpressionVisitor;
class QueryBuilderVisitor implements ExpressionVisitor
{
private $qb;
public function __construct(QueryBuilder $qb)
{
$this->qb = $qb;
}
public function enterExpression(Expression $expr)
{
// configure the $qb...
}
public function leaveExpression(Expression $expr)
{
// configure the $qb...
}
}
Use an [ExpressionTraverser
] to traverse an expression with your visitor:, (*35)
public function expressionToQueryBuilder(Expression $expr)
{
$qb = new QueryBuilder();
$traverser = new ExpressionTraverser();
$traverser->addVisitor(new QueryBuilderVisitor($qb));
$traverser->traverse($expr);
return $qb;
}
Authors
Contribute
Contributions to the package are always welcome!, (*36)
Support
If you are having problems, send a mail to bschussek@gmail.com or shout out to
@webmozart on Twitter., (*37)
License
All contents of this package are licensed under the MIT license., (*38)