Skip to content

Commit a399082

Browse files
authored
Merge pull request #2 from kennyth01/feat/fact-to-fact-comparison
feat: Add support for fact-to-fact comparison
2 parents a6e7449 + 55e9271 commit a399082

5 files changed

Lines changed: 457 additions & 0 deletions

File tree

FACT_TO_FACT_FEATURE.md

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
# Fact-to-Fact Comparison Feature
2+
3+
## Overview
4+
Added support for fact-to-fact comparison in the PHP Rules Engine, allowing rules to compare two dynamic facts at runtime instead of comparing a fact to a static value.
5+
6+
## Changes Made
7+
8+
### 1. Updated `src/Rule.php`
9+
10+
#### Modified `evaluateCondition()` method (lines 198-222)
11+
Added logic to resolve fact references in the `value` field:
12+
13+
```php
14+
// Resolve value if it's another fact (fact-to-fact comparison)
15+
if (is_array($value) && isset($value['fact'])) {
16+
$value = $facts->get($value['fact'], $value['path'] ?? null);
17+
}
18+
```
19+
20+
This checks if the value is structured as a fact reference (an array with a `fact` key), and if so, retrieves that fact's value dynamically before performing the comparison.
21+
22+
#### Modified `interpretCondition()` method (lines 265-303)
23+
Added logic to display fact-to-fact comparisons in human-readable format:
24+
25+
```php
26+
// Handle fact-to-fact comparison display
27+
if (is_array($value) && isset($value['fact'])) {
28+
$valueFact = $value['fact'];
29+
$valuePath = $value['path'] ?? null;
30+
$valueDisplay = $valueFact;
31+
if ($valuePath) {
32+
$valuePathParts = explode('.', ltrim($valuePath, '$.'));
33+
$valueDisplay = end($valuePathParts);
34+
}
35+
return "$factDisplay $operatorText $valueDisplay";
36+
}
37+
```
38+
39+
### 2. Updated `README.md`
40+
- Added "Fact-to-Fact Comparison" to the Features list
41+
- Added comprehensive documentation with examples:
42+
- Speed Limit Check example
43+
- Age Verification with nested paths example
44+
45+
### 3. Added `tests/EngineTest.php::testFactToFactComparison()`
46+
Created comprehensive test coverage with three test cases:
47+
- Basic fact-to-fact comparison (distance vs limit)
48+
- Fact-to-fact comparison with failure scenario
49+
- Nested fact comparison with paths (user.age vs requirements.minimumAge)
50+
51+
### 4. Created `examples/fact-to-fact-comparison.php`
52+
Added a complete working example file demonstrating:
53+
- Speed limit checking
54+
- Age verification with nested paths
55+
- Credit limit checking with multiple fact comparisons
56+
57+
## Usage Examples
58+
59+
### Basic Example
60+
```php
61+
$ruleConfig = [
62+
"name" => "test.factToFact",
63+
"conditions" => [
64+
"all" => [
65+
[
66+
"fact" => "distance",
67+
"operator" => "lessThanInclusive",
68+
"value" => ["fact" => "limit"] // Reference another fact
69+
]
70+
]
71+
],
72+
"event" => ["type" => "passed", "params" => []],
73+
"failureEvent" => ["type" => "failed", "params" => []]
74+
];
75+
76+
$engine->addRule(new Rule($ruleConfig));
77+
$engine->setTargetRule('test.factToFact');
78+
79+
$engine->addFact('distance', 40);
80+
$engine->addFact('limit', 50);
81+
82+
$result = $engine->evaluate();
83+
// Result: passed (40 <= 50)
84+
```
85+
86+
### Nested Facts Example
87+
```php
88+
$ruleConfig = [
89+
"conditions" => [
90+
"all" => [
91+
[
92+
"fact" => "user",
93+
"path" => "$.age",
94+
"operator" => "greaterThanInclusive",
95+
"value" => [
96+
"fact" => "requirements",
97+
"path" => "$.minimumAge"
98+
]
99+
]
100+
]
101+
]
102+
];
103+
104+
$engine->addFact('user', ['age' => 25]);
105+
$engine->addFact('requirements', ['minimumAge' => 18]);
106+
// Compares: user.age (25) >= requirements.minimumAge (18)
107+
```
108+
109+
## Benefits
110+
1. **Dynamic Thresholds**: Comparison values can change based on context without modifying rule definitions
111+
2. **Runtime Flexibility**: Different scenarios can use different limits/thresholds
112+
3. **Real-world Use Cases**:
113+
- Compare `currentSpeed` to `speedLimit` (where limit changes by zone)
114+
- Compare `userAge` to `minimumAge` (where minimum might vary by country)
115+
- Compare `accountBalance` to `creditLimit` (personalized per user)
116+
117+
## Testing
118+
All tests pass successfully:
119+
```bash
120+
./vendor/bin/phpunit tests/EngineTest.php
121+
122+
OK (8 tests, 32 assertions)
123+
```
124+
125+
## Backward Compatibility
126+
This feature is fully backward compatible. Existing rules that use static values continue to work as before. The fact-to-fact comparison is only activated when the `value` is an array with a `fact` key.

