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 | [] |
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
- 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:
- Both rules match
- Rule 1 executes first (order: 10) → cache set to 3600
- Rule 2 executes second (order: 20) → cache overridden to 7200
- 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:
- Both rules match
- Rule 1 executes first (order: 10) → cache set to 3600 and locked
- Rule 2 matches, but its
set_cacheaction is blocked (cache already locked) - 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!
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
->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 })
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();
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:
- Detects required packages
- Loads dependencies first
- Prevents circular dependencies
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:
- Explicit
typeparameter - Used conditions (WordPress conditions →
wptype) - Hook registration (
.on()→wptype) - Default to
phpif 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();
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*/
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:
- Building Rules - Master the fluent API in depth
- Built-in Conditions Reference - Complete condition reference
- Operators - Pattern matching and comparisons
- Packages - Deep dive into the package system
Questions about core concepts? Check the Complete API Reference or explore Real-World Examples to see these concepts in action.