Built-in Actions Reference

Actions are the "then" part of your rules—they define what happens when conditions are met. Unlike conditions, MilliRules' action system is primarily designed around custom actions that you define for your specific needs.

Understanding the Action System#Copied!

MilliRules provides a flexible action framework that allows you to:

  • Register custom callback actions for quick, inline functionality
  • Create reusable action classes for complex operations
  • Execute actions sequentially in the order they're defined
  • Access full context within actions for data-driven decisions

Action Execution Flow#Copied!

When a rule's conditions match:

1. Rule conditions evaluate to true
   ↓
2. Rule engine triggers action execution
   ↓
3. For each action in the rule:
   a. Instantiate action with config and context
   b. Execute action's execute() method
   c. Continue to next action
   ↓
4. Return execution statistics

Actions execute immediately and sequentially. There's no action queue or deferred execution.

Important: Actions execute in the exact order they're defined in your rule. If one action fails, MilliRules logs the error and continues to the next action.

Action Types#Copied!

1. Custom Callback Actions#Copied!

The simplest way to create actions is using callback functions.

Registering Callback Actions

 1use MilliRulesRules;
 2use MilliRulesContext;
 3
 4// Simple action
 5Rules::register_action('log_message', function($args, Context $context) {
 6    $message = $args['message'] ?? $args[0] ?? 'No message';
 7    error_log('MilliRules: ' . $message);
 8});
 9
10// Action with context access
11Rules::register_action('log_user_action', function($args, Context $context) {
12    $action = $args['action'] ?? 'accessed';
13    $user = $context->get('user.login', 'guest');
14    $url = $context->get('request.uri', 'unknown');
15    error_log("User {$user} {$action} {$url}");
16});

Using Callback Actions in Rules

 1Rules::create('log_admin_access')
 2    ->when()
 3        ->request_url('/wp-admin/*')
 4        ->is_user_logged_in()
 5    ->then()
 6        ->custom('log_message', ['value' => 'Admin area accessed'])
 7        ->custom('log_user_action')
 8    ->register();

Callback Parameters:

  • $context (array): Full execution context with request, WP data, etc.
  • $config (array): Action configuration passed from the rule
Tip: Use callback actions for simple operations that don't require state management or extensive configuration.

2. Class-Based Actions#Copied!

For complex operations, create action classes implementing ActionInterface.

Action Interface

 1namespace MilliRulesInterfaces;
 2
 3interface ActionInterface {
 4    public function execute(Context $context): void;
 5    public function get_type(): string;
 6}

Creating an Action Class

 1namespace MyPluginActions;
 2
 3use MilliRulesInterfacesActionInterface;
 4
 5class SendEmailAction implements ActionInterface {
 6    private $config;
 7    private $context;
 8
 9    public function __construct(array $config, Context $context) {
10        $this->config = $config;
11        $this->context = $context;
12    }
13
14    public function execute(Context $context): void {
15        $to = $this->config['to'] ?? '';
16        $subject = $this->config['subject'] ?? 'Notification';
17        $message = $this->config['message'] ?? '';
18
19        // Access context for dynamic data
20        $user_email = $context['wp']['user']['email'] ?? $to;
21
22        wp_mail($to, $subject, $message);
23    }
24
25    public function get_type(): string {
26        return 'send_email';
27    }
28}

Registering the Namespace

 1use MilliRulesRuleEngine;
 2
 3// Register action namespace so MilliRules can find your action classes
 4RuleEngine::register_namespace('Actions', 'MyPluginActions');

Using Class-Based Actions

 1Rules::create('user_registration_notification')
 2    ->when()
 3        ->request_url('/wp-admin/user-new.php')
 4        ->request_param('action', 'createuser')
 5    ->then()
 6        ->custom('send_email', [
 7            'to' => '',
 8            'subject' => 'New User Registration',
 9            'message' => 'A new user has registered.'
10        ])
11    ->register();
Note: Class-based actions provide better organization, testability, and reusability for complex operations.

3. BaseAction Helper Class#Copied!

MilliRules provides a BaseAction abstract class that includes placeholder resolution.

 1namespace MyPluginActions;
 2
 3use MilliRulesActionsBaseAction;
 4
 5class CustomAction extends BaseAction {
 6    public function execute(Context $context): void {
 7        // Resolve placeholders in config values
 8        $message = $this->resolve_value($this->config['value'] ?? '');
 9
10        // Use resolved value
11        error_log($message);
12    }
13
14    public function get_type(): string {
15        return 'custom_action';
16    }
17}

