Dynamic Placeholders

Placeholders allow you to inject dynamic runtime values into your rules. Instead of hardcoding values, you can reference contextual data using a simple colon-separated syntax that gets resolved during rule execution.

What Are Placeholders?#Copied!

Placeholders are special tokens enclosed in curly braces that get replaced with actual values from the execution context:

 1// Static value
 2'value' => 'Fixed string'
 3
 4// Dynamic placeholder
 5'value' => '{request.uri}'         // Current URL
 6'value' => '{request.method}'      // HTTP method
 7'value' => '{user.login}'          // Current user's login
 8'value' => '{cookie.session_id}'   // Session cookie value

Placeholder Syntax#Copied!

The placeholder syntax uses colon-separated parts to navigate the context hierarchy:

{category:subcategory:key}
  • category - Top-level context category (request, user, post, cookie, etc.)
  • subcategory - Nested category (optional, can have multiple levels)
  • key - Specific value to retrieve

Examples#Copied!

 1'{request.uri}'              // $context['request']['uri']
 2'{request.method}'           // $context['request']['method']
 3'{request:headers:host}'     // $context['request']['headers']['host']
 4'{user.id}'                  // $context['user']['id']
 5'{post.title}'               // $context['post']['title']
 6'{cookie.session_id}'        // $context['cookie']['session_id']

Built-in Placeholder Categories#Copied!

Request Placeholders#Copied!

Access HTTP request data from the PHP package context.

Available Request Placeholders

Placeholder Description Example Value
{request.method} HTTP method GET, POST
{request.uri} Full request URI /wp-admin/edit.php
{request.scheme} URL scheme https
{request.host} Host name example.com
{request.path} URL path /wp-admin/edit.php
{request.query} Query string post_type=page
{request.referer} HTTP referer https://example.com/previous
{request.user_agent} User agent string Mozilla/5.0...
{request.ip} Client IP address 192.168.1.1

Request Headers

 1'{request:headers:content-type}'    // Content-Type header
 2'{request:headers:authorization}'   // Authorization header
 3'{request:headers:accept}'          // Accept header
 4'{request:headers:user-agent}'      // User-Agent header
Note: Header names in placeholders are case-insensitive: {request:headers:Content-Type} and {request:headers:content-type} are equivalent.

Examples

 1Rules::register_action('log_request', function($args, Context $context) {
 2    $message = $args['value'] ?? '';
 3    error_log($message);
 4});
 5
 6Rules::create('log_requests')
 7    ->when()->request_url('/api/*')
 8    ->then()
 9        ->custom('log_request', [
10            'value' => 'API request: {request.method} {request.uri} from {request.ip}'
11        ])
12    ->register();
13
14// Logs: "API request: GET /api/users from 192.168.1.1"

Access cookie values.

 1'{cookie.session_id}'        // $_COOKIE['session_id']
 2'{cookie.user_preference}'   // $_COOKIE['user_preference']
 3'{cookie.theme}'             // $_COOKIE['theme']

Examples

 1Rules::register_action('personalize', function($args, Context $context) {
 2    $theme = $args['theme'] ?? 'default';
 3    apply_theme($theme);
 4});
 5
 6Rules::create('apply_user_theme')
 7    ->when()->cookie('theme')
 8    ->then()
 9        ->custom('personalize', [
10            'theme' => '{cookie.theme}'  // Uses cookie value
11        ])
12    ->register();

Parameter Placeholders#Copied!

Access query and form parameters.

 1'{param.action}'         // $_GET['action'] or $_POST['action']
 2'{param.id}'            // $_GET['id'] or $_POST['id']
 3'{param.page}'          // $_GET['page'] or $_POST['page']

Examples

 1Rules::register_action('process_action', function($args, Context $context) {
 2    $action = $args['action'] ?? '';
 3    $id = $args['id'] ?? 0;
 4    error_log("Processing action: {$action} for ID: {$id}");
 5});
 6
 7Rules::create('process_request')
 8    ->when()
 9        ->request_param('action')
10        ->request_param('id')
11    ->then()
12        ->custom('process_action', [
13            'action' => '{param.action}',
14            'id' => '{param.id}'
15        ])
16    ->register();

WordPress Placeholders#Copied!

Access WordPress-specific data (available only when WordPress package is loaded).

User Placeholders

Placeholder Description Example Value
{user.id} User ID 123
{user.login} User login name john_doe
{user.email} User email
{user.display_name} Display name John Doe
{user.roles} User roles (array) administrator

