Skip to content

yl0711-coder/ruleflow-php

RuleFlow PHP

English | 简体中文

Tests Packagist Version License

A lightweight Decision List rule engine for PHP and Laravel.

RuleFlow helps backend teams move complex business rules out of hard-coded if/else logic and into testable, configurable, and traceable rule definitions.

It is designed for risk control, content moderation, marketing eligibility, access control, and business decision workflows.

Why This Project Exists

Many PHP systems start with a few inline checks:

if ($order->amount > 1000 && $user->risk_score < 60) {
    return 'reject';
}

That works until the rules become:

  • scattered across controllers, services, jobs, and listeners
  • hard to validate before deployment
  • hard to explain to support, operations, or reviewers
  • risky to change because business logic is mixed with application flow

RuleFlow keeps the model intentionally small:

  • it uses a Decision List model: deterministic priority order, first matching decision by default
  • rules are structured data, not framework-specific code
  • evaluation is deterministic and priority-based
  • decisions can be inspected with trace() or summarized with explain()
  • Laravel integration exists, but the core stays framework-agnostic

RuleFlow is not a RETE inference engine. It is for PHP business decisions where the problem is scattered rules, unsafe changes, and unclear explanations rather than working-memory inference or complex event processing.

What You Get

  • First-match and all-match evaluation with evaluate() and evaluateAll()
  • Nested condition groups such as A AND (B OR C)
  • Built-in operators for equality, numeric checks, arrays, existence, strings, and regex
  • Rule validation before runtime
  • Trace diagnostics with failure reasons and timing
  • Compact explain() output for APIs, logs, and support tools
  • Sensitive-value redaction with sensitive: true
  • Optional rule metadata for ownership, versions, tickets, and rollout notes
  • Laravel config loading, cache support, and php artisan ruleflow:validate

Good Fit

RuleFlow is a good fit when you need:

  • order or payment risk decisions
  • content moderation routing
  • campaign or coupon eligibility checks
  • access-control decisions with request context
  • business rules that need to be reviewed, tested, and explained

It is not trying to be:

  • a visual rule builder
  • a BPMN or workflow platform
  • a distributed decision service
  • a replacement for full policy, validation, or workflow systems

Why Not A Heavy Rule Engine

Many teams do not need a full decision platform. They need a small library that can live inside an existing PHP service and solve one practical problem: replace scattered conditional business logic with something structured, testable, and explainable.

RuleFlow is intentionally optimized for that use case:

  • no external server to run
  • no DSL or visual designer to learn
  • no database schema required
  • no framework lock-in for core usage
  • no large integration surface before the first useful rule ships

Compared with heavier rule engines, RuleFlow trades breadth for clarity:

  • fewer concepts to learn
  • faster adoption inside an existing Laravel or PHP codebase
  • easier code review because rules stay close to application context
  • easier production debugging because trace() and explain() are built in

If your problem is "we need a rules platform", RuleFlow is probably too small. If your problem is "our PHP business logic is turning into untestable if/else sprawl", RuleFlow is the right size.

Project Status

Current release line: v0.3.x

The project already includes:

  • Packagist distribution
  • CI with PHPUnit, PHPCS, PHPStan, examples, and install validation
  • changelog and GitHub Release flow
  • production, security, and Laravel documentation

Installation

Via Packagist

composer require yl0711-coder/ruleflow-php

From GitHub VCS

composer config repositories.ruleflow vcs https://github.com/yl0711-coder/ruleflow-php
composer require yl0711-coder/ruleflow-php:^0.3

The package requires PHP 8.1 or later.

Quick Start

use RuleFlow\Engine;
use RuleFlow\RuleSet;

$rules = [
    [
        'name' => 'high_risk_order',
        'priority' => 100,
        'match' => 'all',
        'conditions' => [
            ['field' => 'order.amount', 'operator' => '>', 'value' => 1000],
            ['field' => 'user.risk_score', 'operator' => '<', 'value' => 60],
        ],
        'action' => 'reject',
        'reason' => 'High-risk order requires manual review.',
    ],
];

$context = [
    'user' => [
        'id' => 1001,
        'risk_score' => 45,
    ],
    'order' => [
        'id' => 'O-20260420-001',
        'amount' => 1299,
    ],
];

$result = Engine::make(RuleSet::fromArray($rules))->evaluate($context);

$result->matched(); // true
$result->action();  // reject
$result->reason();  // High-risk order requires manual review.
$result->trace();   // explainable rule execution trace

JSON Rules

Rules can also be stored as JSON and loaded at runtime:

