Powerful implementation of the Specification pattern in PHP

Overview

RulerZ Build Status Scrutinizer Code Quality

The central idea of Specification is to separate the statement of how to match a candidate, from the candidate object that it is matched against.

Specifications, explained by Eric Evans and Martin Fowler

RulerZ is a PHP implementation of the Specification pattern which puts the emphasis on three main aspects:

  • an easy and data-agnostic DSL to define business rules and specifications,
  • the ability to check if a candidate satisfies a specification,
  • the ability to filter or query any datasource to only retrieve candidates matching a specification.

Introduction

Business rules can be written as text using a dedicated language, very close to SQL, in which case we refer to them as rules or they can be encapsulated in single classes and referred to as specifications.

Once a rule (or a specification) is written, it can be used to check if a single candidate satisfies it or directly to query a datasource.

The following datasources are supported natively:

  • array of arrays,
  • array of objects.

And support for each one of these is provided by an additional library:

Killer feature: when working with Doctrine, Pomm, or Elasticsearch, RulerZ is able to convert rules directly in queries and does not need to fetch data beforehand.

That's cool, but why do I need that?

First of all, you get to express business rules in a dedicated, simple language. Then, these business rules can be encapsulated in specification classes, reused and composed to form more complex rules. Specifications are now reusable and testable. And last but not least, these rules can be used both to check if a candidate satisfies it and to filter any datasource.

If you still need to be conviced, you can read the whole reasoning in this article.

Quick usage

As a quick overview, we propose to see a little example that manipulates a simple rule and several datasources.

1. Write a rule

The rule hereafter describes a "high ranked female player" (basically, a female player having more than 9000 points).

$highRankFemalesRule = 'gender = "F" and points > 9000';

2. Define a datasource

We have the following datasources:

// a Doctrine QueryBuilder
$playersQb = $entityManager
    ->createQueryBuilder()
    ->select('p')
    ->from('Entity\Player', 'p');

// or an array of arrays
$playersArr = [
    ['pseudo' => 'Joe',   'gender' => 'M', 'points' => 2500],
    ['pseudo' => 'Moe',   'gender' => 'M', 'points' => 1230],
    ['pseudo' => 'Alice', 'gender' => 'F', 'points' => 9001],
];

// or an array of objects
$playersObj = [
    new Player('Joe',   'M', 40, 2500),
    new Player('Moe',   'M', 55, 1230),
    new Player('Alice', 'F', 27, 9001),
];

3. Use a rule to query a datasource

For any of our datasource, retrieving the results is as simple as calling the filter method:

// converts the rule in DQL and makes a single query to the DB
$highRankFemales = $rulerz->filter($playersQb, $highRankFemalesRule);
// filters the array of arrays
$highRankFemales = $rulerz->filter($playersArr, $highRankFemalesRule);
// filters the array of objects
$highRankFemales = $rulerz->filter($playersObj, $highRankFemalesRule);

3. (bis) Check if a candidate satisfies a rule

Given a candidate, checking if it satisfies a rule boils down to calling the satisfies method:

$isHighRankFemale = $rulerz->satisfies($playersObj[0], $highRankFemalesRule);

Going further

Check out the documentation to discover what RulerZ can do for you.

License

This library is under the MIT license.