Using placeholder resolution:

 1Rules::create('dynamic_logging')
 2    ->when()
 3        ->request_url('/api/*')
 4    ->then()
 5        ->custom('custom_action', [
 6            'value' => 'API request to {request.uri} from {request.ip}'
 7        ])
 8    ->register();
 9
10// Logs: "API request to /api/users from 192.168.1.1"

See Dynamic Placeholders for complete placeholder syntax.


Common Action Patterns#Copied!

1. Logging Actions#Copied!

 1// Simple logging
 2Rules::register_action('log', function($args, Context $context) {
 3    error_log($args['value'] ?? '');
 4});
 5
 6// Structured logging
 7Rules::register_action('log_structured', function($args, Context $context) {
 8    $data = [
 9        'timestamp' => time(),
10        'user' => $context->get('user.login', 'guest') ?? 'guest',
11        'ip' => $context['request']['ip'] ?? 'unknown',
12        'message' => $args['value'] ?? '',
13    ];
14    error_log(json_encode($data));
15});
16
17// Usage
18Rules::create('log_actions')
19    ->when()->request_url('/important/*')
20    ->then()
21        ->custom('log', ['value' => 'Important URL accessed'])
22        ->custom('log_structured', ['value' => 'Security alert'])
23    ->register();

2. Redirect Actions#Copied!

 1Rules::register_action('redirect', function($args, Context $context) {
 2    $url = $args['url'] ?? home_url();
 3    $status = $args['status'] ?? 302;
 4
 5    if (!headers_sent()) {
 6        wp_redirect($url, $status);
 7        exit;
 8    }
 9});
10
11// Usage
12Rules::create('redirect_logged_out_users')
13    ->when()
14        ->request_url('/members/*')
15        ->is_user_logged_in(false)
16    ->then()
17        ->custom('redirect', [
18            'url' => wp_login_url(),
19            'status' => 302
20        ])
21    ->register();
Warning: Redirect actions should typically be the last action in a rule, as they terminate execution with exit.

3. Cache Control Actions#Copied!

 1Rules::register_action('set_cache_headers', function($args, Context $context) {
 2    $duration = $args['duration'] ?? 3600;
 3
 4    if (!headers_sent()) {
 5        header("Cache-Control: public, max-age={$duration}");
 6        header('Expires: ' . gmdate('D, d M Y H:i:s', time() + $duration) . ' GMT');
 7    }
 8});
 9
10// Usage
11Rules::create('cache_api_responses')
12    ->when()
13        ->request_url('/api/*')
14        ->request_method(['GET', 'HEAD'], 'IN')
15    ->then()
16        ->custom('set_cache_headers', ['duration' => 7200])
17    ->register();

4. Database Operations#Copied!

 1Rules::register_action('log_to_database', function($args, Context $context) {
 2    global $wpdb;
 3
 4    $table = $wpdb->prefix . 'access_log';
 5    $wpdb->insert($table, [
 6        'user_id' => $context->get('user.id', 0) ?? 0,
 7        'url' => $context->get('request.uri', '') ?? '',
 8        'timestamp' => current_time('mysql'),
 9    ]);
10});
11
12// Usage
13Rules::create('track_premium_access')
14    ->when()
15        ->request_url('/premium/*')
16        ->is_user_logged_in()
17    ->then()
18        ->custom('log_to_database')
19    ->register();

5. WordPress Hook Triggers#Copied!

 1// Trigger WordPress actions
 2Rules::register_action('do_action', function($args, Context $context) {
 3    $hook = $args['value'] ?? '';
 4    $args = $args['args'] ?? [];
 5
 6    if ($hook) {
 7        do_action($hook, ...$args);
 8    }
 9});
10
11// Trigger WordPress filters
12Rules::register_action('apply_filters', function($args, Context $context) {
13    $hook = $args['value'] ?? '';
14    $value = $args['filter_value'] ?? '';
15    $args = $args['args'] ?? [];
16
17    if ($hook) {
18        return apply_filters($hook, $value, ...$args);
19    }
20});
21
22// Usage
23Rules::create('trigger_custom_hooks')
24    ->when()->request_url('/checkout/*')
25    ->then()
26        ->custom('do_action', [
27            'value' => 'my_checkout_started',
28            'args' => ['checkout_page']
29        ])
30    ->register();

6. Content Modification#Copied!

 1Rules::register_action('modify_content', function($args, Context $context) {
 2    add_filter('the_content', function($content) use ($config) {
 3        $prepend = $args['prepend'] ?? '';
 4        $append = $args['append'] ?? '';
 5
 6        return $prepend . $content . $append;
 7    }, $args['priority'] ?? 10);
 8});
 9
