{"id":33963015,"url":"https://github.com/krixon/rules","last_synced_at":"2026-03-17T20:09:55.992Z","repository":{"id":37546284,"uuid":"159951356","full_name":"krixon/rules","owner":"krixon","description":"A simple language for defining and building Specification Pattern objects.","archived":false,"fork":false,"pushed_at":"2023-04-19T19:21:30.000Z","size":219,"stargazers_count":0,"open_issues_count":4,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2026-01-14T19:37:47.005Z","etag":null,"topics":["rules","specification-pattern","specifications"],"latest_commit_sha":null,"homepage":"","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/krixon.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"license.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2018-12-01T14:14:28.000Z","updated_at":"2020-03-08T20:14:00.000Z","dependencies_parsed_at":"2022-08-18T03:10:23.892Z","dependency_job_id":null,"html_url":"https://github.com/krixon/rules","commit_stats":null,"previous_names":[],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/krixon/rules","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/krixon%2Frules","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/krixon%2Frules/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/krixon%2Frules/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/krixon%2Frules/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/krixon","download_url":"https://codeload.github.com/krixon/rules/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/krixon%2Frules/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30630190,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-17T17:32:55.572Z","status":"ssl_error","status_checked_at":"2026-03-17T17:32:38.732Z","response_time":56,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["rules","specification-pattern","specifications"],"created_at":"2025-12-12T22:21:44.050Z","updated_at":"2026-03-17T20:09:55.986Z","avatar_url":"https://github.com/krixon.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"Rules\n=====\n\n[![Build Status](https://travis-ci.org/krixon/rules.svg?branch=master)](https://travis-ci.org/krixon/rules)\n[![Coverage Status](https://coveralls.io/repos/github/krixon/rules/badge.svg?branch=master)](https://coveralls.io/github/krixon/rules?branch=master)\n[![Code Climate](https://codeclimate.com/github/krixon/rules/badges/gpa.svg)](https://codeclimate.com/github/krixon/rules)\n[![Latest Stable Version](https://poser.pugx.org/krixon/rules/v/stable)](https://packagist.org/packages/krixon/rules)\n[![Latest Unstable Version](https://poser.pugx.org/krixon/rules/v/unstable)](https://packagist.org/packages/krixon/rules)\n[![License](https://poser.pugx.org/krixon/rules/license)](https://packagist.org/packages/krixon/rules)\n\nA simple language for defining and building [Specification Pattern](https://en.wikipedia.org/wiki/Specification_pattern) objects.\n\n# Prerequisites\n\n- PHP 7.2+\n\n# Installation\n## Install via composer\n\nTo install this library with Composer, run the following command:\n\n```sh\n$ composer require krixon/rules\n```\n\nYou can see this library on [Packagist](https://packagist.org/packages/krixon/rules).\n\n## Install from source\n\n```sh\n# HTTP\n$ git clone https://github.com/krixon/rules.git\n# SSH\n$ git clone git@github.com:krixon/rules.git\n```\n\n# Supported Syntax\n\nRefer to the [syntax documentation](./docs/syntax.md) for detailed information on the rule syntax.\n\n# Usage\n\nThe main task involved in using this library is implementing `BaseCompiler::generate()`. This method has the following\nsignature:\n\n```php\npublic function generate(ComparisonNode $comparison) : Specification\n```\n\nIts job is to generate a `Specification` object from a `ComparisonNode` AST object.\n\nA `ComparisonNode` consists of an `IdentifierNode` which identifies the data against which the specification should\nbe checked, and a `LiteralNode` which contains the value to compare against. It also contains information about\nthe type of comparison (equals, greater than, etc).\n\nFor example, imagine you have the following Specification which can be applied to a `User` object:\n\n```php\nclass EmailAddressMatches implements Specification\n{\n    private $email;\n    \n    \n    public function __construct(string $email)\n    {\n        $this-\u003eemail = $email;\n    }\n    \n    \n    public function isSatisfiedBy($value) : bool\n    {\n        return $value instanceof User \u0026\u0026 $value-\u003ehasEmailAddress($this-\u003eemail);\n    }\n}\n```\n\nYou can define a rule for this Specification as `email is \"karl.rixon@gmail.com\"`.\n\nIn this rule, `email` is an identifier which refers to the user's email address. It is up to you how to interpret a\ngiven identifier. The string value `email` is converted to an `IdentifierNode` AST node during parsing. This node can\nbe accessed via `ComparisonNode::identifier()`.\n\nThe comparison operator is `is`, which means \"equals\". You can use `ComparisonNode::isEquals()`, \n`ComparisonNode::isLessThan()` etc to determine the comparison type.\n\nFinally, `karl.rixon@gmail.com` is converted into a `StringNode` AST node during parsing. This node can be accessed\nvia `ComparisonNode::value()`.\n\nBased on the above, the `BaseCompiler::generate()` method might be implemented as follows:\n\n```php\nclass MyCompiler extends BaseCompiler\n{\n  public function generate(ComparisonNode $comparison) : Specification\n  {\n      $identifier = $comparison-\u003eidentifierFullName();\n      \n      if (strtolower($indentifier) !== 'email') {\n          throw CompilerError::unknownIdentifier($identifier);\n      }\n      \n      if (!$comparison-\u003eisEquals()) {\n          throw CompilerError::unsupportedComparisonType($comparison-\u003etype(), $identifier);\n      }\n  \n      return new EmailAddressMatches($comparison-\u003eliteralValue());\n  }\n}\n```\n\n## Delegating generation to services\n\nAlthough extending `BaseCompiler` is convenient in simple cases, it becomes complicated when you have many\nspecifications to support. In this case, you might want to delegate the generation work to dedicated services.\n\nThe `DelegatingCompiler` class is provided for this purpose. To use it, first create a class which implements the\n`SpecificationGenerator` interface, which defines a single method:\n\n```php\npublic function attempt(ComparisonNode $comparison) : ?Specification;\n```\n\nThis is very similar to `BaseCompiler::generate()`, however returning a `Specification` is optional.\n\nNext, register an instance of your class with the `DelegatingCompiler`:\n\n```php\n$generator = new EmailAddressGenerator();\n$compiler  = new DelegatingCompiler($generator);\n```\n\nWhen `DelegatingCompiler::compile()` is invoked, the `DelegatingCompiler` will loop through all registered generators\nand call `SpecificationGenerator::attempt()` with each `ComparisonNode`.\n\nAll `SpecificationGenerator`s provided via the `DelegatingCompiler`'s constructor share the same priority of `0`,\nhowever they can also be registered with an explicit priority:\n\n```php\n$generator = new EmailAddressGenerator();\n$compiler  = new DelegatingCompiler();\n\n$compiler-\u003eregister($generator, 100); // Priority of 100.\n```\n\n`SpecificationGenerator`s with higher priority are invoked first.\n\nThe library provides some [built-in specifications and corresponding generators](./docs/specifications.md)\nwhich can be used if desired. These are also easy to extend with custom logic.\n\n## Negating comparisons\n\n`ComparisonNode` does not expose negated comparisons like `does not equal` and `does not match`. However this is\nsupported in the language by adding `not` before the comparison operator:\n\n```\nemail not is \"karl.rixon@gmail.com\"\n```\n```\naddress.county not matches \"/(east|west)\\s+sussex/i\"\n```\n```\nage not \u003e 5\n```\n\nYou do not need to write any code to handle these cases because the compiler will produce a `Specification` based on\nthe non-negated comparison and then wrap the result in a `Not` specification which simply inverts the result of\n`Specification::isSatisfiedBy` returned by the wrapped `Specification`.\n\nA shorthand syntax for `not is` can also be used by simply omitting the `is`:\n\n```\nemail not \"karl.rixon@gmail.com\"\n```\n\n# Contributing\n\nPlease refer to [CONTRIBUTING.md](./CONTRIBUTING.md)","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkrixon%2Frules","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkrixon%2Frules","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkrixon%2Frules/lists"}