Comments
  • Cannot use the same specification multiple times?

    Cannot use the same specification multiple times?

    Hello. I love that someone is finally making a nice specification-pattern library for PHP. However, I have a concern I've ran across.

    In the example below, it appears that if I want to filter by more than one group, it will overwrite the parameter with the last group that was specified. This effectively makes the specification as if I had set $spec to new GroupSpec('group 2').

    Are there any plans to remedy this within the library itself, or is it something users will need to be aware of when writing custom specifications?

    class GroupSpec implements RulerZ\Spec\Specification
    {
        private $group;
    
        public function __construct($group)
        {
            $this->group = $group;
        }
    
        public function getRule()
        {
            return 'group = :group';
        }
    
        public function getParameters()
        {
            return [
                'group' => $this->group
            ];
        }
    }
    
    
    $spec = new RulerZ\Spec\OrX([
        new GroupSpec('group 1'),
        new GroupSpec('group 2')
    ]);
    
    var_dump($spec->getParameters());
    /*
     * array(1) {
     *  'group' =>
     *  string(7) "group 2"
     * }
     */
    
    var_dump($spec->getRule());
    // string(32) "group = :group OR group = :group"
    
    bug under discussion 
    opened by estelsmith 18
  • Using without DSL

    Using without DSL

    First off, this library is excellent. I would really, really like to use this library in our project but the DSL makes it a nonstarter for our specific use case. Is it possible to use this library without the DSL? Is it possible to split the library into reusable components? From a high level (without looking at the internals), the DSL, the spec pattern, and the storage abstraction layer seem like they might be better off as separate pieces.

    question 
    opened by tristanpemble 10
  • Fix phpSpec configuration because of coduo/phpspec-data-provider-extension BC break

    Fix phpSpec configuration because of coduo/phpspec-data-provider-extension BC break

    opened by mnapoli 9
  • UI: get parameters and highlight code

    UI: get parameters and highlight code

    I write a rule manager for the end-users. How can I extract all parameters from a rule like user.group in :group? I would like to generate the form which it asks a value for each parameter. I can do it with a regexp: \b:\w+\b but I'm not sure that it matches exactly like the RuerZ pattern. Moreover this function should be in the same workspace as the other RulerZ tools.

    Ahead this form, I display the rule. But some complex rules are difficult to read. How can I colorize it? (highlighting keywords, parameters, opertator…)

    Thanks

    enhancement 
    opened by 1e1 8
  • Issue with the FileCompiler and Elasticsearch

    Issue with the FileCompiler and Elasticsearch

    Hi, here is the code generated by the FileCompiler when calling filter() for the following rule price = 30 ($rulerz->filter($elasticsearch, 'price = 30', [], $context)):

    <?php
    namespace RulerZ\Compiled\Executor;
    
    use RulerZ\Executor\Executor;
    
    class Executor_3f155154 implements Executor
    {
            use \RulerZ\Executor\Elasticsearch\ElasticsearchFilterTrait;
            use \RulerZ\Executor\Polyfill\FilterBasedSatisfaction;
    
        protected function execute($target, array $operators, array $parameters)
        {
            return Array;
        }
    }
    

    As you can see, there's an error: execute() returns Array (the compiled rules seem to be an array instead of a string).

    I believe (I might be wrong) that this is because the elasticsearch implementation returns arrays (instead of strings) for operators.

    Am I doing something wrong here? Here is the configuration of RulerZ:

    $compiler = new RulerZ\Compiler\FileCompiler(new RulerZ\Parser\HoaParser);
    $rulerz = new RulerZ\RulerZ($compiler, [
        new RulerZ\Compiler\Target\Elasticsearch\ElasticsearchVisitor,
    ])
    
    bug 
    opened by mnapoli 8
  • Why ExecutionContext is immutable ?

    Why ExecutionContext is immutable ?

    In my FilterTrait i want to inject metadata in ExecutionContext. Right now i pushing metadata in parameters : $parameters['ting.metadata'] = $repository->getMetadata();

    But i think metadata is more context related than parameters related. So is there any reason to make ExecutionContext immutable ?

    question 
    opened by syrm 7
  • Add Doctrine Obscure Alias Support

    Add Doctrine Obscure Alias Support

    Hello,

    I'm having an issue with RulerZ when attempting to auto-join tables with obscure aliases. In particular, when an association is assigned an alias in the query builder that does not match the name of the association.

    This query builder works fine in the current version (0.19.1) of RulerZ:

    $entity_manager->createQueryBuilder()
        ->select('permission')
        ->from('Permission', 'permission')
        ->innerJoin('permission.entitlement', 'entitlement')
        ->innerJoin('entitlement.product', 'product');
    

    However, this query builder throws a RuntimeException: Could not automatically join table "product"

    $entity_manager->createQueryBuilder()
        ->select('permission')
        ->from('Permission', 'permission')
        ->innerJoin('permission.entitlement', 'entitlement')
        ->innerJoin('entitlement.product', 'product_1');
    

    In both cases, my specification rule looks like this:

    public function getRule()
    {
        return 'entitlement.product.id = :entitlement_product_id';
    }
    

    Do you have any thoughts on this issue? My fix appears to revolve it.

    opened by bobdercole 7
  • DoctrineQueryBuilderVisitor compile issue

    DoctrineQueryBuilderVisitor compile issue

    \RulerZ\Compiler\Target\Sql\DoctrineQueryBuilderVisitor::$detectedJoins must be reseted after compilation, otherwise one rule will use $detectedJoins data of previous rule

    bug 
    opened by kandelyabre 7
  • Configure Composer bin-dir

    Configure Composer bin-dir

    This pull request configures the Composer bin-dir, so you can type bin/phpspec instead of vendor/phpspec/phpspec/bin/phpspec in order to run tests.

    Also, it adds PHPStorm's .idea directory to .gitignore.

    opened by estelsmith 7
  • Issue with Compiler when deploying code

    Issue with Compiler when deploying code

    Hi,

    Sometime, when we deploy our code, we have the following error:

    Uncaught PHP Exception Symfony\Component\Debug\Exception\ClassNotFoundException: 
    "Attempted to load class "Executor_e54f2a89" from namespace "\RulerZ\Compiled\Executor". 
    Did you forget a "use" statement for another namespace?"
     at /.../vendor/kphoen/rulerz/src/Compiler/Compiler.php line 47
    

    If I read correctly, the compiled class is generated at runtime and is not written to disk (in fact, the bundle cache directory seems to be always empty).

    Do you have any idea where it could come from ?

    Thanks !

    opened by jdeniau 6
  • Duplicate auto joins for DoctrineORMVisitor

    Duplicate auto joins for DoctrineORMVisitor

    When I'm using a complex composite specification on master branch that uses some autojoin multiple times then for DoctrineORMVisitor detected joins include duplicates. E.g. if my rule contains sections like foo.id = ? AND foo.name = ? it results in two joins to foo table, both having the same alias like _3_foo. This results in Doctrine throwing an exception when query is executed as same alias for join is used multiple times (e.g. there are two identical LEFT JOINs with the same alias).

    Am I doing something wrong here? For now I did a simple workaround just to keep me going (you can have a look at it at https://github.com/malef/rulerz/commit/28f8ac1c8291525e0bf8633124f26ac780ffa697) but I'm not sure if this is a bug or if I'm misusing this library.

    opened by malef 6
  • 🐞 Satisfies return null instead of bool

    🐞 Satisfies return null instead of bool

    Satisfies return null value instead of bool

    The error occurs when the rule is written incorrectly, Like below: any-example=123 Instead of: any-example = 123

    opened by devlomingo 0
  • Composite must be extends AbstractSpecification

    Composite must be extends AbstractSpecification

    Hi,

    Composite must be extends AbstractSpecification to chain multiple specifications.

    Sample :

    class specificationA extends AbstractSpecification 
    {
        ...
    }
    
    class specificationB extends AbstractSpecification 
    {
        ...
    }
    
    class specificationC extends AbstractSpecification 
    {
        ...
    }
    
    $specificationA
        ->andX($specificationB)
        ->andX($specificationC);
    

    Keep chain support to specification type composite

    class specificationD extends AndX
    {
        public function __construct()
        {
            parent::__construct([
                new specificationA(),
                new specificationB(),
                new specificationC()
            ]);
        }
    }
    
    class specificationE extends Composite
    {
        public function __construct()
        {
            parent::__construct(
                "MyOperator",
                [
                    new specificationA(),
                    new specificationB(),
                    new specificationC()
                ]
            );
        }
    }
    
    $specificationD->andX($specificationE)->andX(...)
    
    opened by cedvan 0
  • Property accessor 5 support

    Property accessor 5 support

    Hello! I cannot install this package in symfony 5 project.

    composer require 'kphoen/rulerz' -w Using version ^0.21.1 for kphoen/rulerz ./composer.json has been updated Running composer update kphoen/rulerz --with-dependencies Loading composer repositories with package information Updating dependencies Restricting packages listed in "symfony/symfony" to "5.1.*" Your requirements could not be resolved to an installable set of packages.

    Problem 1 - Root composer.json requires kphoen/rulerz ^0.21.1 -> satisfiable by kphoen/rulerz[0.21.1]. - kphoen/rulerz 0.21.1 requires symfony/property-access ~3.0|~4.0 -> found symfony/property-access[v3.0.0, ..., v3.4.47, v4.0.0, ..., v4.4.17] but it conflicts with another require. Problem 2 - scheb/two-factor-bundle is locked to version v4.18.0 and an update of this package was not requested. - scheb/two-factor-bundle v4.18.0 requires symfony/property-access ^3.4|^4.0|^5.0 -> found symfony/property-access[v3.4.0, ..., v3.4.47, v4.0.0, ..., v4.4.17, v5.0.0, ..., v5.2.0] but it conflicts with another require. Problem 3 - symfony/form is locked to version v5.0.10 and an update of this package was not requested. - symfony/form v5.0.10 requires symfony/property-access ^5.0 -> found symfony/property-access[v5.0.0, ..., v5.2.0] but it conflicts with another require. Problem 4 - symfony/serializer-pack is locked to version v1.0.3 and an update of this package was not requested. - symfony/serializer-pack v1.0.3 requires symfony/property-access * -> found symfony/property-access[v2.2.0, ..., v2.8.52, v3.0.0, ..., v3.4.47, v4.0.0, ..., v4.4.17, v5.0.0, ..., v5.2.0] but it conflicts with another require. Problem 5 - symfony/form v5.0.10 requires symfony/property-access ^5.0 -> found symfony/property-access[v5.0.0, ..., v5.2.0] but it conflicts with another require. - hwi/oauth-bundle dev-master requires symfony/form ^3.4|^4.3|^5.0 -> satisfiable by symfony/form[v5.0.10]. - hwi/oauth-bundle is locked to version dev-master and an update of this package was not requested.

    Use the option --with-all-dependencies (-W) to allow upgrades, downgrades and removals for packages currently locked to specific versions.

    Installation failed, reverting ./composer.json and ./composer.lock to their original content.

    Are you planing to provide property accessor 5 support? Because it's impossible to install the package in symfony 5 projects without it.

    opened by vshmakov 1
Owner
Kévin Gomez
I can git now.
Kévin Gomez
Primitives for functional programming in PHP

Functional PHP: Functional primitives for PHP NOTE: functional-php used to come with a C extension that implemented most of the functions natively. As

Lars Strojny 1.9k Jan 3, 2023
Option Type for PHP

PHP Option Type This package implements the Option type for PHP! Motivation The Option type is intended for cases where you sometimes might return a v

Johannes 2.4k Dec 26, 2022
A Simple PHP Finite State Machine

Finite, A Simple PHP Finite State Machine Finite is a Simple State Machine, written in PHP. It can manage any Stateful object by defining states and t

Yohan Giarelli 1.3k Dec 31, 2022
A simple stateless production rules engine for PHP 5.3+

Ruler Ruler is a simple stateless production rules engine for PHP 5.3+. Ruler has an easy, straightforward DSL ... provided by the RuleBuilder: $rb =

Justin Hileman 1k Dec 28, 2022
A simple Monad library for PHP

MonadPHP This is a basic Monad library for PHP. Usage Values are "wrapped" in the monad via either the constructor: new MonadPHP\Identity($value) or t

Anthony Ferrara 283 Dec 29, 2022
YCOM Impersonate. Login as selected YCOM user 🧙‍♂️in frontend.

YCOM Impersonate Login as selected YCOM user in frontend. Features: Backend users with admin rights or YCOM[] rights, can be automatically logged in v

Friends Of REDAXO 17 Sep 12, 2022
A PHP implementation of the GraphQL specification based on the JavaScript reference implementation

GraphQL This is a PHP implementation of the GraphQL specification based on the JavaScript reference implementation. Related projects DateTime scalar R

Digia 219 Nov 16, 2022
Laravel specification pattern

Laravel specification pattern Filter an Illuminate collection with specifications. Installation You can install the package via composer: composer req

Maarten Paauw 5 Nov 30, 2022
A simple implementation of the api-problem specification. Includes PSR-15 support.

ApiProblem This library provides a simple and straightforward implementation of the IETF Problem Details for HTTP APIs, RFC 7807. RFC 7807 is a simple

Larry Garfield 236 Dec 21, 2022
The Kafka Enqueue transport - This is an implementation of Queue Interop specification

Supporting Enqueue Enqueue is an MIT-licensed open source project with its ongoing development made possible entirely by the support of community and

Enqueue 40 Oct 6, 2022
This is an implementation of PSR specification. It allows you to send and consume message with Redis store as a broker.

This is an implementation of PSR specification. It allows you to send and consume message with Redis store as a broker.

Enqueue 35 Nov 4, 2022
Tukio is a complete and robust implementation of the PSR-14 Event Dispatcher specification

Tukio is a complete and robust implementation of the PSR-14 Event Dispatcher specification. It supports normal and debug Event Dispatchers, both runtime and compiled Providers, complex ordering of Listeners, and attribute-based registration on PHP 8.

Larry Garfield 70 Dec 19, 2022
[READ-ONLY] A flexible, lightweight and powerful Object-Relational Mapper for PHP, implemented using the DataMapper pattern. This repo is a split of the main code that can be found in https://github.com/cakephp/cakephp

CakePHP ORM The CakePHP ORM provides a powerful and flexible way to work with relational databases. Using a datamapper pattern the ORM allows you to m

CakePHP 146 Sep 28, 2022
PHP implementation of Fowler's Money pattern.

Money PHP library to make working with money safer, easier, and fun! "If I had a dime for every time I've seen someone use FLOAT to store currency, I'

Money PHP 4.2k Jan 2, 2023
PHP implementation of circuit breaker pattern.

What is php-circuit-breaker A component helping you gracefully handle outages and timeouts of external services (usually remote, 3rd party services).

ArturEjsmont 169 Jul 28, 2022
Baum is an implementation of the Nested Set pattern for Laravel's Eloquent ORM.

Baum Baum is an implementation of the Nested Set pattern for Laravel 5's Eloquent ORM. For Laravel 4.2.x compatibility, check the 1.0.x branch branch

Estanislau Trepat 2.2k Jan 3, 2023
Adjacency List’ed Closure Table database design pattern implementation for the Laravel framework.

ClosureTable This is a database manipulation package for the Laravel 5.4+ framework. You may want to use it when you need to store and operate hierarc

Yan Ivanov 441 Dec 11, 2022
Simplified Repository pattern implementation in Laravel

Laravository - Repository Pattern for Laravel Simplified Repository pattern implementation in Laravel. Requirement Laravel 8.x Installation Execute th

Benny Rahmat 14 Oct 1, 2021
Repository Pattern implementation for Laravel

This is a Simple Repository Pattern implementation for Laravel Projects and an easily way to build Eloquent queries from API requests.

Ephraïm SEDDOR 1 Nov 22, 2021
Fast and simple implementation of a REST API based on the Laravel Framework, Repository Pattern, Eloquent Resources, Translatability, and Swagger.

Laravel Headless What about? This allows a fast and simple implementation of a REST API based on the Laravel Framework, Repository Pattern, Eloquent R

Julien SCHMITT 6 Dec 30, 2022