Building Rules with the Fluent API
MilliRules provides an elegant, fluent API that makes building rules intuitive and readable. This guide covers everything from basic rule creation to advanced pattern matching and complex condition logic.
The Fluent Interface#Copied!
The fluent interface allows you to chain methods together to build rules in a natural, readable way:
flowchart LR
Create["Rules::create()"] --> Meta["Metadata<br/><i> ->title(), ->order(), ->enabled() </i>"]
Meta --> When["when()<br/><i>+ conditions</i>"]
When --> Then["then()<br/><i>+ actions</i>"]
Then --> Register["register()"]
1use MilliRulesRules;
2
3Rules::create('my_rule')
4 ->title('My Rule Title')
5 ->order(10)
6 ->enabled(true)
7 ->when()
8 ->condition1()
9 ->condition2()
10 ->then()
11 ->action1()
12 ->action2()
13 ->register();
Each method returns the builder object, allowing you to continue chaining.
Creating Rules#Copied!
Basic Rule Creation#Copied!
The create() method is your starting point:
1use MilliRulesRules;
2
3// Minimal rule with auto-detected type
4$rule = Rules::create('rule_id');
5
6// With explicit type
7$rule = Rules::create('rule_id', 'wp'); // WordPress rule
8$rule = Rules::create('rule_id', 'php'); // PHP rule
'my_plugin_rule_id' or 'company_feature_rule'.Replacing and Removing Rules#Copied!
Rule Replacement
Registering a rule with an existing ID replaces the previous rule:
1// Original rule
2Rules::create('api_cache')
3 ->when()->request_url('/api/*')
4 ->then()->custom('cache_response', ['ttl' => 3600])
5 ->register();
6
7// Later: Replace with updated version (same ID)
8Rules::create('api_cache')
9 ->when()->request_url('/api/*')
10 ->then()->custom('cache_response', ['ttl' => 7200]) // Different TTL
11 ->register();
12
13// Only the second rule exists - the first was replaced
This is useful for:
- Child themes overriding parent theme rules
- Plugins modifying default rules
- Environment-specific rule customization
Removing Rules
To completely remove a rule, use Rules::unregister():
1// Remove a rule by ID
2Rules::unregister('unwanted_rule');
3
4// Example: Child theme disables parent's rule
5Rules::unregister('parent_theme_sidebar_rule');
6
7// Example: Conditionally disable rules
8if (wp_get_environment_type() === 'production') {
9 Rules::unregister('debug_logging_rule');
10}
Rules::unregister() returns true if the rule was found and removed, false otherwise.Rule Metadata#Copied!
Add descriptive information to your rules:
1Rules::create('api_cache_control')
2 ->title('Control API Response Caching') // Human-readable title
3 ->order(15) // Execution sequence
4 ->enabled(true) // Enable/disable
5 ->register();
Setting Execution Order
The order() method controls when rules execute (lower numbers execute first):
1// These execute in sequence: security → cache → logging
2Rules::create('security_check')->order(5)->when()->then()->register();
3Rules::create('cache_control')->order(10)->when()->then()->register();
4Rules::create('request_logging')->order(15)->when()->then()->register();
Enabling/Disabling Rules
1// Enable rule
2Rules::create('my_rule')->enabled(true)->when()->then()->register();
3
4// Disable rule (useful for testing or feature flags)
5Rules::create('my_rule')->enabled(false)->when()->then()->register();
6
7// Use constant for dynamic control
8Rules::create('debug_rule')
9 ->enabled(defined('WP_DEBUG') && WP_DEBUG)
10 ->when()->then()->register();
Building Conditions#Copied!
Conditions determine when a rule should execute. The when() method starts the condition builder.
Basic Conditions#Copied!
1Rules::create('check_request')
2 ->when()
3 ->request_url('/api/users') // Check URL
4 ->request_method('POST') // Check HTTP method
5 ->then()
6 ->custom('process_users')
7 ->register();
Condition Chaining#Copied!
Chain multiple conditions together:
1Rules::create('secure_api')
2 ->when()
3 ->request_url('/api/*') // URL pattern
4 ->request_method('POST') // HTTP method
5 ->request_header('Content-Type', 'application/json') // Header
6 ->cookie('session_id') // Cookie exists
7 ->then()
8 ->custom('process_request')
9 ->register();
By default, all conditions must be true (AND logic).
Match Types: Controlling Condition Logic#Copied!
MilliRules supports three match types that control how conditions are evaluated:
Match All (AND Logic) - Default
All conditions must be true:
1Rules::create('strict_validation')
2 ->when() // Implicitly uses match_all()
3 ->request_url('/api/secure')
4 ->request_method('POST')
5 ->cookie('auth_token')
6 ->then()
7 ->custom('process_secure_request')
8 ->register();
Evaluates as: condition1 AND condition2 AND condition3
Match Any (OR Logic)
At least one condition must be true:
1Rules::create('flexible_access')
2 ->when()
3 ->match_any() // Use OR logic
4 ->request_url('/public/*')
5 ->cookie('visitor_token')
6 ->is_user_logged_in()
7 ->then()
8 ->custom('grant_access')
9 ->register();
Evaluates as: condition1 OR condition2 OR condition3
Match None (NOT Logic)
All conditions must be false:
1Rules::create('production_only')
2 ->when()
3 ->match_none() // Use NOT logic
4 ->constant('WP_DEBUG', true)
5 ->constant('WP_LOCAL_DEV', true)
6 ->then()
7 ->custom('enable_production_features')
8 ->register();
Evaluates as: NOT condition1 AND NOT condition2
Alternative Match Type Methods#Copied!
You can also use dedicated methods to start condition building with a specific match type:
1// These are equivalent:
2->when()->match_all()
3->when_all()
4
5->when()->match_any()
6->when_any()
7
8->when()->match_none()
9->when_none()
Example with alternative syntax:
1Rules::create('development_environments')
2 ->when_any() // Start with OR logic
3 ->constant('WP_DEBUG', true)
4 ->constant('WP_ENVIRONMENT_TYPE', 'local')
5 ->constant('WP_ENVIRONMENT_TYPE', 'development')
6 ->then()
7 ->custom('enable_debug_tools')
8 ->register();
when() block. Choose one match type and stick with it for that condition group.Seamless Builder Transitions#Copied!
The fluent API allows seamless transitions between builders:
1Rules::create('wordpress_admin_check')
2 ->when()
3 ->request_url('/wp-admin/*')
4 ->is_user_logged_in() // WordPress condition
5 // Automatically transitions from ConditionBuilder to Rules
6 ->then()
7 ->custom('log_admin_access')
8 ->register();
This works because:
- The
->when()method returns aConditionBuilder - WordPress conditions (like
is_user_logged_in()) are not defined onConditionBuilder - The builder's
__call()magic method detects this - It adds the condition and returns the parent
Rulesobject - You can seamlessly call
->then()to build actions
Manual Builder Management#Copied!
For more control, you can manually manage builder instances:
1$rule = Rules::create('complex_rule');
2
3$conditions = $rule->when();
4$conditions->request_url('/api/*');
5$conditions->request_method('POST');
6
7$actions = $rule->then();
8$actions->custom('validate_data');
9$actions->custom('process_request');
10
11$rule->register();
Building Actions#Copied!
Actions execute when conditions are satisfied. The then() method starts the action builder.
Basic Actions#Copied!
1Rules::create('process_request')
2 ->when()
3 ->request_url('/api/process')
4 ->then()
5 ->custom('log_request')
6 ->custom('validate_data')
7 ->custom('process_data')
8 ->custom('send_response')
9 ->register();
Actions execute sequentially in the order they're defined.
Action Configuration#Copied!
Pass configuration to actions using arrays:
1Rules::create('send_notification')
2 ->when()
3 ->request_url('/api/notify')
4 ->then()
5 ->custom('send_email', [
6 'value' => '',
7 'subject' => 'New Notification',
8 'message' => 'You have a new notification'
9 ])
10 ->custom('log_notification', [
11 'value' => 'Email sent to admin'
12 ])
13 ->register();
Inline Actions with Closures#Copied!
For simple operations, define actions inline:
1use MilliRulesContext;
2
3Rules::register_action('inline_log', function($args, Context $context) {
4 $message = $args['value'] ?? 'No message';
5 error_log('MilliRules: ' . $message);
6});
7
8Rules::create('use_inline_action')
9 ->when()->request_url('/test')
10 ->then()->custom('inline_log', ['value' => 'Test URL accessed'])
11 ->register();
Using Operators#Copied!
Operators control how condition values are compared. While MilliRules auto-detects operators, you can specify them explicitly.
Explicit Operator Specification#Copied!
1Rules::create('operator_examples')
2 ->when()
3 // Equality
4 ->request_method('GET', '=') // Exact match (default)
5 ->request_method('POST', '!=') // Not equal
6
7 // Numeric comparison
8 ->request_param('age', '18', '>') // Greater than
9 ->request_param('age', '65', '<') // Less than
10
11 // Pattern matching
12 ->request_url('/admin/*', 'LIKE') // Wildcard pattern
13 ->request_url('/^\/api\/v[0-9]+\//i', 'REGEXP') // Regex
14
15 // Array membership
16 ->request_method(['GET', 'HEAD'], 'IN') // In array
17 ->request_method(['POST', 'PUT'], 'NOT IN') // Not in array
18
19 // Existence checking
20 ->cookie('session_id', null, 'EXISTS') // Cookie exists
21 ->cookie('temp_token', null, 'NOT EXISTS') // Cookie doesn't exist
22
23 // Boolean comparison
24 ->constant('WP_DEBUG', true, 'IS') // Is true
25 ->constant('WP_DEBUG', false, 'IS NOT') // Is not true
26 ->then()
27 ->custom('action')
28 ->register();
Auto-Detected Operators#Copied!
MilliRules automatically infers operators from values:
1Rules::create('auto_operators')
2 ->when()
3 // String → '=' operator
4 ->request_method('GET')
5
6 // Array → 'IN' operator
7 ->request_method(['GET', 'HEAD'])
8
9 // Boolean → 'IS' operator
10 ->constant('WP_DEBUG', true)
11
12 // Null → 'EXISTS' operator
13 ->cookie('session_id')
14
15 // String with wildcards → 'LIKE' operator
16 ->request_url('/admin/*')
17
18 // String starting with '/' → 'REGEXP' operator
19 ->request_url('/^\/api\//i')
20 ->then()
21 ->custom('action')
22 ->register();
For complete operator documentation, see Operators and Pattern Matching.
Custom Conditions#Copied!
When built-in conditions aren't enough, use custom conditions.
Inline Custom Conditions#Copied!
1use MilliRulesContext;
2
3Rules::register_condition('is_weekend', function(Context $context) {
4 $day = date('N'); // 1 (Monday) to 7 (Sunday)
5 return $day >= 6; // Saturday or Sunday
6});
7
8Rules::create('weekend_special')
9 ->when()
10 ->custom('is_weekend')
11 ->request_url('/special-offer')
12 ->then()
13 ->custom('show_weekend_discount')
14 ->register();
Parameterized Custom Conditions#Copied!
1use MilliRulesContext;
2
3Rules::register_condition('time_range', function($args, Context $context) {
4 $current_hour = (int) date('H');
5 $start = $args['start'] ?? 0;
6 $end = $args['end'] ?? 23;
7
8 return $current_hour >= $start && $current_hour <= $end;
9});
10
11Rules::create('business_hours')
12 ->when()
13 ->custom('time_range', ['start' => 9, 'end' => 17])
14 ->then()
15 ->custom('show_business_hours_message')
16 ->register();
See Creating Custom Conditions for advanced techniques.
WordPress Hook Integration#Copied!
WordPress rules can execute on specific hooks:
1Rules::create('admin_notice', 'wp')
2 ->on('admin_notices', 10) // Hook name and priority
3 ->when()
4 ->is_user_logged_in()
5 ->constant('WP_DEBUG', true)
6 ->then()
7 ->custom('show_debug_notice')
8 ->register();
Common WordPress Hooks#Copied!
1// Initialization
2->on('init', 10)
3->on('plugins_loaded', 10)
4
5// Frontend
6->on('wp', 10)
7->on('template_redirect', 10)
8->on('wp_enqueue_scripts', 10)
9
10// Admin
11->on('admin_init', 10)
12->on('admin_menu', 10)
13->on('admin_notices', 10)
14
15// Content
16->on('the_content', 10)
17->on('the_title', 10)
18
19// Saving
20->on('save_post', 10)
21->on('wp_insert_post', 10)
->on() method automatically sets the rule type to 'wp'. You don't need to specify the type explicitly when using hooks.Advanced Patterns#Copied!
1. Conditional Rule Registration#Copied!
Register rules only when needed:
1if (is_admin()) {
2 Rules::create('admin_only_rule')
3 ->when()->is_user_logged_in()
4 ->then()->custom('admin_action')
5 ->register();
6}
7
8if (defined('WP_CLI') && WP_CLI) {
9 Rules::create('cli_only_rule')
10 ->when()->custom('is_cli_context')
11 ->then()->custom('cli_action')
12 ->register();
13}
2. Dynamic Rule Generation#Copied!
Generate rules programmatically:
1$protected_urls = ['/admin', '/dashboard', '/settings'];
2
3foreach ($protected_urls as $url) {
4 Rules::create('protect_' . sanitize_title($url))
5 ->when()
6 ->request_url($url . '/*')
7 ->is_user_logged_in(false) // Not logged in
8 ->then()
9 ->custom('redirect_to_login', ['url' => $url])
10 ->register();
11}
3. Rule Groups with Shared Configuration#Copied!
Create related rules with shared settings:
1$api_rules_config = [
2 'order' => 10,
3 'type' => 'php',
4];
5
6$api_endpoints = ['users', 'posts', 'comments'];
7
8foreach ($api_endpoints as $endpoint) {
9 Rules::create("api_{$endpoint}_cache")
10 ->order($api_rules_config['order'])
11 ->when()
12 ->request_url("/api/{$endpoint}/*")
13 ->request_method('GET')
14 ->then()
15 ->custom('set_cache_headers', ['duration' => 3600])
16 ->register();
17}
4. Nested Condition Logic Simulation#Copied!
While MilliRules doesn't support nested conditions directly, you can simulate them with multiple rules:
1// Simulate: (A AND B) OR (C AND D)
2
3// Rule 1: A AND B
4Rules::create('condition_group_1')->order(10)
5 ->when()
6 ->request_url('/special/*') // A
7 ->cookie('premium_user') // B
8 ->then()
9 ->custom('grant_access')
10 ->register();
11
12// Rule 2: C AND D
13Rules::create('condition_group_2')->order(10)
14 ->when()
15 ->is_user_logged_in() // C
16 ->constant('EARLY_ACCESS', true) // D
17 ->then()
18 ->custom('grant_access')
19 ->register();
Method Chaining Reference#Copied!
Rule Builder Methods#Copied!
| Method | Parameters | Returns | Description |
|---|---|---|---|
create() |
string $id, ?string $type |
Rules |
Create new rule |
title() |
string $title |
Rules |
Set rule title |
order() |
int $order |
Rules |
Set execution order |
enabled() |
bool $enabled |
Rules |
Enable/disable rule |
when() |
- | ConditionBuilder |
Start condition builder |
when_all() |
- | ConditionBuilder |
Start with AND logic |
when_any() |
- | ConditionBuilder |
Start with OR logic |
when_none() |
- | ConditionBuilder |
Start with NOT logic |
then() |
?array $actions |
ActionBuilder |
Start action builder |
on() |
string $hook, int $priority |
Rules |
Set WordPress hook |
register() |
- | bool |
Register rule (replaces if ID exists) |
unregister() |
string $rule_id |
bool |
Remove rule by ID (static method) |
Condition Builder Methods#Copied!
| Method | Parameters | Returns | Description |
|---|---|---|---|
match_all() |
- | ConditionBuilder |
Use AND logic |
match_any() |
- | ConditionBuilder |
Use OR logic |
match_none() |
- | ConditionBuilder |
Use NOT logic |
custom() |
string $type, mixed $arg |
ConditionBuilder |
Add custom condition |
add_namespace() |
string $namespace |
ConditionBuilder |
Add condition namespace |
{dynamic}() |
mixed ...$args |
mixed |
Dynamic condition methods |
Action Builder Methods#Copied!
| Method | Parameters | Returns | Description |
|---|---|---|---|
custom() |
string $type, mixed $arg |
ActionBuilder |
Add custom action |
add_namespace() |
string $namespace |
ActionBuilder |
Add action namespace |
{dynamic}() |
mixed ...$args |
mixed |
Dynamic action methods |
Best Practices#Copied!
1. Use Descriptive Rule IDs#Copied!
1// ✅ Good - clear and descriptive
2Rules::create('block_non_authenticated_api_access')
3Rules::create('cache_public_api_responses')
4Rules::create('log_admin_user_actions')
5
6// ❌ Bad - unclear and unmaintainable
7Rules::create('rule1')
8Rules::create('check')
9Rules::create('x')
2. Add Titles to All Rules#Copied!
1// ✅ Good - includes helpful title
2Rules::create('api_authentication')
3 ->title('Enforce API Authentication')
4 ->when()->request_url('/api/*')
5 ->then()->custom('check_auth')
6 ->register();
7
8// ❌ Bad - no title makes debugging harder
9Rules::create('api_authentication')
10 ->when()->request_url('/api/*')
11 ->then()->custom('check_auth')
12 ->register();
3. Keep Condition Groups Logical#Copied!
1// ✅ Good - logical grouping
2Rules::create('secure_api_access')
3 ->when()
4 ->request_url('/api/secure/*') // Context: API endpoint
5 ->request_method('POST') // Context: HTTP method
6 ->cookie('auth_token') // Context: Authentication
7 ->then()->custom('process_secure_request')
8 ->register();
9
10// ❌ Bad - unrelated conditions
11Rules::create('random_checks')
12 ->when()
13 ->request_url('/api/*')
14 ->is_home() // Unrelated to API
15 ->constant('WP_DEBUG', true) // Unrelated to request
16 ->then()->custom('do_something')
17 ->register();
4. Use Comments for Complex Logic#Copied!
1Rules::create('complex_caching_logic')
2 ->order(15)
3 ->when()
4 // Check if this is a cacheable request
5 ->request_method(['GET', 'HEAD'], 'IN')
6
7 // Ensure we're not in admin or login areas
8 ->request_url('/wp-admin/*', 'NOT LIKE')
9 ->request_url('/wp-login.php', '!=')
10
11 // Verify user preferences allow caching
12 ->cookie('disable_cache', null, 'NOT EXISTS')
13 ->then()
14 ->custom('apply_caching_headers')
15 ->register();
5. Test Rules Incrementally#Copied!
1// Start simple
2Rules::create('test_rule')
3 ->when()->request_url('/test')
4 ->then()->custom('log', ['value' => 'Test URL hit'])
5 ->register();
6
7// Add complexity gradually
8Rules::create('test_rule')
9 ->when()
10 ->request_url('/test')
11 ->request_method('POST') // Add second condition
12 ->then()
13 ->custom('log', ['value' => 'Test POST hit'])
14 ->register();
Common Pitfalls#Copied!
1. Forgetting to Register#Copied!
1// ❌ Wrong - rule never registered
2Rules::create('my_rule')
3 ->when()->request_url('/test')
4 ->then()->custom('action');
5// Missing ->register()
6
7// ✅ Correct
8Rules::create('my_rule')
9 ->when()->request_url('/test')
10 ->then()->custom('action')
11 ->register(); // Always register!
2. Mixing Match Types#Copied!
1// ❌ Wrong - cannot switch match types mid-chain
2Rules::create('mixed_logic')
3 ->when()
4 ->match_all()
5 ->condition1()
6 ->match_any() // Cannot switch!
7 ->condition2()
8
9// ✅ Correct - use one match type
10Rules::create('consistent_logic')
11 ->when_any()
12 ->condition1()
13 ->condition2()
3. Incorrect Hook Timing#Copied!
1// ❌ Wrong - registering rules too late
2add_action('wp_footer', function() {
3 MilliRules::init();
4 Rules::create('my_rule')->on('init')->when()->then()->register();
5 // 'init' hook already fired!
6});
7
8// ✅ Correct - register early
9add_action('plugins_loaded', function() {
10 MilliRules::init();
11 Rules::create('my_rule')->on('template_redirect')->when()->then()->register();
12}, 1); // Early priority
Troubleshooting#Copied!
Rules Not Executing#Copied!
Check initialization:
1// Verify MilliRules is initialized
2$packages = MilliRules::get_loaded_packages();
3error_log('Loaded packages: ' . print_r($packages, true));
Verify rule registration:
1Rules::create('debug_rule')
2 ->title('Debug Rule')
3 ->when()->request_url('*')
4 ->then()->custom('log', ['value' => 'Rule executed'])
5 ->register();
6
7error_log('Rule registered');
Check execution statistics:
1$result = MilliRules::execute_rules();
2error_log('Execution stats: ' . print_r($result, true));
Conditions Not Matching#Copied!
Add debugging to your conditions:
1use MilliRulesContext;
2
3Rules::register_condition('debug_condition', function(Context $context) {
4 $array = $context->to_array();
5 error_log('Context: ' . print_r($array, true));
6 return true;
7});
8
9Rules::create('debug_rule')
10 ->when()
11 ->custom('debug_condition')
12 ->your_actual_condition()
13 ->then()->custom('action')
14 ->register();
Next Steps#Copied!
- Built-in Conditions Reference - Explore all available conditions
- Operators and Pattern Matching - Master comparison operators
- Custom Conditions - Create your own conditions
- WordPress Integration - WordPress-specific features
Need more examples? Check out Real-World Examples for complete, working code samples.