Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 126 additions & 0 deletions FACT_TO_FACT_FEATURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# Fact-to-Fact Comparison Feature

## Overview
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.

## Changes Made

### 1. Updated `src/Rule.php`

#### Modified `evaluateCondition()` method (lines 198-222)
Added logic to resolve fact references in the `value` field:

```php
// Resolve value if it's another fact (fact-to-fact comparison)
if (is_array($value) && isset($value['fact'])) {
$value = $facts->get($value['fact'], $value['path'] ?? null);
}
```

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.

#### Modified `interpretCondition()` method (lines 265-303)
Added logic to display fact-to-fact comparisons in human-readable format:

```php
// Handle fact-to-fact comparison display
if (is_array($value) && isset($value['fact'])) {
$valueFact = $value['fact'];
$valuePath = $value['path'] ?? null;
$valueDisplay = $valueFact;
if ($valuePath) {
$valuePathParts = explode('.', ltrim($valuePath, '$.'));
$valueDisplay = end($valuePathParts);
}
return "$factDisplay $operatorText $valueDisplay";
}
```

### 2. Updated `README.md`
- Added "Fact-to-Fact Comparison" to the Features list
- Added comprehensive documentation with examples:
- Speed Limit Check example
- Age Verification with nested paths example

### 3. Added `tests/EngineTest.php::testFactToFactComparison()`
Created comprehensive test coverage with three test cases:
- Basic fact-to-fact comparison (distance vs limit)
- Fact-to-fact comparison with failure scenario
- Nested fact comparison with paths (user.age vs requirements.minimumAge)

### 4. Created `examples/fact-to-fact-comparison.php`
Added a complete working example file demonstrating:
- Speed limit checking
- Age verification with nested paths
- Credit limit checking with multiple fact comparisons

## Usage Examples

### Basic Example
```php
$ruleConfig = [
"name" => "test.factToFact",
"conditions" => [
"all" => [
[
"fact" => "distance",
"operator" => "lessThanInclusive",
"value" => ["fact" => "limit"] // Reference another fact
]
]
],
"event" => ["type" => "passed", "params" => []],
"failureEvent" => ["type" => "failed", "params" => []]
];

$engine->addRule(new Rule($ruleConfig));
$engine->setTargetRule('test.factToFact');

$engine->addFact('distance', 40);
$engine->addFact('limit', 50);

$result = $engine->evaluate();
// Result: passed (40 <= 50)
```

### Nested Facts Example
```php
$ruleConfig = [
"conditions" => [
"all" => [
[
"fact" => "user",
"path" => "$.age",
"operator" => "greaterThanInclusive",
"value" => [
"fact" => "requirements",
"path" => "$.minimumAge"
]
]
]
]
];

$engine->addFact('user', ['age' => 25]);
$engine->addFact('requirements', ['minimumAge' => 18]);
// Compares: user.age (25) >= requirements.minimumAge (18)
```

## Benefits
1. **Dynamic Thresholds**: Comparison values can change based on context without modifying rule definitions
2. **Runtime Flexibility**: Different scenarios can use different limits/thresholds
3. **Real-world Use Cases**:
- Compare `currentSpeed` to `speedLimit` (where limit changes by zone)
- Compare `userAge` to `minimumAge` (where minimum might vary by country)
- Compare `accountBalance` to `creditLimit` (personalized per user)

## Testing
All tests pass successfully:
```bash
./vendor/bin/phpunit tests/EngineTest.php

OK (8 tests, 32 assertions)
```