use RuleFlow\Engine;
use RuleFlow\Loaders\JsonRuleLoader;
use RuleFlow\Validation\RuleValidator;

$rulesPath = __DIR__ . '/rules/order-risk.json';
$rules = json_decode((string) file_get_contents($rulesPath), true);

RuleValidator::defaults()->assertValid($rules);

$ruleSet = (new JsonRuleLoader($rulesPath))->load();
$result = Engine::make($ruleSet)->evaluate($context);

See examples/json-loader.php.

Documentation

Start here:

Operational usage:

Laravel:

Trace Output

RuleFlow returns a trace so engineers, support teams, and reviewers can understand why a rule matched.

print_r($result->toArray());

Example output:

[
    'matched' => true,
    'rule' => 'high_risk_order',
    'action' => 'reject',
    'reason' => 'High-risk order requires manual review.',
    'trace' => [
        [
            'rule' => 'high_risk_order',
            'priority' => 0,
            'matched' => true,
            'match' => 'all',
            'action' => 'reject',
            'reason' => 'High-risk order requires manual review.',
            'duration_ms' => 0.042,
            'stop_reason' => 'first_match',
            'checks' => [
                [
                    'field' => 'order.amount',
                    'exists' => true,
                    'missing' => false,
                    'sensitive' => false,
                    'actual' => 1299,
                    'operator' => '>',
                    'expected' => 1000,
                    'passed' => true,
                    'duration_ms' => 0.011,
                ],
                [
                    'field' => 'user.risk_score',
                    'exists' => true,
                    'missing' => false,
                    'sensitive' => false,
                    'actual' => 45,
                    'operator' => '<',
                    'expected' => 60,
                    'passed' => true,
                    'duration_ms' => 0.009,
                ],
            ],
        ],
    ],
]

When a field does not exist, the trace explicitly marks it with exists: false and missing: true. Disabled rules are marked with skipped: true and skipped_reason: "disabled".

When a rule or condition fails, RuleFlow includes failure_reason when it can infer one. Examples include field_missing, type_mismatch, invalid_expected, value_mismatch, value_not_allowed, value_not_contained, and pattern_mismatch.

When a condition is marked with sensitive: true, RuleFlow redacts actual and expected values in trace and explain output as [redacted].

You can also use trace helpers for operational debugging:

$trace = $result->trace();

$trace->matchedRuleNames(); // ['high_risk_order']
$trace->failedEntries();    // rules evaluated but not matched
$trace->skippedEntries();   // disabled rules
$trace->summary();          // matched, failed, skipped, and total duration

Explain Output

Use explain() when you want a compact decision summary for logs, API responses, or support tooling:

$explain = $result->explain();

Example output:

[
    'matched' => false,
    'rule' => null,
    'matched_rules' => [],
    'action' => null,
    'reason' => null,
    'failure_reason' => 'field_missing',
    'summary' => [
        'evaluated_rules' => 1,
        'matched_rules' => [],
        'failed_rules' => ['phone_present'],
        'skipped_rules' => [],
        'duration_ms' => 0.031,
    ],
    'rule_explanations' => [
        [
            'rule' => 'phone_present',
            'matched' => false,
            'skipped' => false,
            'failure_reason' => 'field_missing',
            'failed_checks' => [
                [
                    'field' => 'user.phone',
                    'operator' => 'exists',
                    'expected' => null,
                    'actual' => null,
                    'failure_reason' => 'field_missing',
                ],
            ],
        ],
    ],
]

trace() keeps the full execution detail. explain() keeps the decision summary small and stable for operational use.

See docs/explain.md, docs/production.md, docs/security-privacy.md, and examples/explain.php.

Supported Operators

RuleFlow supports:

  • =
  • !=
  • >
  • >=
  • <
  • <=
  • in
  • not_in
  • exists
  • not_exists
  • contains
  • starts_with
  • ends_with
  • between
  • regex

= and != use strict PHP comparison semantics (=== / !==).

See docs/semantics.md for the full evaluation contract.

Match Modes

Each rule supports a match mode:

  • all: every condition must pass. This is the default.
  • any: at least one condition must pass.

Example:

[
    'name' => 'suspicious_content',
    'match' => 'any',
    'conditions' => [
        ['field' => 'post.content', 'operator' => 'contains', 'value' => 'free money'],
        ['field' => 'post.report_count', 'operator' => '>=', 'value' => 3],
    ],
    'action' => 'manual_review',
]

Nested Condition Groups

RuleFlow also supports nested condition groups for cases like A AND (B OR C):

