Core Concepts - Rules, Conditions, and Actions

Understanding MilliRules' core concepts is essential for building powerful, maintainable rules. This guide explains the fundamental architecture and how all the pieces work together.

The Rule Engine Architecture#Copied!

MilliRules follows a simple but powerful pattern: When conditions are met, then actions execute.

flowchart TB
    subgraph Rule["Rule Definition"]
        direction LR
        Conditions["Conditions<br/>(When)"] -->|"match"| Actions["Actions<br/>(Then)"]
    end

    subgraph Foundation["Context & Packages"]
        Context["Context Data"]
        Packages["Package Providers"]
    end

    Conditions --> Context
    Actions --> Context
    Context <--> Packages

What is a Rule?#Copied!

A rule is a self-contained unit of logic that:

  • Has a unique identifier
  • Contains one or more conditions
  • Contains one or more actions
  • Executes when its conditions are satisfied
  • Operates on a shared context

Rule Structure#Copied!

Every rule consists of:

 1use MilliRulesRules;
 2
 3Rules::create('rule_id')           // Unique identifier
 4    ->title('Rule Title')           // Human-readable name (optional)
 5    ->order(10)                     // Execution sequence (optional)
 6    ->enabled(true)                 // Enable/disable flag (optional)
 7    ->when()                        // Condition builder
 8        ->condition1()
 9        ->condition2()
10    ->then()                        // Action builder
11        ->action1()
12        ->action2()
13    ->register();                   // Register with engine

Rule Properties#Copied!

Property Type Description Default
id string Unique identifier (required) -
title string Human-readable name Empty
order int Execution sequence (lower = first) 10
enabled bool Whether rule should execute true
type string Rule type (php or wp) Auto-detected
match_type string Condition logic (all, any, none) all
conditions array Condition configurations []
actions array Action configurations []
Important: Rule IDs must be unique within your application. Using duplicate IDs may cause unexpected behavior. Consider prefixing IDs with your plugin or project name.

Rule Execution Order#Copied!

Rules execute in sequence based on their order value:

 1Rules::create('second_rule')->order(10)->when()->request_url('/test')->then()->register();
 2Rules::create('first_rule')->order(5)->when()->request_url('/test')->then()->register();
 3Rules::create('third_rule')->order(20)->when()->request_url('/test')->then()->register();

Execution order: first_rule → second_rule → third_rule

Tip: Use order ranges to organize rules by purpose:

  • 0-9: Core system rules
  • 10-19: Default application rules
  • 20-49: Feature-specific rules
  • 50-99: Override rules
  • 100+: Emergency override rules

Why Order Matters#Copied!

When multiple rules modify the same value or state:

 1// Rule 1 (order: 10) sets cache to 3600 seconds
 2Rules::create('cache_short')->order(10)
 3    ->when()->request_url('/api/*')
 4    ->then()->custom('set_cache', ['value' => '3600'])
 5    ->register();
 6
 7// Rule 2 (order: 20) overrides cache to 7200 seconds
 8Rules::create('cache_long')->order(20)
 9    ->when()->request_url('/api/stable/*')
10    ->then()->custom('set_cache', ['value' => '7200'])
11    ->register();

For URL /api/stable/users:

  1. Both rules match
  2. Rule 1 executes first (order: 10) → cache set to 3600
  3. Rule 2 executes second (order: 20) → cache overridden to 7200
  4. Final value: 7200 seconds

Preventing Overwrites with Action Locking#Copied!

Sometimes you want to prevent later rules from overriding values. Use ->lock() to lock an action, preventing subsequent actions of the same type from executing:

 1// Rule 1 (order: 10) sets cache and LOCKS it
 2Rules::create('cache_short')->order(10)
 3    ->when()->request_url('/api/*')
 4    ->then()->custom('set_cache', ['value' => '3600'])->lock()  // Lock this action
 5    ->register();
 6
 7// Rule 2 (order: 20) tries to override but is BLOCKED
 8Rules::create('cache_long')->order(20)
 9    ->when()->request_url('/api/stable/*')
10    ->then()->custom('set_cache', ['value' => '7200'])  // IGNORED - cache is locked
11    ->register();

