krixon / rules
Converts textual rules into specification objects
Installs: 2 027
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 2
Forks: 0
Open Issues: 4
pkg:composer/krixon/rules
Requires
- php: >=7.2
- ext-ctype: *
- ext-intl: *
- ext-mbstring: *
Requires (Dev)
- ext-json: *
- php-coveralls/php-coveralls: ^2.1
- phpunit/phpunit: ^8.2
This package is auto-updated.
Last update: 2025-10-20 01:15:36 UTC
README
A simple language for defining and building Specification Pattern objects.
Prerequisites
- PHP 7.2+
Installation
Install via composer
To install this library with Composer, run the following command:
$ composer require krixon/rules
You can see this library on Packagist.
Install from source
# HTTP $ git clone https://github.com/krixon/rules.git # SSH $ git clone git@github.com:krixon/rules.git
Supported Syntax
Refer to the syntax documentation for detailed information on the rule syntax.
Usage
The main task involved in using this library is implementing BaseCompiler::generate(). This method has the following
signature:
public function generate(ComparisonNode $comparison) : Specification
Its job is to generate a Specification object from a ComparisonNode AST object.
A ComparisonNode consists of an IdentifierNode which identifies the data against which the specification should
be checked, and a LiteralNode which contains the value to compare against. It also contains information about
the type of comparison (equals, greater than, etc).
For example, imagine you have the following Specification which can be applied to a User object:
class EmailAddressMatches implements Specification { private $email; public function __construct(string $email) { $this->email = $email; } public function isSatisfiedBy($value) : bool { return $value instanceof User && $value->hasEmailAddress($this->email); } }
You can define a rule for this Specification as email is "karl.rixon@gmail.com".
In this rule, email is an identifier which refers to the user's email address. It is up to you how to interpret a
given identifier. The string value email is converted to an IdentifierNode AST node during parsing. This node can
be accessed via ComparisonNode::identifier().
The comparison operator is is, which means "equals". You can use ComparisonNode::isEquals(),
ComparisonNode::isLessThan() etc to determine the comparison type.
Finally, karl.rixon@gmail.com is converted into a StringNode AST node during parsing. This node can be accessed
via ComparisonNode::value().
Based on the above, the BaseCompiler::generate() method might be implemented as follows:
class MyCompiler extends BaseCompiler { public function generate(ComparisonNode $comparison) : Specification { $identifier = $comparison->identifierFullName(); if (strtolower($indentifier) !== 'email') { throw CompilerError::unknownIdentifier($identifier); } if (!$comparison->isEquals()) { throw CompilerError::unsupportedComparisonType($comparison->type(), $identifier); } return new EmailAddressMatches($comparison->literalValue()); } }
Delegating generation to services
Although extending BaseCompiler is convenient in simple cases, it becomes complicated when you have many
specifications to support. In this case, you might want to delegate the generation work to dedicated services.
The DelegatingCompiler class is provided for this purpose. To use it, first create a class which implements the
SpecificationGenerator interface, which defines a single method:
public function attempt(ComparisonNode $comparison) : ?Specification;
This is very similar to BaseCompiler::generate(), however returning a Specification is optional.
Next, register an instance of your class with the DelegatingCompiler:
$generator = new EmailAddressGenerator(); $compiler = new DelegatingCompiler($generator);
When DelegatingCompiler::compile() is invoked, the DelegatingCompiler will loop through all registered generators
and call SpecificationGenerator::attempt() with each ComparisonNode.
All SpecificationGenerators provided via the DelegatingCompiler's constructor share the same priority of 0,
however they can also be registered with an explicit priority:
$generator = new EmailAddressGenerator(); $compiler = new DelegatingCompiler(); $compiler->register($generator, 100); // Priority of 100.
SpecificationGenerators with higher priority are invoked first.
The library provides some built-in specifications and corresponding generators which can be used if desired. These are also easy to extend with custom logic.
Negating comparisons
ComparisonNode does not expose negated comparisons like does not equal and does not match. However this is
supported in the language by adding not before the comparison operator:
email not is "karl.rixon@gmail.com"
address.county not matches "/(east|west)\s+sussex/i"
age not > 5
You do not need to write any code to handle these cases because the compiler will produce a Specification based on
the non-negated comparison and then wrap the result in a Not specification which simply inverts the result of
Specification::isSatisfiedBy returned by the wrapped Specification.
A shorthand syntax for not is can also be used by simply omitting the is:
email not "karl.rixon@gmail.com"
Contributing
Please refer to CONTRIBUTING.md