yl0711-coder / ruleflow-php
A lightweight Decision List rule engine for PHP and Laravel.
Requires
- php: ^8.1
Requires (Dev)
- orchestra/testbench: ^8.0|^9.0|^10.0|^11.0
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^10.5|^11.5|^12.0|^13.0
- squizlabs/php_codesniffer: ^3.9
Suggests
- laravel/framework: Required when using RuleFlow as a Laravel package.
README
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 withexplain() - 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()andevaluateAll() - 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()andexplain()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);
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:
=!=>>=<<=innot_inexistsnot_existscontainsstarts_withends_withbetweenregex
= 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);
The context accepts nested arrays, public object properties, ArrayAccess
offsets, and magic __isset/__get accessors — so Eloquent models can be
passed directly, for example ['order' => $order].
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.