For URL /api/stable/users:

  1. Both rules match
  2. Rule 1 executes first (order: 10) → cache set to 3600 and locked
  3. Rule 2 matches, but its set_cache action is blocked (cache already locked)
  4. Final value: 3600 seconds (protected from override)

Key Points:

  • Locks are per action type, not per rule
  • Only affects actions with the same type
  • Different action types can still execute
  • Useful for security headers, cache TTL, access control decisions

Conditions: The "When" Logic#Copied!

Conditions determine whether a rule should execute. They evaluate the current context and return true or false.

Condition Types#Copied!

MilliRules provides two categories of conditions:

1. PHP Package Conditions (Framework-Agnostic)

Available in any PHP environment:

 1->when()
 2    ->request_url('/api/*')           // URL pattern matching
 3    ->request_method('POST')          // HTTP method
 4    ->request_header('Content-Type', 'application/json')  // Headers
 5    ->cookie('session_id')            // Cookie existence/value
 6    ->request_param('action', 'save') // Query/form parameters
 7    ->constant('WP_DEBUG', true)      // PHP/WordPress constants

2. WordPress Package Conditions

Available only in WordPress environments. MilliRules supports all WordPress is_* conditional tags (like is_single(), is_tax(), is_404(), etc.):

 1->when()
 2    ->is_user_logged_in()             // User authentication
 3    ->is_singular('post')             // Singular post/page
 4    ->is_archive()                    // Archive pages
 5    ->is_tax('channel', 'mtv', '!=')  // Taxonomy term with optional operator
 6    ->post_type('product')            // Post type
 7    ->is_home()                       // Home page
 8    ->is_sticky()                     // Supports ANY is_* function!
Note: WordPress conditions require the WordPress package to be loaded. MilliRules automatically detects WordPress and loads the appropriate package.

Condition Evaluation Logic#Copied!

MilliRules supports three evaluation strategies:

Match All (AND Logic)

All conditions must be true for the rule to execute. This is the default behavior.

 1Rules::create('secure_api_access')
 2    ->when()  // Implicitly uses match_all()
 3        ->request_url('/api/*')
 4        ->request_method('POST')
 5        ->request_header('Authorization', 'Bearer *', 'LIKE')
 6    ->then()
 7        ->custom('process_request')
 8    ->register();

Evaluates to: condition1 AND condition2 AND condition3

Match Any (OR Logic)

At least one condition must be true for the rule to execute.

 1Rules::create('development_environments')
 2    ->when()
 3        ->match_any()  // Explicit OR logic
 4        ->constant('WP_DEBUG', true)
 5        ->constant('WP_ENVIRONMENT_TYPE', 'local')
 6        ->constant('WP_ENVIRONMENT_TYPE', 'development')
 7    ->then()
 8        ->custom('enable_debug_bar')
 9    ->register();

Evaluates to: condition1 OR condition2 OR condition3

Match None (NOT Logic)

All conditions must be false for the rule to execute.

 1Rules::create('production_only')
 2    ->when()
 3        ->match_none()  // Explicit NOT logic
 4        ->constant('WP_DEBUG', true)
 5        ->constant('WP_ENVIRONMENT_TYPE', 'local')
 6    ->then()
 7        ->custom('enable_caching')
 8    ->register();

Evaluates to: NOT condition1 AND NOT condition2

Warning: You cannot mix match types within a single ->when() block. If you need complex logic like (A AND B) OR (C AND D), create separate rules or use custom condition callbacks.

Actions: The "Then" Behavior#Copied!

Actions are what happens when conditions are satisfied. They can modify data, trigger side effects, log information, or perform any operation.

Action Execution#Copied!

Actions execute immediately and sequentially when their rule's conditions match:

 1Rules::create('api_request_handler')
 2    ->when()
 3        ->request_url('/api/process')
 4    ->then()
 5        ->custom('log_request')      // Executes first
 6        ->custom('validate_data')    // Executes second
 7        ->custom('process_request')  // Executes third
 8        ->custom('send_response')    // Executes fourth
 9    ->register();

