webmozart / expression
Implementation of the Specification pattern and logical expressions for PHP.
Installs: 277 970
Dependents: 14
Suggesters: 0
Security: 0
Stars: 215
Watchers: 15
Forks: 10
Open Issues: 1
Requires
- php: >=5.3.9
Requires (Dev)
- phpunit/phpunit: ^4.6
- sebastian/version: ^1.0.1
This package is auto-updated.
Last update: 2024-11-08 00:57:08 UTC
README
Latest release: 1.0.0
PHP >= 5.3.9
This library implements the Specification Pattern for PHP. You can use it to easily filter results of your domain services by evaluating logical expressions.
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.
Visitors can be implemented that convert Expression
objects into Doctrine
queries and similar objects.
Installation
Use Composer to install the package:
$ composer require webmozart/expression
Basic Usage
Use the Expression
interface in finder methods of your service classes:
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:
$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:
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.
Domain Expressions
Extend existing expressions to build domain-specific expressions:
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.
Expressions
The Expr
class is able to create the following expressions:
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.
When you evaluate arrays, use the key()
selector to match the value of an
array key:
$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.
When evaluating objects, use property()
and method()
to evaluate the values
of properties and the results of method calls:
$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:
$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:
$expr = Expr::method('getBirthDate', Expr::method('format', 'Y', Expr::lessThan(2000))); $expr->evaluate(new Person(12)); // => false
The following table lists all available selectors:
The count()
selector accepts arrays and Countable
objects.
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:
$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:
Logical Operators
You can negate an expression with not()
:
$expr = Expr::not(Expr::method('getFirstName', Expr::startsWith('Tho')));
You can connect multiple expressions with "and" using the and*()
methods:
$expr = Expr::method('getFirstName', Expr::startsWith('Tho')) ->andMethod('getAge', Expr::greaterThan(35));
The same is possible for the "or" operator:
$expr = Expr::method('getFirstName', Expr::startsWith('Tho')) ->orMethod('getAge', Expr::greaterThan(35));
You can use and/or inside selectors:
$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:
$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:
// 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
:
<!-- phpunit.xml.dist --> <?xml version="1.0" encoding="UTF-8"?> <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:
// 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
Expression Transformation
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.
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:
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:
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!
- Report any bugs or issues you find on the issue tracker.
- You can grab the source code at the package's Git repository.
Support
If you are having problems, send a mail to bschussek@gmail.com or shout out to @webmozart on Twitter.
License
All contents of this package are licensed under the MIT license.