Download the PHP package leeovery/rules-engine without Composer

On this page you can find all versions of the php package leeovery/rules-engine. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.

FAQ

After the download, you have to make one include require_once('vendor/autoload.php');. After that you have to import the classes with use statements.

Example:
If you use only one package a project is not needed. But if you use more then one package, without a project it is not possible to import the classes with use statements.

In general, it is recommended to use always a project to download your libraries. In an application normally there is more than one library needed.
Some PHP packages are not free to download and because of that hosted in private repositories. In this case some credentials are needed to access such packages. Please use the auth.json textarea to insert credentials, if a package is coming from a private repository. You can look here for more information.

  • Some hosting areas are not accessible by a terminal or SSH. Then it is not possible to use Composer.
  • To use Composer is sometimes complicated. Especially for beginners.
  • Composer needs much resources. Sometimes they are not available on a simple webspace.
  • If you are using private repositories you don't need to share your credentials. You can set up everything on our site and then you provide a simple download link to your team member.
  • Simplify your Composer build process. Use our own command line tool to download the vendor folder as binary. This makes your build process faster and you don't need to expose your credentials for private repositories.
Please rate this library. Is it a good library?

Informations about the package rules-engine

# Rules Engine **Versioned business rules for Laravel** Resolve values from named rule sets against facts, with immutable, dated versions,
rules that keep their identity through every edit, and a record of what each change did. [![Tests](https://img.shields.io/github/actions/workflow/status/leeovery/rules-engine/tests.yml?branch=main&label=tests)](https://github.com/leeovery/rules-engine/actions/workflows/tests.yml) [![Packagist](https://img.shields.io/packagist/v/leeovery/rules-engine.svg)](https://packagist.org/packages/leeovery/rules-engine) [![PHP](https://img.shields.io/badge/PHP-8.4+-777BB4.svg)](https://www.php.net) [![Laravel](https://img.shields.io/badge/Laravel-13-FF2D20.svg)](https://laravel.com) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE.md) [Install](#install) · [Quick Start](#quick-start) · [Core Concepts](#core-concepts) · [Reference](#reference) · [Testing](#testing)

A rule set is a named, ordered list of rules, and each rule has a key, a condition and a value. You resolve a rule set against facts, and the best-matching rule answers.

Rule sets are stored as immutable versions. Every change publishes the next version, and nothing is ever edited or deleted. Each rule keeps a permanent uuid across versions, and every match says which version and rule answered, so a decision can be explained and reproduced later.

Why Rules Engine?

Business rules change more often than code: which category a merchant belongs to, which rate applies from April, which queue a request goes to. Hard-code them and every change needs a deploy. Keep them in a plain table and every change overwrites the old answer, so nobody can say later why a decision came out the way it did.

Install

The service provider and the RulesEngine facade register themselves. The migrations run from the package, so there's nothing to publish. They create three tables, rule_sets, rule_set_versions and rules:

Publish the config file if you want to change the cache settings:

Requirements

Quick Start

Core Concepts

Reference

Publishing a Rule Set

The examples below follow one rule set, which sorts card transactions into spending categories by merchant. Name rule sets with a backed enum, or plain strings:

Publish the first version as a complete list of rules:

publish() stores version 1 and returns it as a RuleSetVersion with its rules loaded.

Publishing a complete list again creates the next version, holding exactly the rules listed. A rule keeps the uuid of the previous version's rule with the same key, and a new key gets a new uuid.

The facade stands for LeeOvery\RulesEngine\RulesEngine, which you can inject instead.

Resolving

Method Returns
first() the best RuleMatch, or null
firstValue() the best match's value, or null
sole() the only RuleMatch, throwing NoMatchingRuleException or MultipleRulesMatchedException otherwise
soleValue() the only match's value, throwing as sole() does
all() every RuleMatch, best first
resolve() a RuleMatchCollection of every match, best first

A RuleMatch is a readonly value that says what answered:

Add facts as an array or a key and value. Calling with() again adds to them, and a later fact replaces an earlier one with the same key. Backed enums in facts are compared by their values, the same way they're stored in conditions.

Changing Rules

Instead of listing every rule again, publish edits to the version in force. Edits name rules by key:

Edit Does Refuses
addRule($key, $condition, $value, $metadata, before: $key, after: $key) adds a rule with a new uuid, at the end unless before or after places it a key that's already there, a place next to a missing rule, both before and after
updateRule($key, value: …, condition: …, metadata: …) changes any of them, keeping the rule's uuid, key and position; metadata: null clears it a missing key, or nothing to change
moveRule($key, before: $key) or moveRule($key, after: $key) moves the rule, keeping its uuid a missing key, anything but exactly one of before and after, a rule placed next to itself
renameRule($from, $to) renames the rule, keeping its uuid and position a missing $from, a $to that's already there
removeRule($key) leaves the rule out of the next version a missing key

One publish can carry several edits, applied in order:

Edits are applied when you publish, under a lock on the rule set, to whatever version is current then. If someone else publishes in between, both changes land: yours are applied on top of theirs. Publishes of one set queue on that lock and number their versions in turn.

The lock is a SELECT … FOR UPDATE on the rule set's row. SQLite has no row locks, so Laravel leaves the lock out there. SQLite lets only one transaction write at a time, so there a publish that races another fails instead of queueing, and never overwrites it.

A rule's uuid never changes while the rule exists, through any number of updates, moves and renames. A key that's removed and later added again is a new rule with a new uuid.

A builder publishes either a complete list or edits, not both.

Previewing a Change

preview() works out what a publish would do, without publishing it or firing anything:

It refuses whatever publishing would refuse.

Reverting

revertTo() publishes a copy of an earlier version's rules, with the same keys and uuids, as the next version:

It can't be combined with a list or edits. A version that doesn't exist throws NoApplicableVersionException. In a dated set, the copy needs an effective date of its own, as any new version does: ->effectiveFrom('2027-04-06')->revertTo(1).

Seeing What Changed

RuleSetChanges compares two versions rule by rule, matching rules by uuid:

moved lists the fewest rules whose moves give the new order, so adding or removing a rule doesn't count the rules around it as moved. Values and metadata are compared as they're stored.

Every published version carries its changes from the version it follows, in its event. For version 1, every rule is added.

As of a Date, as Known at a Moment

Every lookup is as of a date, today unless you pass asOf(), and considers only versions published at or before a moment, now unless you pass knownAt():

Choosing the version is a small query that's never cached, so a new version takes effect at once. If no version applies, the lookup throws NoApplicableVersionException, naming the date and moment it looked for. An unknown rule set throws RuleSetNotFoundException.

Effective Dates

Reference data such as tax rates changes on known dates. Give its versions effective dates. With a UkDividendRates = 'uk.dividend-rates' case on the enum:

What Publishing Refuses

Publishing throws CannotPublishRuleSetException, and stores nothing, when:

Mixing a list and edits, or a revert with either, throws straight away, and so do an addRule() with both before and after, a moveRule() without exactly one of them, and an updateRule() with nothing to change.

Events

Publishing runs in one database transaction and dispatches LeeOvery\RulesEngine\Events\RuleSetVersionPublished inside it, after the version is written and before the transaction commits. The event carries ruleSet (the name), version, effectiveFrom, publishedBy, note and changes, the RuleSetChanges from the version it follows.

The event doesn't wait for the commit: listeners run inside the transaction, and if one throws, the version is rolled back. A queued listener's job is pushed at once too, unless the listener or its queue connection waits for commits (after_commit). On a database queue on the same connection, a job pushed at once is written in the same transaction as the version, so neither lands without the other.

Immutability

RuleSetVersion and Rule models throw ImmutableRecordException when you update, increment or delete them, including the quiet variants and while model events are faked. A rule set that has versions can't be deleted either: the foreign keys restrict it.

Building Conditions

Conditions use a fluent API modelled on Laravel's query builder.

Clauses combine from left to right. Without nesting, ->orWhere() applies to the whole condition chain before it, and a ->where() after it applies to the whole result, so where('a', 1)->orWhere('b', 2)->where('c', 3) means (a or b) and c. Nest a closure to group clauses any other way.

Operator String Example
Equals =, == where('status', 'booked')
NotEquals !=, <> where('status', '!=', 'pending')
GreaterThan > where('amount', '>', 1000)
GreaterThanOrEquals >= where('amount', '>=', 1000)
LessThan < where('amount', '<', 1000)
LessThanOrEquals <= where('amount', '<=', 1000)
In in whereIn('status', ['a', 'b'])
NotIn not_in whereNotIn('type', ['x'])
Contains contains whereContains('merchant', 'PRET')
StartsWith starts_with whereStartsWith('merchant', 'TESCO')
EndsWith ends_with whereEndsWith('merchant', 'LTD')

Equality and in are strict, so 1 doesn't equal '1', and true doesn't equal 1. >, >=, < and <= use PHP's comparison operators, and contains, starts_with and ends_with compare case-sensitive strings. A condition on a fact that isn't there doesn't match, not even whereNull() or whereNotIn().

Scoring and Ties

The engine scores matches with MAX-CSP (maximum constraint satisfaction): a match scores the number of clauses satisfied along the most specific way its condition matched. Clauses combine from left to right, as in building conditions:

So where('a', 1)->where('b', 2)->where('c', 3)->orWhere('d', 4) scores 3 when a, b and c match, whether or not d does, and 1 when d matches but c doesn't, even if a and b do.

Matches are ordered by score, highest first, so the most specific matching rule wins. Equal scores are ordered by position, lowest first, so ties never depend on database order.

Caching

Matches are cached for the configured time, keyed by the version's id and the facts. Versions never change, so a cached result never goes stale, and a new version is used as soon as it's published.

cache() takes a number of seconds greater than zero. The defaults live in the config file:

Matches are cached as plain arrays, so they work with Laravel 13's default cache.serializable_classes => false.

Reading History

A RuleSetVersion has version, effective_from (a CarbonImmutable date or null), published_at (a CarbonImmutable with microseconds), published_by, note, and its ruleSet and rules, ordered by position.

Database Schema

rule_sets

Column Type Description
id bigint Primary key
name string Unique name
description text Optional description
created_at, updated_at timestamp

rule_set_versions

Column Type Description
id bigint Primary key
rule_set_id bigint The rule set; unique with version
version int 1, 2, 3… within the set
effective_from date When a dated version takes effect
published_at timestamp(6) When it was published
published_by string Who published it
note text Why

rules

Column Type Description
id bigint Primary key
rule_set_version_id bigint The version
uuid uuid The rule's permanent identity, the same in every version; unique within the version
key string The rule's name, unique within the version
position int Order within the version, unique
condition json The condition
condition_hash string SHA-256 of the condition, unique within the version
value json The value
metadata json Optional metadata

Testing

The suite runs on SQLite by default. Postgres is the only database that lets the row-locking tests hold a lock from a second session, so they're skipped elsewhere. Point the suite at a Postgres database to run them:

CI runs the suite on SQLite and on Postgres 17, with PHP 8.4 and 8.5, against the lowest and the newest dependencies.

Changelog

See CHANGELOG.md.

License

MIT. See LICENSE.md.


All versions of rules-engine with dependencies

PHP Build Version
Package Version
Requires php Version ^8.4
illuminate/cache Version ^13.0
illuminate/database Version ^13.0
illuminate/events Version ^13.0
illuminate/support Version ^13.0
spatie/laravel-package-tools Version ^1.93
Composer command for our command line client (download client) This client runs in each environment. You don't need a specific PHP version etc. The first 20 API calls are free. Standard composer command

The package leeovery/rules-engine contains the following files

Loading the files please wait ...