Post Placeholders

Placeholder Description Example Value
{post.id} Post ID 456
{post.title} Post title My Blog Post
{post.type} Post type post, page
{post.status} Post status publish, draft
{post.author} Author ID 123

Query Variable Placeholders

Access WordPress query variables from $wp_query->query_vars:

Placeholder Description Example Value
{query.post_type} Current post type 'post', 'page'
{query.paged} Current page number 1, 2, 3
{query.s} Search query 'search term'
{query.m} Month/year archive '202312'
{query.cat} Category ID '5'
{query.tag} Tag slug 'news'
{query.author} Author ID or name '1'

Examples

 1use MilliRulesContext;
 2
 3Rules::register_action('log_user_action', function($args, Context $context) {
 4    error_log($args['message'] ?? '');
 5});
 6
 7Rules::create('log_search')
 8    ->when()
 9        ->request_url('/search')
10        ->is_search()
11    ->then()
12        ->custom('log_user_action', [
13            'message' => 'User {user.login} searched for "{query.s}" on {request.uri}'
14        ])
15    ->register();
16
17// Logs: "User john_doe searched for "wordpress plugins" on /somewhere"

Using Placeholders in Actions#Copied!

Placeholders are primarily used in action configurations to inject dynamic values.

Basic Usage#Copied!

 1Rules::register_action('send_notification', function($args, Context $context) {
 2    $to = $args['to'] ?? '';
 3    $subject = $args['subject'] ?? '';
 4    $message = $args['message'] ?? '';
 5
 6    // Placeholders already resolved by BaseAction
 7    wp_mail($to, $subject, $message);
 8});
 9
10Rules::create('notify_on_login')
11    ->when()
12        ->is_user_logged_in()
13        ->request_url('/wp-admin/*')
14    ->then()
15        ->custom('send_notification', [
16            'to' => '',
17            'subject' => 'User Login Alert',
18            'message' => 'User {user.login} logged in from {request.ip}'
19        ])
20    ->register();

Multiple Placeholders#Copied!

 1Rules::register_action('log_detailed', function($args, Context $context) {
 2    error_log($args['message'] ?? '');
 3});
 4
 5Rules::create('detailed_logging')
 6    ->when()->request_url('/api/*')
 7    ->then()
 8        ->custom('log_detailed', [
 9            'message' => '{request.method} request to {request.uri} from {request.ip} '
10                       . 'at {request.timestamp} by user {user.login}'
11        ])
12    ->register();

Nested Placeholders#Copied!

 1Rules::register_action('set_header', function($args, Context $context) {
 2    $name = $args['name'] ?? '';
 3    $value = $args['value'] ?? '';
 4
 5    if (!headers_sent()) {
 6        header("{$name}: {$value}");
 7    }
 8});
 9
10Rules::create('custom_header')
11    ->when()->request_url('/api/*')
12    ->then()
13        ->custom('set_header', [
14            'name' => 'X-Request-ID',
15            'value' => '{request:headers:x-request-id}'  // Forward header value
16        ])
17    ->register();

Implementing Placeholder Resolution#Copied!

In BaseAction Subclasses#Copied!

Actions extending BaseAction automatically get placeholder resolution:

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

In Callback Actions#Copied!

Callback actions need to manually resolve placeholders:

 1use MilliRulesPlaceholderResolver;
 2
 3Rules::register_action('manual_resolution', function($args, Context $context) {
 4    $resolver = new PlaceholderResolver($context);
 5
 6    // Resolve individual value
 7    $message = $resolver->resolve($args['message'] ?? '');
 8
 9    // Use resolved value
10    error_log($message);
11});
12
13Rules::create('use_manual_resolution')
14    ->when()->request_url('/test')
15    ->then()
16        ->custom('manual_resolution', [
17            'message' => 'Testing from {request.ip}'
18        ])
19    ->register();

Creating Custom Placeholder Resolvers#Copied!

Register custom placeholder categories for your own data sources.

Registering Custom Resolvers#Copied!

 1use MilliRulesRules;
 2
 3// Register custom placeholder category
 4Rules::register_placeholder('custom', function($context, $parts) {
 5    // $parts[0] is the first key after 'custom:'
 6    // $parts[1] is the second key, etc.
 7
 8    switch ($parts[0] ?? '') {
 9        case 'site_name':
10            return get_bloginfo('name');
11
12        case 'site_url':
13            return home_url();
14
15        case 'current_time':
16            return date('Y-m-d H:i:s');
17
18        case 'option':
19            return get_option($parts[1] ?? '');
20
21        default:
22            return '';
23    }
24});