[
    'name' => 'high_risk_order',
    'match' => 'all',
    'conditions' => [
        ['field' => 'order.amount', 'operator' => '>', 'value' => 1000],
        [
            'match' => 'any',
            'conditions' => [
                ['field' => 'user.risk_score', 'operator' => '<', 'value' => 60],
                ['field' => 'user.country', 'operator' => 'in', 'value' => ['NG', 'RU']],
            ],
        ],
    ],
    'action' => 'manual_review',
]

Nested groups are evaluated recursively and included in the execution trace.

Collect All Matches

Use evaluateAll() when you need every matched rule instead of only the first one:

$result = Engine::make(RuleSet::fromArray($rules))->evaluateAll($context);

$result->matched();   // true
$result->ruleNames(); // ['amount_review', 'risk_hold']
$result->actions();   // ['manual_review', 'hold']
$result->reasons();   // ['Amount threshold reached.', 'Risk score threshold reached.']

This is useful for risk scoring, moderation signals, and rule-based tagging scenarios.

Custom Operators

Register a custom operator when built-in operators are not enough:

use RuleFlow\Engine;
use RuleFlow\Operators\OperatorInterface;
use RuleFlow\Operators\OperatorRegistry;
use RuleFlow\RuleSet;

final class RegexOperator implements OperatorInterface
{
    public function name(): string
    {
        return 'regex';
    }

    public function evaluate(mixed $actual, mixed $expected): bool
    {
        return is_string($actual)
            && is_string($expected)
            && preg_match($expected, $actual) === 1;
    }
}

$operators = OperatorRegistry::defaults();
$operators->register(new RegexOperator());

$result = Engine::makeWithOperators(RuleSet::fromArray($rules), $operators)->evaluate($context);

Rule Validation

Validate rule definitions before loading them:

use RuleFlow\Validation\RuleValidator;

$validation = RuleValidator::defaults()->validate($rules);

if (!$validation->valid()) {
    print_r($validation->errors());
}

See docs/validation.md.

Laravel Usage

Publish the config:

php artisan vendor:publish --tag=ruleflow-config

Define rules in config/ruleflow.php:

'rules' => [
    [
        'name' => 'new_user_external_link',
        'conditions' => [
            ['field' => 'user.days_since_signup', 'operator' => '<=', 'value' => 7],
            ['field' => 'post.links', 'operator' => 'contains', 'value' => 'https://example.com'],
        ],
        'action' => 'manual_review',
    ],
],

Optional cache settings:

'cache' => [
    'enabled' => true,
    'driver' => 'laravel', // or 'in_memory'
    'store' => 'redis',    // optional, uses default cache store when null
    'key' => 'ruleflow.rules',
    'ttl' => 300,
],

Evaluate rules:

$result = app(\RuleFlow\RuleFlow::class)->evaluate($context);

Validate configured rules:

php artisan ruleflow:validate

See docs/laravel.md, docs/laravel-installation.md, and docs/laravel-example.md.

Use Cases

  • Risk control: reject or review suspicious orders, users, or API requests.
  • Content moderation: route posts, comments, and profiles to review queues.
  • Marketing eligibility: decide whether a user can receive a coupon or campaign.
  • Access control: evaluate contextual access decisions.
  • Business workflow: choose approval paths from structured business conditions.

Non-Goals

RuleFlow is intentionally small. The first versions do not aim to provide:

  • a visual rule management UI
  • a distributed decision platform
  • a full workflow engine
  • database migrations or admin dashboards
  • replacement for Laravel policies, gates, or validation

Development

composer install
composer test
composer lint
composer analyse

Run examples:

php examples/order-risk.php
php examples/content-moderation.php
php examples/json-loader.php
php examples/explain.php

Generate coverage locally when Xdebug or PCOV is available:

composer test-coverage

Run the local benchmark:

php benchmarks/evaluate.php

See docs/benchmark.md.

For production usage recommendations, see docs/production.md.

Roadmap

  • v0.1: core engine, built-in operators, trace output, array/JSON loaders, custom operators, rule validation
  • v0.2: nested rule groups, evaluateAll, existence operators, built-in regex, trace improvements, Laravel cache driver, artisan validation command, PHPStan, coverage CI
  • v0.3: richer trace diagnostics, failure reasons, compact explain output, benchmark suite, production and security guidance
  • v0.4: documentation polish, more production examples, API refinement, and ecosystem hardening
  • v1.0: stable rule format and semantic versioning guarantee

License

RuleFlow PHP is open-sourced software licensed under the MIT license.

About

A lightweight Decision List rule engine for PHP and Laravel.

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors