Creating Custom Actions

Custom actions define the "then" and implement the actual business logic when rules match. This guide covers registering and using custom actions.

Quick Start#Copied!

 1use MilliRulesRules;
 2use MilliRulesContext;
 3
 4// 1. Register reusable action
 5Rules::register_action('alert_slack', function ($config, Context $context) {
 6    // Reusable logic receiving config + context
 7    error_log("Slack Alert [{$config['channel']}]: " . ($config['message'] ?? ''));
 8});
 9
10// 2. Create Rule
11Rules::create('monitor_admin')
12    ->when()
13        ->request_url('/wp-admin/*')
14        ->user_role('editor')
15    ->then()
16        // Option A: Inline custom action without registration
17        ->custom('log_ip', function (Context $context) {
18            error_log("Access from IP: " . $context->get('request.ip'));
19        })
20
21        // Option B: Call registered action via magic method
22        ->alert_slack(['channel' => '#security', 'message' => 'Editor in admin area'])
23        
24        // Option C: Call registered action via custom()
25        ->custom('alert_slack', ['channel' => '#security', 'message' => '{user.login} accessed admin area'])
26    ->register();

Registering Actions#Copied!

Choosing the Right Registration Method#Copied!

MilliRules offers four ways to define custom actions. Choose based on your needs:

Method Best For Reusable?
Inline with ->custom() One-off actions ❌ No
Callback Registration Reusable simple actions ✅ Yes
Namespace Registration Multiple action classes ✅ Yes
Manual Wrapper Advanced use cases ✅ Yes

Recommendation: Start with inline ->custom() for one-off actions. Use callback registration for reusable simple actions. Use namespace registration for complex actions with placeholders.


Method 1: Inline with ->custom() (Simplest - One-Off Actions)#Copied!

Best for: Quick one-off actions that are only used in a single rule.

Define the action directly in the rule using a callback:

 1use MilliRulesRules;
 2use MilliRulesContext;
 3
 4Rules::create('log_important_access')
 5    ->when()->request_url('/important/*')
 6    ->then()
 7        ->custom('log_access', function(Context $context) {
 8            // One-off action logic right here
 9            error_log('Important page accessed: ' . $context->get('request.url'));
10        })
11    ->register();

Note: Inline callbacks receive only the Context parameter (not $args), since arguments are redundant for inline-defined actions. To access context data, use $context->get('key').

Example with context access:

 1Rules::create('send_notification')
 2    ->when()->user_role('administrator')
 3    ->then()
 4        ->custom('notify', function(Context $context) {
 5            $user = $context->get('user.login');
 6            $url = $context->get('request.url');
 7
 8            wp_mail(
 9                '',
10                'Admin Login',
11                "User {$user} accessed {$url}"
12            );
13        })
14    ->register();

Pros:

  • ✅ Very simple - no separate registration step
  • ✅ Perfect for one-off actions
  • ✅ Quick to write and test
  • ✅ Clean signature - only receives Context

Cons:

  • ❌ Not reusable across multiple rules
  • ❌ No placeholder support (e.g. {user.login})
  • ❌ Harder to test in isolation

Method 2: Callback Registration (Reusable Simple Actions)#Copied!

Best for: Reusable actions across multiple rules, simple logic without placeholders.

Register once, use everywhere:

 1use MilliRulesRules;
 2use MilliRulesContext;
 3
 4// Register once at plugin initialization
 5Rules::register_action('send_email', function($args, Context $context) {
 6    $to = $args['to'] ?? '';
 7    $subject = $args['subject'] ?? 'Notification';
 8    $message = $args['message'] ?? '';
 9
10    if (!$to) {
11        error_log('send_email: missing recipient');
12        return;
13    }
14
15    wp_mail($to, $subject, $message);
16});
17
18// Simple logging action
19Rules::register_action('log_message', function($args, Context $context) {
20    $message = $args['message'] ?? $args[0] ?? '';
21    error_log($message);
22});

Then use in any rule:

 1Rules::create('log_important_requests')
 2    ->when()->request_url('/important/*')
 3    ->then()
 4        ->log_message(['message' => 'Important page accessed'])
 5        ->send_email(['to' => '', 'subject' => 'Important Access'])
 6    ->register();

Pros:

  • ✅ Reusable across all rules
  • ✅ Simple to register and use
  • ✅ Good for basic actions