Using Custom Placeholders#Copied!

 1Rules::register_action('log_custom', function($args, Context $context) {
 2    error_log($args['message'] ?? '');
 3});
 4
 5Rules::create('use_custom_placeholders')
 6    ->when()->request_url('/api/*')
 7    ->then()
 8        ->custom('log_custom', [
 9            'message' => 'Request to {custom.site_name} at {custom.current_time}'
10        ])
11    ->register();
12
13// Access WordPress options
14Rules::create('use_option_placeholder')
15    ->when()->request_url('/test')
16    ->then()
17        ->custom('log_custom', [
18            'message' => 'Site tagline: {custom:option:blogdescription}'
19        ])
20    ->register();

Complex Custom Resolvers#Copied!

 1Rules::register_placeholder('env', function($context, $parts) {
 2    $key = $parts[0] ?? '';
 3
 4    // Environment variables
 5    if ($key === 'var') {
 6        return getenv($parts[1] ?? '');
 7    }
 8
 9    // Server information
10    if ($key === 'server') {
11        return $_SERVER[strtoupper($parts[1] ?? '')] ?? '';
12    }
13
14    // Custom environment data
15    $env_data = [
16        'name' => WP_ENVIRONMENT_TYPE ?? 'production',
17        'debug' => WP_DEBUG ?? false,
18        'version' => get_bloginfo('version'),
19    ];
20
21    return $env_data[$key] ?? '';
22});
23
24// Usage:
25// {env.name}           → 'production'
26// {env.debug}          → true/false
27// {env:var:API_KEY}    → getenv('API_KEY')
28// {env:server:http_host} → $_SERVER['HTTP_HOST']

Advanced Placeholder Patterns#Copied!

Conditional Placeholders#Copied!

Use placeholders with fallback values:

 1Rules::register_action('log_with_fallback', function($args, Context $context) {
 2    $resolver = new PlaceholderResolver($context);
 3
 4    // Resolve with fallback
 5    $user = $resolver->resolve($args['user'] ?? '') ?: 'guest';
 6    $message = "User: {$user}";
 7
 8    error_log($message);
 9});
10
11Rules::create('log_with_defaults')
12    ->when()->request_url('*')
13    ->then()
14        ->custom('log_with_fallback', [
15            'user' => '{user.login}'  // Falls back to 'guest' if empty
16        ])
17    ->register();

Placeholder Transformation#Copied!

Transform placeholder values:

 1Rules::register_action('transform_placeholder', function($args, Context $context) {
 2    $resolver = new PlaceholderResolver($context);
 3    $value = $resolver->resolve($args['value'] ?? '');
 4
 5    // Transform resolved value
 6    $transformed = strtoupper($value);
 7    $transformed = sanitize_text_field($transformed);
 8
 9    error_log($transformed);
10});

Array Placeholders#Copied!

Access array values:

 1// Access first role
 2'{user:roles:0}'        // First role
 3
 4// Access header values
 5'{request:headers:accept}' // Accept header

Object Property Access#Copied!

Access public properties and magic properties on objects using dot notation:

 1// Access public object properties
 2'{hook:args:0:ID}'           // WP_Post object's ID property
 3'{hook:args:0:post_title}'   // WP_Post object's post_title property
 4'{hook:args:0:post_author}'  // WP_Post object's post_author property
 5
 6// Access magic properties (via __get() method)
 7'{hook:args:2:permalink}'    // WP_Post object's permalink (magic property)
 8
 9// Mixed array and object access
10'{hook:args:2:ID}'          // Third argument (index 2) → object's ID property

WordPress Hook Examples

WordPress hooks often pass objects as arguments. You can now access their properties directly:

 1use MilliRulesContext;
 2
 3// Example: transition_post_status hook passes (new_status, old_status, $post)
 4Rules::register_action('clear_post_cache', function($args, Context $context) {
 5    $url = $args['url'] ?? '';
 6    // Clear cache for the URL
 7    wp_cache_delete($url);
 8});
 9
10Rules::create('clear_on_publish')
11    ->when()
12        ->hook_is('transition_post_status')
13        ->hook_arg(0, '==', 'publish')  // New status is 'publish'
14    ->then()
15        ->custom('clear_post_cache', [
16            'url' => '{hook:args:2:permalink}'  // Access WP_Post's permalink property
17        ])
18    ->register();