Action Types#Copied!

MilliRules supports various action types:

1. Custom Callback Actions

Define actions inline using callbacks:

 1Rules::register_action('send_email', function($args, Context $context) {
 2    $to = $args['to'] ?? '';
 3    $subject = $args['subject'] ?? 'Notification';
 4    $message = $args['message'] ?? 'Your message';
 5    wp_mail($to, $subject, $message);
 6});
 7
 8// Use in rules:
 9->then()
10    ->send_email(['to' => '', 'subject' => 'New User Registration'])
11    // OR
12    ->custom('send_email', ['to' => '', 'subject' => 'New User Registration'])

2. Class-Based Actions

Create reusable action classes:

 1use MilliRulesActionsActionInterface;
 2use MilliRulesContext;
 3
 4class SendEmailAction implements ActionInterface {
 5    private $config;
 6    private $context;
 7
 8    public function __construct(array $config, Context $context) {
 9        $this->config = $config;
10        $this->context = $context;
11    }
12
13    public function execute(Context $context): void {
14        $to = $this->config['value'] ?? '';
15        wp_mail($to, 'Subject', 'Message');
16    }
17
18    public function get_type(): string {
19        return 'send_email';
20    }
21}

3. WordPress Hook Actions

Trigger WordPress actions or filters with inlined callback:

 1->on('wp_mail', 10) // Registers with WordPress hook
 2->then()
 3    ->log_sent_mail(function($args, Context $context) {
 4        $hook_name = $context->get('hook.name'); // Will be 'wp_mail'
 5        $hook_args = $context->get('hook.args'); // Will be the array of arguments
 6
 7        if ($hook_name === 'wp_mail') {
 8            // Log email sent to $hook_args[0] with subject $hook_args[1] and message $hook_args[2]
 9            error_log("Sent email to {$hook_args[0]} with subject "{$hook_args[1]}"");
10        }
11    })
Tip: Use class-based actions for complex logic that requires state management or extensive configuration. Use callback actions for simple, one-off operations.

Context: Shared Data Pool#Copied!

The context is an object that provides lazy-loaded access to all the data available to conditions and actions. Context sections are loaded on-demand, meaning only the data you actually need is retrieved.

Context Structure#Copied!

Context provides a flat, organized structure:

 1use MilliRulesContext;
 2
 3// Context sections (loaded on-demand):
 4[
 5    'request' => [
 6        'method' => 'GET',
 7        'uri' => '/wp-admin/edit.php',
 8        'scheme' => 'https',
 9        'host' => 'example.com',
10        'path' => '/wp-admin/edit.php',
11        'query' => 'post_type=page',
12        'referer' => 'https://example.com',
13        'user_agent' => 'Mozilla/5.0...',
14        'headers' => [...],
15        'ip' => '192.168.1.1',
16    ],
17    'cookie' => [...],       // Cookies (separate from request)
18    'param' => [...],        // Request parameters (GET/POST)
19    'post' => [...],         // WordPress post data
20    'user' => [...],         // WordPress user data
21    'query' => [...],        // WordPress query variables (post_type, paged, s, etc.)
22    'term' => [...],         // WordPress taxonomy terms
23    // Custom package data...
24]

Lazy Loading#Copied!

Context data is loaded only when needed. This provides significant performance benefits by avoiding unnecessary data retrieval:

 1use MilliRulesMilliRules;
 2use MilliRulesContext;
 3
 4// Initialize MilliRules
 5MilliRules::init();
 6
 7// Context is created but data isn't loaded yet
 8$context = new Context();
 9
10// Data loads automatically when accessed
11$uri = $context->get('request.uri');  // Triggers 'request' provider loading
12
13// Access nested values using dot notation
14$userId = $context->get('user.id', 0);  // Triggers 'user' provider loading

Key features:

  • On-demand loading: Context sections load only when accessed
  • Memoization: Each section loads at most once per request
  • Granular providers: Separate providers for request, cookie, and param data
  • Automatic dependencies: Dependencies load automatically when needed

Accessing Context in Custom Code#Copied!

Callback actions and conditions receive the Context object, which provides methods to access data:

 1use MilliRulesContext;
 2
 3Rules::register_action('log_context', function($args, Context $context) {
 4    // get() automatically loads data (recommended)
 5    $method = $context->get('request.method', 'UNKNOWN');
 6    $user_id = $context->get('user.id', 0);
 7
 8    error_log("Request: $method, User: $user_id");
 9});

You can also explicitly load context sections for clarity:

 1use MilliRulesContext;
 2
 3Rules::register_action('log_context', function($args, Context $context) {
 4    // Explicit load() for clarity (optional)
 5    $context->load('request');
 6    $context->load('user');
 7
 8    $method = $context->get('request.method', 'UNKNOWN');
 9    $user_id = $context->get('user.id', 0);
10
11    error_log("Request: $method, User: $user_id");
12});

Context Methods#Copied!

The Context class provides several useful methods:

 1// Get a value using dot notation (automatically loads the section if needed)
 2$value = $context->get('post.type', 'post');
 3// ↑ Internally calls $context->load('post') if not already loaded
 4
 5// Set a value using dot notation
 6$context->set('custom.data', 'value');
 7
 8// Check if a path exists
 9if ($context->has('user.id')) {
10    // User data is loaded
11}
12
13// Explicitly load a context section (optional - get() does this automatically)
14$context->load('request');
15
16// Export context as array (for debugging)
17$array = $context->to_array();
Important: Context sections are loaded lazily. The get() method automatically loads the top-level section if it hasn't been loaded yet. You rarely need to call load() manually - it's mainly useful for pre-loading multiple sections or for code clarity.

Packages: Modular Functionality#Copied!

Packages are self-contained modules that provide:

  • Conditions
  • Actions
  • Context data
  • Placeholder resolvers

Built-in Packages#Copied!

PHP Package

Always available in any PHP environment:

  • Framework-agnostic HTTP conditions
  • Request/response handling
  • Cookie and header management
 1// PHP package is always loaded
 2MilliRules::init();

WordPress Package

Available only in WordPress:

  • WordPress-specific conditions
  • Hook-based execution
  • WordPress data in context
 1// Automatically loads WordPress package if WordPress is detected
 2MilliRules::init();

Package Dependencies#Copied!

Packages can depend on other packages:

 1// WordPress package requires PHP package
 2class WordPressPackage extends BasePackage {
 3    public function get_required_packages(): array {
 4        return ['PHP'];  // PHP must be loaded first
 5    }
 6}

MilliRules automatically resolves dependencies:

  1. Detects required packages
  2. Loads dependencies first
  3. Prevents circular dependencies
Warning: Circular dependencies (Package A requires Package B, Package B requires Package A) will cause an error. Design your packages carefully to avoid this.

Rule Types: PHP vs. WordPress#Copied!

MilliRules supports two rule types that determine execution strategy:

PHP Rules (type: 'php')#Copied!

  • Execute immediately when execute_rules() is called
  • No WordPress hook integration
  • Suitable for early execution (caching, redirects)
  • Framework-agnostic
 1Rules::create('cache_check', 'php')
 2    ->when()->request_url('/api/*')
 3    ->then()->custom('check_cache')
 4    ->register();
 5
 6// Manual execution required
 7MilliRules::execute_rules(['PHP']);

WordPress Rules (type: 'wp')#Copied!

  • Execute automatically on WordPress hooks
  • Integrated with WordPress lifecycle
  • Access to WordPress data and functions
  • Default type when WordPress is detected
 1Rules::create('admin_notice', 'wp')
 2    ->on('admin_notices', 10)  // Registers with WordPress hook
 3    ->when()->is_user_logged_in()
 4    ->then()->custom('show_notice')
 5    ->register();
 6
 7// Executes automatically when 'admin_notices' hook fires

Auto-Detection#Copied!

MilliRules auto-detects rule type based on:

  1. Explicit type parameter
  2. Used conditions (WordPress conditions → wp type)
  3. Hook registration (.on()wp type)
  4. Default to php if ambiguous
 1// Auto-detected as 'wp' due to WordPress condition
 2Rules::create('wp_rule_auto')
 3    ->when()->is_user_logged_in()
 4    ->then()->custom('action')
 5    ->register();
