Creating Custom Conditions
Custom conditions define the "when" logic that determines if a rule should execute. This guide covers registering and using custom conditions.
Quick Start#Copied!
1use MilliRulesRules;
2use MilliRulesContext;
3
4// 1. Register reusable condition
5Rules::register_condition('is_weekend', function ($args, Context $context) {
6 // Reusable logic receiving args + context
7 $day = date('N'); // 1 (Monday) to 7 (Sunday)
8 return $day >= 6; // Saturday or Sunday
9});
10
11// 2. Create Rule
12Rules::create('weekend_special')
13 ->when()
14 // Option A: Inline custom condition without registration
15 ->custom('is_business_hours', function (Context $context) {
16 $hour = (int) date('H');
17 return $hour >= 9 && $hour <= 17;
18 })
19
20 // Option B: Call registered condition via magic method
21 ->is_weekend()
22
23 // Option C: Call registered condition via custom()
24 ->custom('is_weekend')
25 ->then()
26 ->custom('apply_discount')
27 ->register();
Registering Conditions#Copied!
Choosing the Right Registration Method#Copied!
MilliRules offers four ways to define custom conditions. Choose based on your needs:
| Method | Best For | Reusable? | Operator Support? |
|---|---|---|---|
Inline with ->custom() |
One-off checks | ❌ No | ❌ No |
| Callback Registration | Simple boolean checks | ✅ Yes | ❌ No |
| Namespace Registration | Complex conditions with operators | ✅ Yes | ✅ Yes (via BaseCondition) |
| Manual Wrapper | Advanced use cases | ✅ Yes | ✅ Yes (if using BaseCondition) |
Recommendation: Start with inline ->custom() for one-off checks. Use callback registration for reusable simple checks. Use namespace registration for complex conditions with operator support.
Method 1: Inline with ->custom() (Simplest - One-Off Checks)#Copied!
Best for: Quick one-off conditions that are only used in a single rule.
Define the condition directly in the rule using a callback:
1use MilliRulesRules;
2use MilliRulesContext;
3
4Rules::create('business_hours_only')
5 ->when()
6 ->custom('is_business_hours', function(Context $context) {
7 // One-off condition logic right here
8 $hour = (int) date('H');
9 return $hour >= 9 && $hour <= 17;
10 })
11 ->then()
12 ->custom('process_request')
13 ->register();
Note: Inline callbacks receive only the Context parameter (not $args), since arguments are redundant for inline-defined conditions. To access context data, use $context->get('key').
Example with context access:
1Rules::create('premium_user_check')
2 ->when()
3 ->custom('is_premium', function(Context $context) {
4 $user = $context->get('user.id');
5 $status = get_user_meta($user, 'account_status', true);
6
7 return $status === 'premium';
8 })
9 ->then()
10 ->custom('enable_features')
11 ->register();
Pros:
- ✅ Very simple - no separate registration step
- ✅ Perfect for one-off checks
- ✅ Quick to write and test
- ✅ Clean signature - only receives Context
Cons:
- ❌ Not reusable across multiple rules
- ❌ No operator support
- ❌ Harder to test in isolation
Method 2: Callback Registration (Reusable Simple Checks)#Copied!
Best for: Reusable boolean checks across multiple rules, simple logic without operators.
Register once, use everywhere:
1use MilliRulesRules;
2use MilliRulesContext;
3
4// Register once at plugin initialization
5Rules::register_condition('is_weekend', function($args, Context $context) {
6 $day = date('N'); // 1 (Monday) to 7 (Sunday)
7 return $day >= 6; // Saturday or Sunday
8});
9
10// With configuration
11Rules::register_condition('time_in_range', function($args, Context $context) {
12 $current_hour = (int) date('H');
13 $start = $args[0] ?? 0;
14 $end = $args[1] ?? 23;
15
16 return $current_hour >= $start && $current_hour <= $end;
17});
18
19// Access context data
20Rules::register_condition('user_has_role', function($args, Context $context) {
21 $context->load('user');
22 $required_role = $args['value'] ?? $args[0] ?? '';
23 $user_roles = $context->get('user.roles', []);
24
25 return in_array($required_role, $user_roles, true);
26});
Then use in any rule:
1Rules::create('weekend_special')
2 ->when()
3 ->is_weekend()
4 ->time_in_range(9, 17)
5 ->user_has_role('customer')
6 ->then()
7 ->custom('apply_discount')
8 ->register();
Pros:
- ✅ Reusable across all rules
- ✅ Simple to register and use
- ✅ Good for boolean checks
Cons:
- ❌ No operator support (must implement manually)
- ❌ Harder to organize many conditions
Method 3: Namespace Registration (Best for Classes)#Copied!
Best for: Complex conditions with operator support, reusable logic, testable code.
Register an entire namespace once and all condition classes are auto-discovered:
1use MilliRulesRules;
2
3// One-time registration at plugin initialization
4Rules::register_namespace('Conditions', 'MyPluginConditions');
Create your condition class:
1namespace MyPluginConditions;
2
3use MilliRulesConditionsBaseCondition;
4use MilliRulesContext;
5
6class UserPurchaseCount extends BaseCondition
7{
8 protected function get_actual_value(Context $context): int
9 {
10 $context->load('user');
11 $user_id = $context->get('user.id', 0);
12
13 if (!$user_id) {
14 return 0;
15 }
16
17 // Get purchase count from database
18 return (int) get_user_meta($user_id, 'purchase_count', true);
19 }
20
21 protected function get_expected_value(): int
22 {
23 return (int) ($this->config['value'] ?? 0);
24 }
25
26 public function get_type(): string
27 {
28 return 'user_purchase_count'; // Used for auto-discovery
29 }
30}
How it works:
- The class name
UserPurchaseCountis converted touser_purchase_count - MilliRules finds the class automatically via
get_type() - No need to manually register each condition
- Supports all operators (=, !=, >, >=, <, <=, LIKE, IN, REGEXP, EXISTS, IS)
- Access configuration via
$this->config
Usage:
1Rules::create('vip_customers')
2 ->when()
3 // Both calling styles work identically
4 ->user_purchase_count(10, '>=') // Auto-discovered, operator supported
5 // OR
6 ->custom('user_purchase_count', ['value' => 10, 'operator' => '>='])
7 ->then()
8 ->custom('apply_vip_discount')
9 ->register();
Method 4: Manual Wrapper (Advanced - Rarely Needed)#Copied!
Only use when: Namespace registration isn't suitable (dynamic class names, runtime conditions, etc.)
1use MilliRulesRules;
2
3// ⚠️ Avoid this if possible - creates type duplication
4Rules::register_condition('user_purchase_count', function($args, Context $context) {
5 $condition = new MyPluginConditionsUserPurchaseCount($args, $context);
6 return $condition->matches($context);
7});
Why to avoid:
- Type specified twice (in registration AND in
get_type()) - More verbose than namespace registration
- Harder to maintain
When it's necessary:
- You can't register the entire namespace
- You need to pass custom dependencies to the constructor
- You need conditional registration logic
Using Registered Conditions#Copied!
Once registered, conditions can be used via dynamic method calls:
1Rules::create('special_offer')
2 ->when()
3 ->is_weekend() // Dynamic method
4 ->time_in_range(9, 17) // With parameters
5 ->user_has_role('customer') // Single value
6 ->then()
7 ->custom('send_offer')
8 ->register();
Argument Patterns#Copied!
Both ->condition_name() and ->custom() accept identical argument formats:
No parameters (boolean check):
1->is_weekend()
2->custom('is_weekend')
3// Both result in: ['type' => 'is_weekend']
Single value (equality check):
1->user_role('administrator')
2->custom('user_role', 'administrator')
3// Both result in: ['type' => 'user_role', 'value' => 'administrator', 'operator' => '=']
Value with operator:
1->user_age(18, '>=')
2->custom('user_age', ['value' => 18, 'operator' => '>='])
3// Both result in: ['type' => 'user_age', 'value' => 18, 'operator' => '>=']
Multiple positional arguments:
1->time_in_range(9, 17)
2// Result in: ['type' => 'time_in_range', 0 => 9, 1 => 17]
3// Access via: $args[0], $args[1]
When to use which:
- Dynamic methods: Shorter syntax when method name is the condition type
->custom(): When condition type needs to be dynamic or passed as variable
Operator Support#Copied!
Custom conditions inheriting from BaseCondition automatically support all standard operators:
=,==- Equality!=,<>- Not equal>,>=,<,<=- ComparisonLIKE,NOT LIKE- Pattern matchingIN,NOT IN- Array membershipREGEXP- Regular expressionEXISTS,NOT EXISTS- Value existenceIS- Boolean strict comparison
See Operators Reference for complete details.
Auto-Detection#Copied!
When using dynamic methods, operators are auto-detected from value types:
1->user_age(18) // Auto: '=' for scalar
2->user_age(18, '>=') // Explicit: '>='
3->user_email('') // Auto: 'LIKE' for wildcard
4->user_role(['admin', 'editor']) // Auto: 'IN' for array
5->is_logged_in(true) // Auto: 'IS' for boolean
Configuration Reference#Copied!
Standard Config Keys#Copied!
1[
2 'type' => 'condition_type', // Required: condition identifier
3 'value' => 'expected_value', // Common: value to compare against
4 'operator' => '=', // Common: comparison operator (default: '=')
5 'name' => 'field_name', // For name-based conditions (header, param, etc.)
6 // ... custom keys as needed
7]
Common Patterns#Copied!
Boolean check:
1->is_weekend()
2// Becomes: ['type' => 'is_weekend', 'operator' => 'IS']
Single value (equality):
1->user_role('administrator')
2// Becomes: ['type' => 'user_role', 'value' => 'administrator', 'operator' => '=']
Value with operator:
1->user_age(18, '>=')
2// Becomes: ['type' => 'user_age', 'value' => 18, 'operator' => '>=']
Name-based condition:
1->request_header('User-Agent', '*Chrome*')
2// Becomes: ['type' => 'request_header', 'name' => 'User-Agent', 'value' => '*Chrome*', 'operator' => 'LIKE']
Complex configuration:
1->custom('advanced_check', [
2 'value' => 'expected',
3 'operator' => 'LIKE',
4 'case_sensitive' => false,
5 'cache' => true
6])
Best Practices#Copied!
1. Always Return Boolean#Copied!
1// ✅ Good - explicit boolean return
2Rules::register_condition('is_valid', function($args, Context $context) {
3 $value = $context->get('custom.value');
4 return (bool) $value; // Explicit cast
5});
6
7// ❌ Bad - may return non-boolean
8Rules::register_condition('is_valid', function($args, Context $context) {
9 return $context->get('custom.value'); // Could be string, int, null...
10});
2. Avoid Side Effects#Copied!
1// ✅ Good - pure check
2Rules::register_condition('has_permission', function($args, Context $context) {
3 return current_user_can($args['value'] ?? 'read');
4});
5
6// ❌ Bad - modifies state
7Rules::register_condition('has_permission', function($args, Context $context) {
8 update_option('last_check', time()); // Don't do this!
9 return current_user_can($args['value'] ?? 'read');
10});
3. Handle Missing Data Gracefully#Copied!
1Rules::register_condition('user_has_meta', function($args, Context $context) {
2 $context->load('user');
3 $user_id = $context->get('user.id', 0);
4
5 // Handle case where user is not logged in
6 if (!$user_id) {
7 return false;
8 }
9
10 $meta_key = $args['value'] ?? $args[0] ?? '';
11 return !empty(get_user_meta($user_id, $meta_key, true));
12});
4. Use Type Hints#Copied!
1use MilliRulesContext;
2
3Rules::register_condition('my_check', function(array $args, Context $context): bool {
4 // Full IDE autocomplete and type safety
5 $value = $context->get('custom.key');
6 return $value === ($args['value'] ?? null);
7});
Common Pitfalls#Copied!
Must Return Boolean#Copied!
1// ❌ Wrong - returns string
2Rules::register_condition('check_status', function($args, Context $context) {
3 return get_option('site_status'); // Returns 'active', 'inactive', etc.
4});
5
6// ✅ Correct - returns boolean
7Rules::register_condition('is_active', function($args, Context $context) {
8 return get_option('site_status') === 'active';
9});
Don't Cache Incorrectly#Copied!
1// ❌ Bad - static cache persists across requests
2static $cache = null;
3Rules::register_condition('expensive_check', function($args, Context $context) use (&$cache) {
4 if ($cache === null) {
5 $cache = expensive_calculation();
6 }
7 return $cache > 10; // Stale data on subsequent requests!
8});
9
10// ✅ Good - use transients or request-scoped caching
11Rules::register_condition('expensive_check', function($args, Context $context) {
12 $result = get_transient('expensive_check_result');
13 if (false === $result) {
14 $result = expensive_calculation();
15 set_transient('expensive_check_result', $result, 60);
16 }
17 return $result > 10;
18});
Don't Perform Actions in Conditions#Copied!
1// ❌ Wrong - sends email every time condition is checked
2Rules::register_condition('notify_admin', function($args, Context $context) {
3 wp_mail('', 'Check ran', 'Condition checked');
4 return true;
5});
6
7// ✅ Correct - conditions check, actions do things
8Rules::register_condition('should_notify', function($args, Context $context) {
9 return $context->get('user.login') === 'special_user';
10});
11
12// Then use an action to send email when condition matches
Next Steps#Copied!
- Custom Actions - Implement action logic
- Built-in Conditions Reference - See available conditions
- Operators Reference - Complete operator guide
- API Reference - Complete API documentation