Nested Objects and Arrays

Combine array and object access for complex data structures:

 1// WordPress comment object in an array
 2'{comments:0:comment_author}'       // First comment's author
 3'{comments:0:comment_content}'      // First comment's content
 4
 5// API response with nested objects
 6'{api:response:data:items:0:id}'    // First item's ID from API response
 7
 8// Custom data structures
 9'{data:user:profile:settings}'      // Access nested object properties

How It Works

When resolving placeholders, MilliRules automatically detects whether each segment is:

  • Array access: Uses isset() and $array[$key]
  • Object property access: Checks property_exists() for public properties, or __get() for magic properties

This allows seamless access to mixed array/object structures without special syntax.


Placeholder Resolution Flow#Copied!

Understanding how placeholders are resolved:

1. Action configuration contains placeholder: "{request.uri}"
   ↓
2. BaseAction::resolve_value() detects placeholder
   ↓
3. PlaceholderResolver splits by colons: ['request', 'uri']
   ↓
4. Looks up category 'request' in registered resolvers
   ↓
5. PHP package resolver handles 'request' category
   ↓
6. Returns $context['request']['uri']
   ↓
7. Placeholder replaced with actual value: "/api/users"
   ↓
8. Action executes with resolved value

Best Practices#Copied!

1. Use Descriptive Placeholder Names#Copied!

 1// ✅ Good - clear what data is being used
 2'message' => 'User {user.login} accessed {request.uri}'
 3
 4// ❌ Bad - unclear placeholders
 5'message' => 'User {u} accessed {r}'

2. Provide Fallback Values#Copied!

 1Rules::register_action('safe_action', function($args, Context $context) {
 2    $resolver = new PlaceholderResolver($context);
 3
 4    // Resolve with fallback
 5    $user = $resolver->resolve($args['user'] ?? '') ?: 'Unknown User';
 6    $ip = $resolver->resolve($args['ip'] ?? '') ?: '0.0.0.0';
 7
 8    error_log("User: {$user}, IP: {$ip}");
 9});

3. Validate Resolved Values#Copied!

 1Rules::register_action('validated_action', function($args, Context $context) {
 2    $resolver = new PlaceholderResolver($context);
 3    $email = $resolver->resolve($args['email'] ?? '');
 4
 5    // Validate resolved value
 6    if (!is_email($email)) {
 7        error_log('Invalid email from placeholder');
 8        return;
 9    }
10
11    // Use validated value
12    wp_mail($email, 'Subject', 'Message');
13});

4. Document Custom Placeholders#Copied!

 1/**
 2 * Custom Placeholder: {payment.gateway}
 3 * Returns the active payment gateway name
 4 *
 5 * Custom Placeholder: {payment:status:order_id}
 6 * Returns the payment status for a given order ID
 7 *
 8 * Example: {payment:status:123} → 'completed'
 9 */
10Rules::register_placeholder('payment', function($context, $parts) {
11    // Implementation...
12});

Common Pitfalls#Copied!

1. Missing Context Data#Copied!

 1// ❌ Wrong - WordPress placeholders in PHP-only context
 2Rules::create('php_rule', 'php')
 3    ->when()->request_url('/api/*')
 4    ->then()
 5        ->custom('action', [
 6            'value' => '{user.login}'  // Empty! WordPress not available
 7        ])
 8    ->register();
 9
10// ✅ Correct - check context availability
11Rules::register_action('safe_wp_action', function($args, Context $context) {
12    if (!isset($context['wp'])) {
13        error_log('WordPress context not available');
14        return;
15    }
16
17    $resolver = new PlaceholderResolver($context);
18    $user = $resolver->resolve('{user.login}');
19    // ...
20});

2. Incorrect Placeholder Syntax#Copied!

 1// ❌ Wrong - missing braces
 2'value' => 'request:uri'
 3
 4// ❌ Wrong - incorrect separator
 5'value' => '{request.uri}'
 6
 7// ✅ Correct - proper syntax
 8'value' => '{request.uri}'

3. Case Sensitivity#Copied!

 1// Context keys are case-sensitive
 2// ✅ Correct
 3'{request.uri}'
 4
 5// ❌ Wrong
 6'{Request:URI}'
 7'{REQUEST:URI}'

Next Steps#Copied!


Ready to extend MilliRules? Continue to Creating Custom Packages to learn how to add your own context data and placeholders.