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
Important: Rule IDs must be unique across your entire application. Consider using a prefix to avoid conflicts: '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}
Tip: 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();
Tip: Use order increments of 5 or 10 to leave room for inserting rules between existing ones later.

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();
Warning: You cannot mix match types within a single 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:

  1. The ->when() method returns a ConditionBuilder
  2. WordPress conditions (like is_user_logged_in()) are not defined on ConditionBuilder
  3. The builder's __call() magic method detects this
  4. It adds the condition and returns the parent Rules object
  5. 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();
Tip: Let MilliRules auto-detect operators for cleaner code. Only specify operators explicitly when you need precise control or when auto-detection doesn't match your intent.

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)
Note: The ->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!


Need more examples? Check out Real-World Examples for complete, working code samples.