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
NotifyMailis converted tonotify_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!
- Custom Conditions - Create conditional logic
- Built-in Actions Reference - See available actions
- Placeholder System - Dynamic value resolution
- API Reference - Complete API documentation