Cons:

  • ❌ No placeholder support (must implement manually)
  • ❌ Harder to organize many actions

Method 3: Namespace Registration (Best for Classes)#Copied!

Register an entire namespace once and all action classes are auto-discovered:

 1use MilliRulesRules;
 2
 3// One-time registration at plugin initialization
 4Rules::register_namespace('Actions', 'MyPluginActions');

Create your action class:

 1namespace MyPluginActions;
 2
 3use MilliRulesActionsBaseAction;
 4use MilliRulesContext;
 5
 6class NotifyMail extends BaseAction
 7{
 8    public function execute($config, Context $context): void
 9    {
10        // Access arguments via $this->args
11        $to = $this->args['to'] ?? '';
12        $message = $this->args['message'] ?? '';
13
14        // Resolve placeholders like {user.login}
15        $to = $this->resolve_value($to);
16        $message = $this->resolve_value($message);
17
18        wp_mail($to, 'Notification', $message);
19    }
20
21    public function get_type(): string
22    {
23        return 'notify_mail';  // Used for auto-discovery
24    }
25}

How it works:

  • The class name NotifyMail is converted to notify_mail
  • MilliRules finds the class automatically via get_type()
  • No need to manually register each action
  • Supports placeholder resolution via resolve_value()
  • Access arguments via $this->args (numeric and named keys)
  • Access action type via $this->type

Usage:

 1Rules::create('notify_on_login')
 2    ->when()->is_user_logged_in()
 3    ->then()
 4        // Both calling styles work identically
 5        ->notify_mail(['to' => '', 'message' => 'User {user.login} logged in'])
 6        // OR
 7        ->custom('notify_mail', ['to' => '', 'message' => 'User {user.login} logged in'])
 8    ->register();

Accessing Action Arguments#Copied!

When creating custom action classes (using namespace or manual registration), you need to access the arguments passed to the action. MilliRules provides a fluent get_arg() API for type-safe argument access with automatic placeholder resolution.

The get_arg() Method#Copied!

Access action arguments using the get_arg() method in your action classes:

 1namespace MyPluginActions;
 2
 3use MilliRulesActionsBaseAction;
 4use MilliRulesContext;
 5
 6class SendEmail extends BaseAction
 7{
 8    public function execute(Context $context): void
 9    {
10        // Clean type-safe access with automatic placeholder resolution
11        $to = $this->get_arg('to', '')->string();
12        $subject = $this->get_arg('subject', 'Notification')->string();
13        $html = $this->get_arg('html', false)->bool();
14        $priority = $this->get_arg('priority', 10)->int();
15
16        wp_mail($to, $subject, 'Message content');
17    }
18
19    public function get_type(): string
20    {
21        return 'send_email';
22    }
23}

Type Conversion Methods#Copied!

The get_arg() method returns an ArgumentValue object that provides fluent type conversion:

Method Returns Default for null
->string() string '' (empty string)
->bool() bool false
->int() int 0
->float() float 0.0
->array() array [] (empty array)
->raw() mixed null

Automatic Placeholder Resolution#Copied!

Placeholders like {user.email} are automatically resolved when you call any type method:

 1// Rule definition
 2Rules::create('welcome_email')
 3    ->when()->is_user_logged_in()
 4    ->then()
 5        ->send_email([
 6            'to' => '{user.email}',
 7            'subject' => 'Welcome {user.login}!'
 8        ])
 9    ->register();
10
11// In your SendEmail action class
12$to = $this->get_arg('to')->string();
13// Result: '' (placeholder automatically resolved)
14
15$subject = $this->get_arg('subject')->string();
16// Result: 'Welcome john!' (placeholder automatically resolved)

Positional Arguments#Copied!

Works with both named and positional arguments:

 1class LogMessage extends BaseAction
 2{
 3    public function execute(Context $context): void
 4    {
 5        // Called via: ->logMessage('ERROR', 'Something broke', 3)
 6        $level = $this->get_arg(0, 'info')->string();
 7        $message = $this->get_arg(1, 'No message')->string();
 8        $priority = $this->get_arg(2, 1)->int();
 9
10        error_log("[{$level}] {$message} (priority: {$priority})");
11    }
12
13    public function get_type(): string
14    {
15        return 'log_message';
16    }
17}

Method 4: Manual Wrapper (Advanced - Rarely Needed)#Copied!