10// Usage
11Rules::create('add_disclaimer')
12    ->when()
13        ->is_singular('post')
14        ->post_type('product')
15    ->then()
16        ->custom('modify_content', [
17            'prepend' => '<div class="disclaimer">Product information may vary.</div>',
18            'priority' => 10
19        ])
20    ->register();

7. Conditional Execution#Copied!

 1Rules::register_action('execute_if', function($args, Context $context) {
 2    $condition = $args['condition'] ?? null;
 3    $callback = $args['callback'] ?? null;
 4
 5    if (is_callable($condition) && is_callable($callback)) {
 6        if ($condition($context)) {
 7            $callback($context);
 8        }
 9    }
10});
11
12// Usage
13Rules::create('conditional_action')
14    ->when()->request_url('/api/*')
15    ->then()
16        ->custom('execute_if', [
17            'condition' => function(Context $context) {
18                return date('H') >= 9 && date('H') <= 17; // Business hours
19            },
20            'callback' => function(Context $context) {
21                error_log('API accessed during business hours');
22            }
23        ])
24    ->register();

Action Configuration#Copied!

Actions receive configuration through the $config array:

 1Rules::register_action('flexible_action', function($args, Context $context) {
 2    // Common configuration keys
 3    $value = $args['value'] ?? '';           // Primary value
 4    $enabled = $args['enabled'] ?? true;     // Enable flag
 5    $priority = $args['priority'] ?? 10;     // Priority/order
 6    $options = $args['options'] ?? [];       // Additional options
 7
 8    // Custom configuration
 9    $custom_param = $args['custom_param'] ?? 'default';
10});
11
12// Usage with full configuration
13Rules::create('configured_action')
14    ->when()->request_url('/test')
15    ->then()
16        ->custom('flexible_action', [
17            'value' => 'test value',
18            'enabled' => true,
19            'priority' => 20,
20            'options' => ['key' => 'value'],
21            'custom_param' => 'custom value'
22        ])
23    ->register();
Tip: Use consistent configuration key names across your actions:

  • 'value' for the primary action value
  • 'enabled' for enable/disable flags
  • 'priority' for ordering within the action
  • 'options' for nested configuration


Accessing Context in Actions#Copied!

The context provides access to all available data:

 1Rules::register_action('context_aware_action', function($args, Context $context) {
 2    // Request data
 3    $url = $context->get('request.uri', '') ?? '';
 4    $method = $context->get('request.method', '') ?? '';
 5    $ip = $context['request']['ip'] ?? '';
 6
 7    // Cookies
 8    $session = $context['request']['cookies']['session_id'] ?? '';
 9
10    // WordPress data (if available)
11    $user_id = $context->get('user.id', 0) ?? 0;
12    $user_login = $context->get('user.login', 'guest') ?? 'guest';
13    $post_id = $context->get('post.id', 0) ?? 0;
14
15    // Query flags
16    $is_singular = $context['wp']['query']['is_singular'] ?? false;
17    $is_admin = $context['wp']['query']['is_admin'] ?? false;
18
19    // Use context data
20    error_log("User {$user_login} accessed {$url} from {$ip}");
21});

Full context structure:

 1[
 2    'request' => [
 3        'method' => 'GET',
 4        'uri' => '/path',
 5        'scheme' => 'https',
 6        'host' => 'example.com',
 7        'path' => '/path',
 8        'query' => 'key=value',
 9        'referer' => 'https://example.com',
10        'user_agent' => 'Mozilla/5.0...',
11        'headers' => [...],
12        'ip' => '192.168.1.1',
13        'cookies' => [...],
14        'params' => [...],
15    ],
16    'wp' => [  // WordPress only
17        'post' => [...],
18        'user' => [...],
19        'query' => [...],
20        'constants' => [...],
21    ],
22]

Error Handling in Actions#Copied!

MilliRules catches action exceptions and continues execution:

 1Rules::register_action('safe_action', function($args, Context $context) {
 2    try {
 3        // Risky operation
 4        $result = risky_operation();
 5
 6        if (!$result) {
 7            throw new Exception('Operation failed');
 8        }
 9    } catch (Exception $e) {
10        error_log('Action error: ' . $e->getMessage());
11        // Execution continues to next action
12    }
13});
Important: If an action throws an uncaught exception, MilliRules logs the error and continues to the next action. The rule is marked as executed even if actions fail.

Multiple Actions in Sequence#Copied!

Actions execute sequentially in definition order:

 1Rules::create('multi_step_process')
 2    ->when()->request_url('/process')
 3    ->then()
 4        ->custom('log', ['value' => '1. Starting process'])
 5        ->custom('validate_data')
 6        ->custom('log', ['value' => '2. Data validated'])
 7        ->custom('process_data')
 8        ->custom('log', ['value' => '3. Data processed'])
 9        ->custom('send_response')