README.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ This library, inspired by the `json-rules-engine`, ([link](https://github.com/Ca
88

99
## Features
1010
- **JSON-Configurable Rules**: Easily define rules and conditions in JSON format.
11+
- **Fact-to-Fact Comparison**: Compare two dynamic facts at runtime instead of comparing to static values.
1112
- **Rule Dependencies**: Reference other rules as conditions to create complex evaluations.
1213
- **Logical Operators**: Supports `all` (AND), `any` (OR), and `not` operators, allowing for nested conditions.
1314
- **Custom Events and Failure Messages**: Attach custom messages for success or failure, making evaluations easy to interpret.
@@ -114,6 +115,64 @@ print_r($result);
114115
## Advanced Examples
115116
For other examples, refer to the `tests` directory
116117

118+
### Fact-to-Fact Comparison
119+
The engine supports comparing two facts dynamically at runtime. This allows for flexible rule evaluation where comparison values can change based on context.
120+
121+
**Example: Speed Limit Check**
122+
```php
123+
$engine = new Engine();
124+
125+
$ruleConfig = [
126+
"name" => "speed.check",
127+
"conditions" => [
128+
"all" => [
129+
[
130+
"fact" => "currentSpeed",
131+
"operator" => "lessThanInclusive",
132+
"value" => ["fact" => "speedLimit"] // Compare to another fact
133+
]
134+
]
135+
],
136+
"event" => ["type" => "withinLimit", "params" => ["message" => "Speed is within limit"]],
137+
"failureEvent" => ["type" => "speeding", "params" => ["message" => "Exceeding speed limit"]]
138+
];
139+
140+
$engine->addRule(new Rule($ruleConfig));
141+
$engine->setTargetRule('speed.check');
142+
143+
// Add both facts dynamically
144+
$engine->addFact('currentSpeed', 55);
145+
$engine->addFact('speedLimit', 60);
146+
147+
$result = $engine->evaluate();
148+
// Result: withinLimit - Speed is within limit
149+
```
150+
151+
**Example: Nested Fact Comparison**
152+
```php
153+
$ruleConfig = [
154+
"name" => "age.verification",
155+
"conditions" => [
156+
"all" => [
157+
[
158+
"fact" => "user",
159+
"path" => "$.age",
160+
"operator" => "greaterThanInclusive",
161+
"value" => [
162+
"fact" => "requirements",
163+
"path" => "$.minimumAge"
164+
]
165+
]
166+
]
167+
],
168+
"event" => ["type" => "ageVerified", "params" => []],
169+
"failureEvent" => ["type" => "ageFailed", "params" => []]
170+
];
171+
172+
$engine->addFact('user', ['age' => 25]);
173+
$engine->addFact('requirements', ['minimumAge' => 18]);
174+
```
175+
117176
## Run the test
118177
```bash
119178
./vendor/bin/phpunit tests
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
<?php
2+
3+
require_once __DIR__ . '/../vendor/autoload.php';
4+
5+
use Kennyth01\PhpRulesEngine\Engine;
6+
use Kennyth01\PhpRulesEngine\Rule;
7+
8+
echo "=== Fact-to-Fact Comparison Examples ===\n\n";
9+
10+
// Example 1: Speed Limit Check
11+
echo "Example 1: Speed Limit Check\n";
12+
echo "-----------------------------\n";
13+
14+
$engine1 = new Engine();
15+
16+
$speedRule = [
17+
"name" => "speed.check",
18+
"conditions" => [
19+
"all" => [
20+
[
21+
"fact" => "currentSpeed",
22+
"operator" => "lessThanInclusive",
23+
"value" => ["fact" => "speedLimit"]
24+
]
25+
]
26+
],
27+
"event" => [
28+
"type" => "withinLimit",
29+
"params" => ["message" => "Speed is within limit"]
30+
],
31+
"failureEvent" => [
32+
"type" => "speeding",
33+
"params" => ["message" => "Exceeding speed limit"]
34+
]
35+
];
36+
37+
$engine1->addRule(new Rule($speedRule));
38+
$engine1->setTargetRule('speed.check');
39+
$engine1->showInterpretation(true);
40+
41+
// Scenario A: Within speed limit
42+
$engine1->addFact('currentSpeed', 55);
43+
$engine1->addFact('speedLimit', 60);
44+
$result1 = $engine1->evaluate();
45+
46+
echo "Scenario A: currentSpeed = 55, speedLimit = 60\n";
47+
echo "Result: " . $result1[0]['type'] . "\n";
48+
echo "Message: " . $result1[0]['params']['message'] . "\n";
49+
echo "Interpretation: " . $result1[0]['interpretation'] . "\n\n";
50+
51+
// Scenario B: Exceeding speed limit
52+
$engine1b = new Engine();
53+
$engine1b->addRule(new Rule($speedRule));
54+
$engine1b->setTargetRule('speed.check');
55+
$engine1b->showInterpretation(true);
56+
$engine1b->addFact('currentSpeed', 75);
57+
$engine1b->addFact('speedLimit', 60);
58+
$result1b = $engine1b->evaluate();
59+
60+
echo "Scenario B: currentSpeed = 75, speedLimit = 60\n";
61+
echo "Result: " . $result1b[0]['type'] . "\n";
62+
echo "Message: " . $result1b[0]['params']['message'] . "\n\n";
63+
64+
// Example 2: Age Verification with Nested Paths
65+
echo "\nExample 2: Age Verification (Nested Paths)\n";
66+
echo "-------------------------------------------\n";
67+
68+
$engine2 = new Engine();
69+
70+
$ageRule = [
71+
"name" => "age.verification",
72+
"conditions" => [
73+
"all" => [
74+
[
75+
"fact" => "user",
76+
"path" => "$.age",
77+
"operator" => "greaterThanInclusive",
78+
"value" => [
79+
"fact" => "requirements",
80+
"path" => "$.minimumAge"
81+
]
82+
]
83+
]
84+
],
85+
"event" => [
86+
"type" => "ageVerified",
87+
"params" => ["message" => "User meets age requirement"]
88+
],
89+
"failureEvent" => [
90+
"type" => "ageFailed",
91+
"params" => ["message" => "User does not meet age requirement"]
92+
]
93+
];
94+
95+
$engine2->addRule(new Rule($ageRule));
96+
$engine2->setTargetRule('age.verification');
97+
$engine2->showInterpretation(true);
98+
99+
// Scenario A: User meets age requirement
100+
$engine2->addFact('user', ['age' => 25, 'name' => 'John']);
101+
$engine2->addFact('requirements', ['minimumAge' => 18, 'country' => 'US']);
102+
$result2 = $engine2->evaluate();
103+
104+
echo "Scenario A: user.age = 25, requirements.minimumAge = 18\n";
105+
echo "Result: " . $result2[0]['type'] . "\n";
106+
echo "Message: " . $result2[0]['params']['message'] . "\n";
107+
echo "Interpretation: " . $result2[0]['interpretation'] . "\n\n";
108+
109+
// Scenario B: User does not meet age requirement
110+
$engine2b = new Engine();
111+
$engine2b->addRule(new Rule($ageRule));
112+
$engine2b->setTargetRule('age.verification');
113+
$engine2b->showInterpretation(true);
114+
$engine2b->addFact('user', ['age' => 16, 'name' => 'Jane']);
115+
$engine2b->addFact('requirements', ['minimumAge' => 18, 'country' => 'US']);
116+
$result2b = $engine2b->evaluate();
117+
118+
echo "Scenario B: user.age = 16, requirements.minimumAge = 18\n";
119+
echo "Result: " . $result2b[0]['type'] . "\n";
120+
echo "Message: " . $result2b[0]['params']['message'] . "\n\n";
121+
122+
// Example 3: Credit Limit Check
123+
echo "\nExample 3: Credit Limit Check\n";
124+
echo "------------------------------\n";
125+
126+
$engine3 = new Engine();
127+
128+
$creditRule = [
129+
"name" => "credit.limit.check",
130+
"conditions" => [
131+
"all" => [
132+
[
133+
"fact" => "requestedAmount",
134+
"operator" => "lessThanInclusive",
135+
"value" => ["fact" => "creditLimit"]
136+
],
137+
[
138+
"fact" => "currentBalance",
139+
"operator" => "lessThan",
140+
"value" => ["fact" => "creditLimit"]
141+
]
142+
]
143+
],
144+
"event" => [
145+
"type" => "approved",
146+
"params" => ["message" => "Transaction approved"]
147+
],
148+
"failureEvent" => [
149+
"type" => "declined",
150+
"params" => ["message" => "Transaction declined"]
151+
]
152+
];
153+
154+
$engine3->addRule(new Rule($creditRule));
155+
$engine3->setTargetRule('credit.limit.check');
156+
$engine3->showInterpretation(true);
157+
158+
// Scenario: Transaction within limits
159+
$engine3->addFact('requestedAmount', 500);
160+
$engine3->addFact('currentBalance', 3000);
161+
$engine3->addFact('creditLimit', 5000);
162+
$result3 = $engine3->evaluate();
163+
164+
echo "Scenario: requestedAmount = 500, currentBalance = 3000, creditLimit = 5000\n";
165+
echo "Result: " . $result3[0]['type'] . "\n";
166+
echo "Message: " . $result3[0]['params']['message'] . "\n";
167+
echo "Interpretation: " . $result3[0]['interpretation'] . "\n\n";
168+
169+
echo "=== All Examples Completed ===\n";

0 commit comments

Comments
 (0)