## Backward Compatibility
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.
59 changes: 59 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ This library, inspired by the `json-rules-engine`, ([link](https://github.com/Ca

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

### Fact-to-Fact Comparison
The engine supports comparing two facts dynamically at runtime. This allows for flexible rule evaluation where comparison values can change based on context.

**Example: Speed Limit Check**
```php
$engine = new Engine();

$ruleConfig = [
"name" => "speed.check",
"conditions" => [
"all" => [
[
"fact" => "currentSpeed",
"operator" => "lessThanInclusive",
"value" => ["fact" => "speedLimit"] // Compare to another fact
]
]
],
"event" => ["type" => "withinLimit", "params" => ["message" => "Speed is within limit"]],
"failureEvent" => ["type" => "speeding", "params" => ["message" => "Exceeding speed limit"]]
];

$engine->addRule(new Rule($ruleConfig));
$engine->setTargetRule('speed.check');

// Add both facts dynamically
$engine->addFact('currentSpeed', 55);
$engine->addFact('speedLimit', 60);

$result = $engine->evaluate();
// Result: withinLimit - Speed is within limit
```

**Example: Nested Fact Comparison**
```php
$ruleConfig = [
"name" => "age.verification",
"conditions" => [
"all" => [
[
"fact" => "user",
"path" => "$.age",
"operator" => "greaterThanInclusive",
"value" => [
"fact" => "requirements",
"path" => "$.minimumAge"
]
]
]
],
"event" => ["type" => "ageVerified", "params" => []],
"failureEvent" => ["type" => "ageFailed", "params" => []]
];

$engine->addFact('user', ['age' => 25]);
$engine->addFact('requirements', ['minimumAge' => 18]);
```

## Run the test
```bash
./vendor/bin/phpunit tests
Expand Down
169 changes: 169 additions & 0 deletions examples/fact-to-fact-comparison.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
<?php

require_once __DIR__ . '/../vendor/autoload.php';

use Kennyth01\PhpRulesEngine\Engine;
use Kennyth01\PhpRulesEngine\Rule;

echo "=== Fact-to-Fact Comparison Examples ===\n\n";

// Example 1: Speed Limit Check
echo "Example 1: Speed Limit Check\n";
echo "-----------------------------\n";

$engine1 = new Engine();

$speedRule = [
"name" => "speed.check",
"conditions" => [
"all" => [
[
"fact" => "currentSpeed",
"operator" => "lessThanInclusive",
"value" => ["fact" => "speedLimit"]
]
]
],
"event" => [
"type" => "withinLimit",
"params" => ["message" => "Speed is within limit"]
],
"failureEvent" => [
"type" => "speeding",
"params" => ["message" => "Exceeding speed limit"]
]
];

$engine1->addRule(new Rule($speedRule));
$engine1->setTargetRule('speed.check');
$engine1->showInterpretation(true);

// Scenario A: Within speed limit
$engine1->addFact('currentSpeed', 55);
$engine1->addFact('speedLimit', 60);
$result1 = $engine1->evaluate();

echo "Scenario A: currentSpeed = 55, speedLimit = 60\n";
echo "Result: " . $result1[0]['type'] . "\n";
echo "Message: " . $result1[0]['params']['message'] . "\n";
echo "Interpretation: " . $result1[0]['interpretation'] . "\n\n";

// Scenario B: Exceeding speed limit
$engine1b = new Engine();
$engine1b->addRule(new Rule($speedRule));
$engine1b->setTargetRule('speed.check');
$engine1b->showInterpretation(true);
$engine1b->addFact('currentSpeed', 75);
$engine1b->addFact('speedLimit', 60);
$result1b = $engine1b->evaluate();

echo "Scenario B: currentSpeed = 75, speedLimit = 60\n";
echo "Result: " . $result1b[0]['type'] . "\n";
echo "Message: " . $result1b[0]['params']['message'] . "\n\n";

// Example 2: Age Verification with Nested Paths
echo "\nExample 2: Age Verification (Nested Paths)\n";
echo "-------------------------------------------\n";

$engine2 = new Engine();

$ageRule = [
"name" => "age.verification",
"conditions" => [
"all" => [
[
"fact" => "user",
"path" => "$.age",
"operator" => "greaterThanInclusive",
"value" => [
"fact" => "requirements",
"path" => "$.minimumAge"
]
]
]
],
"event" => [
"type" => "ageVerified",
"params" => ["message" => "User meets age requirement"]
],
"failureEvent" => [
"type" => "ageFailed",
"params" => ["message" => "User does not meet age requirement"]
]
];

$engine2->addRule(new Rule($ageRule));
$engine2->setTargetRule('age.verification');
$engine2->showInterpretation(true);

// Scenario A: User meets age requirement
$engine2->addFact('user', ['age' => 25, 'name' => 'John']);
$engine2->addFact('requirements', ['minimumAge' => 18, 'country' => 'US']);
$result2 = $engine2->evaluate();

echo "Scenario A: user.age = 25, requirements.minimumAge = 18\n";
echo "Result: " . $result2[0]['type'] . "\n";
echo "Message: " . $result2[0]['params']['message'] . "\n";
echo "Interpretation: " . $result2[0]['interpretation'] . "\n\n";

// Scenario B: User does not meet age requirement
$engine2b = new Engine();
$engine2b->addRule(new Rule($ageRule));
$engine2b->setTargetRule('age.verification');
$engine2b->showInterpretation(true);
$engine2b->addFact('user', ['age' => 16, 'name' => 'Jane']);
$engine2b->addFact('requirements', ['minimumAge' => 18, 'country' => 'US']);
$result2b = $engine2b->evaluate();

echo "Scenario B: user.age = 16, requirements.minimumAge = 18\n";
echo "Result: " . $result2b[0]['type'] . "\n";
echo "Message: " . $result2b[0]['params']['message'] . "\n\n";

// Example 3: Credit Limit Check
echo "\nExample 3: Credit Limit Check\n";
echo "------------------------------\n";

$engine3 = new Engine();

$creditRule = [
"name" => "credit.limit.check",
"conditions" => [
"all" => [
[
"fact" => "requestedAmount",
"operator" => "lessThanInclusive",
"value" => ["fact" => "creditLimit"]
],
[
"fact" => "currentBalance",
"operator" => "lessThan",
"value" => ["fact" => "creditLimit"]
]
]
],
"event" => [
"type" => "approved",
"params" => ["message" => "Transaction approved"]
],
"failureEvent" => [
"type" => "declined",
"params" => ["message" => "Transaction declined"]
]
];

$engine3->addRule(new Rule($creditRule));
$engine3->setTargetRule('credit.limit.check');
$engine3->showInterpretation(true);

// Scenario: Transaction within limits
$engine3->addFact('requestedAmount', 500);
$engine3->addFact('currentBalance', 3000);
$engine3->addFact('creditLimit', 5000);
$result3 = $engine3->evaluate();

echo "Scenario: requestedAmount = 500, currentBalance = 3000, creditLimit = 5000\n";
echo "Result: " . $result3[0]['type'] . "\n";
echo "Message: " . $result3[0]['params']['message'] . "\n";
echo "Interpretation: " . $result3[0]['interpretation'] . "\n\n";

echo "=== All Examples Completed ===\n";
Loading