10        ->custom('log', ['value' => '4. Response sent'])
11    ->register();

Execution flow:

  1. Log: "1. Starting process"
  2. Validate data
  3. Log: "2. Data validated"
  4. Process data
  5. Log: "3. Data processed"
  6. Send response
  7. Log: "4. Response sent"

Best Practices#Copied!

1. Keep Actions Focused#Copied!

 1// ✅ Good - single responsibility
 2Rules::register_action('log_access', function($args, Context $context) {
 3    error_log('Access logged');
 4});
 5
 6Rules::register_action('update_counter', function($args, Context $context) {
 7    update_option('access_count', get_option('access_count', 0) + 1);
 8});
 9
10// ❌ Bad - multiple responsibilities
11Rules::register_action('do_everything', function($args, Context $context) {
12    error_log('Access logged');
13    update_option('access_count', get_option('access_count', 0) + 1);
14    send_email('', 'Access', 'Someone accessed');
15    update_database();
16    // Too much in one action!
17});

2. Use Descriptive Action Names#Copied!

 1// ✅ Good
 2Rules::register_action('send_admin_notification_email', ...);
 3Rules::register_action('log_security_event', ...);
 4Rules::register_action('update_user_last_login_timestamp', ...);
 5
 6// ❌ Bad
 7Rules::register_action('send', ...);
 8Rules::register_action('log', ...);
 9Rules::register_action('update', ...);

3. Validate Configuration#Copied!

 1Rules::register_action('safe_action', function($args, Context $context) {
 2    // Validate required configuration
 3    if (empty($args['required_value'])) {
 4        error_log('Action error: missing required_value');
 5        return;
 6    }
 7
 8    // Validate data types
 9    $count = absint($args['count'] ?? 0);
10    $enabled = (bool) ($args['enabled'] ?? true);
11
12    // Proceed with validated data
13    // ...
14});

4. Check Prerequisites#Copied!

 1Rules::register_action('wordpress_dependent', function($args, Context $context) {
 2    // Check if WordPress functions are available
 3    if (!function_exists('wp_mail')) {
 4        error_log('WordPress not available');
 5        return;
 6    }
 7
 8    wp_mail($args['to'], $args['subject'], $args['message']);
 9});

5. Use Constants for Configuration#Copied!

 1// Define action configuration constants
 2define('DEFAULT_EMAIL_RECIPIENT', '');
 3define('DEFAULT_LOG_LEVEL', 'info');
 4
 5Rules::register_action('send_notification', function($args, Context $context) {
 6    $to = $args['to'] ?? DEFAULT_EMAIL_RECIPIENT;
 7    $level = $args['level'] ?? DEFAULT_LOG_LEVEL;
 8
 9    // Use constants for consistent configuration
10});

Common Pitfalls#Copied!

1. Modifying Context#Copied!

 1// ❌ Wrong - context modifications don't persist
 2Rules::register_action('modify_context', function($args, Context $context) {
 3    $context['custom_value'] = 'modified';
 4    // This change is lost after the action completes
 5});
 6
 7// ✅ Correct - use external state or return values
 8Rules::register_action('store_value', function($args, Context $context) {
 9    update_option('custom_value', 'modified');
10});
Warning: Context is passed by value to actions. Modifications to $context within an action do not persist to subsequent actions.

2. Assuming Action Order Across Rules#Copied!

 1// ❌ Wrong - different rules, no guaranteed order
 2Rules::create('rule1')->order(10)->when()->then()->custom('action1')->register();
 3Rules::create('rule2')->order(20)->when()->then()->custom('action2')->register();
 4// action1 and action2 only execute if their respective rule conditions match
 5
 6// ✅ Correct - actions in same rule execute in order
 7Rules::create('rule')->order(10)
 8    ->when()->request_url('*')
 9    ->then()
10        ->custom('action1')  // Executes first
11        ->custom('action2')  // Executes second
12    ->register();

3. Using Exit/Die in Actions#Copied!

 1// ❌ Wrong - prevents subsequent actions
 2Rules::register_action('early_exit', function($args, Context $context) {
 3    echo 'Response';
 4    exit; // Stops all subsequent actions!
 5});
 6
 7// ✅ Correct - use flags or return early
 8Rules::register_action('conditional_processing', function($args, Context $context) {
 9    if (!some_condition()) {
10        return; // Skip this action, continue to next
11    }
12
13    // Process normally
14});

Next Steps#Copied!


Ready to create custom actions? Continue to Creating Custom Actions for advanced techniques and patterns.