Only use when: Namespace registration isn't suitable (dynamic class names, runtime actions, etc.)

 1use MilliRulesRules;
 2
 3// ⚠️ Avoid this if possible - creates type duplication
 4Rules::register_action('notify', function($args, Context $context) {
 5    $action = new MyPluginActionsNotifyMail($args, $context);
 6    $action->execute($context);
 7});

Why to avoid:

  • Type specified twice (in registration AND in get_type())
  • More verbose than namespace registration
  • Harder to maintain

When it's necessary:

  • You can't register the entire namespace
  • You need to pass custom dependencies to the constructor
  • You need conditional registration logic

Using Registered Actions#Copied!

Once registered, actions can be used via dynamic method calls:

 1Rules::create('notify_admin')
 2    ->when()->request_url('/important/*')
 3    ->then()
 4        ->send_email(['to' => '', 'subject' => 'Alert'])   // Dynamic method
 5        ->log_message(['message' => 'Important page accessed'])             // Dynamic method
 6    ->register();

Argument Patterns#Copied!

Both ->action_name() and ->custom() accept identical argument formats:

Named parameters (recommended):

 1->send_email(['to' => '', 'subject' => 'Alert'])
 2->custom('send_email', ['to' => '', 'subject' => 'Alert'])
 3// Both result in: $this->args['to'], $this->args['subject']

Positional array:

 1->send_email(['', 'Alert', 'Body'])
 2->custom('send_email', ['', 'Alert', 'Body'])
 3// Both result in: $this->args[0], $this->args[1], $this->args[2]

Single value:

 1->log_message('Important event occurred')
 2->custom('log_message', 'Important event occurred')
 3// Both result in: $this->args[0]

When to use which:

  • Dynamic methods: Shorter syntax when method name is the action type
  • ->custom(): When action type needs to be dynamic or passed as variable

Best Practices#Copied!

1. Keep Actions Focused#Copied!

 1// ✅ Good - single responsibility
 2Rules::register_action('log_event', function($args, Context $context) {
 3    error_log($args[0] ?? '');
 4});
 5
 6// ❌ Bad - too many responsibilities
 7Rules::register_action('do_everything', function($args, Context $context) {
 8    // Logs, sends email, updates database, clears cache...
 9});

2. Validate Configuration#Copied!

 1Rules::register_action('send_email', function($args, Context $context) {
 2    if (!isset($args['to']) || !is_email($args['to'])) {
 3        error_log('send_email: invalid recipient');
 4        return;
 5    }
 6
 7    wp_mail($args['to'], $args['subject'] ?? '', $args['message'] ?? '');
 8});

3. Handle Errors Gracefully#Copied!

 1Rules::register_action('api_call', function($args, Context $context) {
 2    try {
 3        $response = wp_remote_post($args['url'] ?? '', [
 4            'body' => $args['data'] ?? []
 5        ]);
 6
 7        if (is_wp_error($response)) {
 8            error_log('API call failed: ' . $response->get_error_message());
 9            return;
10        }
11    } catch (Exception $e) {
12        error_log('API call exception: ' . $e->getMessage());
13    }
14});

4. Use Type Hints#Copied!

 1use MilliRulesContext;
 2
 3Rules::register_action('my_action', function(array $args, Context $context): void {
 4    // Full IDE autocomplete and type safety
 5    $user = $context->get('user.login');
 6});

Common Pitfalls#Copied!

Don't Modify Context Expecting Persistence#Copied!

 1// ❌ Wrong - context changes don't persist between rules
 2Rules::register_action('bad_action', function($args, Context $context) {
 3    $context->set('custom_value', 'modified');
 4    // This change is lost after the action completes!
 5});
 6
 7// ✅ Correct - use external state
 8Rules::register_action('good_action', function($args, Context $context) {
 9    update_option('custom_value', 'modified');
10    // Or use globals, database, cache, etc.
11});

Don't Perform Heavy Operations Without Caching#Copied!

 1// ❌ Bad - runs on every execution
 2Rules::register_action('slow_action', function($args, Context $context) {
 3    $data = expensive_api_call();
 4    process_data($data);
 5});
 6
 7// ✅ Good - cache expensive operations
 8Rules::register_action('cached_action', function($args, Context $context) {
 9    $data = get_transient('cached_data');
10    if (false === $data) {
11        $data = expensive_api_call();
12        set_transient('cached_data', $data, HOUR_IN_SECONDS);
13    }
14    process_data($data);
15});

Next Steps#Copied!