How to Build a Reusable Rule Engine in PHP with Strategy Pattern and Reflection
This article explains how to replace repetitive if‑else rule checks in PHP with a generic rule module using the Strategy design pattern and reflection, providing step‑by‑step code examples, architectural diagrams, and a clear path to extend rules without modifying client code.
Background and Problem
In many activity‑related features, business requirements often involve rule checks such as "login for X days", "accumulate recharge of X yuan", or "reach level X". These rules are called by various modules like gift‑claim, lottery, or virtual points, leading to duplicated if‑else logic across the codebase.
Traditional implementations embed rule logic directly in each module, resulting in massive code duplication, tight coupling between modules and rule logic, and difficulty in maintenance and testing.
Drawbacks of Simple if‑else / switch
Using a series of if‑else statements works for a few rules but quickly becomes unmanageable as the number of rules grows. The article shows an initial three‑rule example, then expands to five and finally more than ten rules, illustrating how the switch‑case approach becomes verbose and error‑prone.
Introducing the Strategy Pattern
The Strategy pattern encapsulates each rule into its own class, separating the algorithm from the client code. An abstract RuleStrategy defines the ruleCheck() method, and concrete classes ( RulePay, RuleLogin, RuleLevel, etc.) implement specific rule logic.
abstract class RuleStrategy {
abstract public function ruleCheck();
}
class RulePay extends RuleStrategy {
public function ruleCheck() {
// handle recharge rule
}
}
class RuleLogin extends RuleStrategy {
public function ruleCheck() {
// handle login rule
}
}
class RuleLevel extends RuleStrategy {
public function ruleCheck() {
// handle level rule
}
}
class RuleContext {
public $strategy;
public function __construct(RuleStrategy $rs) {
$this->strategy = $rs;
}
public function doCheck() {
return $this->strategy->ruleCheck();
}
}
class Client {
public function rule_check($rule_type) {
switch ($rule_type) {
case self::RULE_TYPE_PAY:
$strategy = new RulePay();
break;
case self::RULE_TYPE_LOGIN:
$strategy = new RuleLogin();
break;
case self::RULE_TYPE_LEVEL:
$strategy = new RuleLevel();
break;
default:
// default handling
}
$rule_context = new RuleContext($strategy);
$rule_context->doCheck();
return true;
}
}With this design, each rule is independent, easier to test, and reusable across multiple components.
Further Decoupling with Reflection
Even the switch statement in the client can be eliminated by using reflection to instantiate the appropriate strategy class based on the rule name.
class RuleContext {
public $rule = [];
/**
* Instantiate a rule strategy object.
* @param string $name Rule type name (e.g., "pay", "login")
* @param array $args Constructor arguments
* @return RuleStrategy
*/
public function rule($name, $args = []) {
$uniq_args_tag = md5(serialize($args));
$name = strtolower($name);
$name_arr = explode('_', $name);
$full_name = '';
foreach ($name_arr as $str) {
$full_name .= ucfirst($str);
}
$className = "Rule{$full_name}";
if (!empty($this->rule[$className][$uniq_args_tag])) {
return $this->rule[$className][$uniq_args_tag];
}
if (empty($args)) {
$this->rule[$className][$uniq_args_tag] = new $className();
} else {
$ref = new ReflectionClass($className);
$this->rule[$className][$uniq_args_tag] = $ref->newInstanceArgs($args);
}
return $this->rule[$className][$uniq_args_tag];
}
}
class Client {
public function rule_check($rule_type) {
$rule_context = new RuleContext();
$strategy = $rule_context->rule($rule_type);
$strategy->ruleCheck();
return true;
}
}Now adding a new rule only requires creating a new strategy class; the client code remains untouched.
class RuleOnlineDuration extends RuleStrategy {
public function ruleCheck() {
// handle online duration rule
}
}Benefits Achieved
Better encapsulation: each rule lives in its own class.
Improved maintainability: changes to one rule do not affect others.
Enhanced testability: individual strategies can be unit‑tested.
Scalability: new rules are added without modifying existing client logic.
The combination of the Strategy pattern and PHP reflection yields a clean, extensible generic rule module that can be shared across multiple activity components.
Illustrative Diagrams
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