Tip: Always explicitly specify the rule type when creating rules to avoid ambiguity: Rules::create('rule_id', 'wp') or Rules::create('rule_id', 'php').

Execution Flow#Copied!

Understanding the execution flow helps debug issues and optimize performance:

1. Initialize MilliRules
   ↓
2. Register packages
   ↓
3. Load available packages
   ↓
4. Build context from packages
   ↓
5. Register rules
   ↓
6. Trigger execution (manual or hook-based)
   ↓
7. For each rule:
   a. Check if enabled
   b. Validate package availability
   c. Evaluate conditions
   d. If conditions match → execute actions
   ↓
8. Return execution statistics

Execution Statistics#Copied!

Every execution returns detailed statistics:

 1$result = MilliRules::execute_rules();
 2
 3/*
 4[
 5    'rules_processed' => 10,   // Total rules evaluated
 6    'rules_skipped' => 2,      // Rules skipped (disabled/missing packages)
 7    'rules_matched' => 5,      // Rules where conditions matched
 8    'actions_executed' => 12,  // Total actions executed
 9    'context' => [...],        // Execution context
10]
11*/
Tip: Use execution statistics for debugging and performance monitoring. Log them in development to understand rule behavior.

Best Practices#Copied!

1. Keep Rules Focused#Copied!

 1// ✅ Good - focused, single purpose
 2Rules::create('cache_api_responses')
 3    ->when()->request_url('/api/*')
 4    ->then()->custom('set_cache_headers')
 5    ->register();
 6
 7// ❌ Bad - too many responsibilities
 8Rules::create('do_everything')
 9    ->when()->request_url('*')
10    ->then()
11        ->custom('check_cache')
12        ->custom('validate_user')
13        ->custom('process_request')
14        ->custom('send_email')
15        ->custom('update_database')
16    ->register();

2. Use Descriptive Names#Copied!

 1// ✅ Good - clear purpose
 2Rules::create('block_non_authenticated_api_access')
 3    ->title('Block API Access for Non-Authenticated Users')
 4
 5// ❌ Bad - unclear purpose
 6Rules::create('rule1')
 7    ->title('Check stuff')

3. Order Rules Logically#Copied!

 1// ✅ Good - logical ordering
 2Rules::create('set_default_cache')->order(10)  // Set defaults first
 3Rules::create('override_api_cache')->order(20) // Override for specific cases
 4Rules::create('disable_cache_dev')->order(30)  // Development override last

4. Leverage Context#Copied!

 1use MilliRulesContext;
 2
 3// ✅ Good - uses context effectively
 4Rules::register_action('log_user_action', function($args, Context $context) {
 5    $action = $args['action'] ?? 'accessed';
 6    $user = $context->get('user.login', 'guest');
 7    $url = $context->get('request.uri', 'unknown');
 8    error_log("User $user $action $url");
 9});

Common Patterns#Copied!

1. Progressive Enhancement Pattern#Copied!

Layer features based on availability:

 1// Base rule for all environments
 2Rules::create('base_security')->order(10)
 3    ->when()->request_url('*')
 4    ->then()->custom('basic_security_headers')
 5    ->register();
 6
 7// Enhanced rule for WordPress
 8Rules::create('wp_security')->order(20)
 9    ->when()->is_user_logged_in()
10    ->then()->custom('additional_security_headers')
11    ->register();

3. Override Pattern#Copied!

Allow specific rules to override general rules:

 1// General rule
 2Rules::create('default_cache')->order(10)
 3    ->when()->request_url('*')
 4    ->then()->custom('set_cache', ['value' => '3600'])
 5    ->register();
 6
 7// Specific override
 8Rules::create('api_no_cache')->order(20)
 9    ->when()->request_url('/api/dynamic/*')
10    ->then()->custom('set_cache', ['value' => '0'])
11    ->register();

Next Steps#Copied!

Now that you understand core concepts, explore these topics:


Questions about core concepts? Check the Complete API Reference or explore Real-World Examples to see these concepts in action.