WordPress Integration Guide

MilliRules integrates seamlessly with WordPress, providing powerful rule-based logic for plugins, themes, and WordPress applications. This guide covers WordPress-specific features, hooks, patterns, and best practices.

WordPress Package Overview#Copied!

The WordPress package extends MilliRules with WordPress-specific functionality:

  • WordPress Conditions - Post types, user roles, query flags, etc.
  • Hook Integration - Automatic WordPress hook registration
  • WordPress Context - Post, user, and query data
  • Template Integration - Content filtering and modification

##WordPress Initialization

Basic Initialization#Copied!

 1/**
 2 * Plugin Name: My MilliRules Plugin
 3 * Description: Custom rules for WordPress
 4 */
 5
 6require_once __DIR__ . '/vendor/autoload.php';
 7
 8use MilliRulesMilliRules;
 9use MilliRulesRules;
10use MilliRulesContext;
11
12add_action('init', function() {
13    // Initialize MilliRules (auto-loads WordPress package)
14    MilliRules::init();
15
16    // Register your rules here
17    register_custom_rules();
18}, 1); // Priority 1 for early initialization

Theme Integration#Copied!

 1// functions.php
 2
 3require_once get_template_directory() . '/vendor/autoload.php';
 4
 5use MilliRulesMilliRules;
 6use MilliRulesRules;
 7use MilliRulesContext;
 8
 9add_action('after_setup_theme', function() {
10    MilliRules::init();
11
12    // Theme-specific rules
13    require_once get_template_directory() . '/rules/content-rules.php';
14    require_once get_template_directory() . '/rules/layout-rules.php';
15}, 1);

WordPress Hooks#Copied!

WordPress rules can execute on specific hooks.

Common Hooks#Copied!

Initialization Hooks

 1// plugins_loaded - Very early, plugins just loaded
 2Rules::create('early_setup')
 3    ->on('plugins_loaded', 10)
 4    ->when()->constant('WP_DEBUG', true)
 5    ->then()->custom('enable_debug_features')
 6    ->register();
 7
 8// init - Standard initialization
 9Rules::create('standard_setup')
10    ->on('init', 10)
11    ->when()->is_user_logged_in()
12    ->then()->custom('setup_user_features')
13    ->register();
14
15// wp_loaded - WordPress fully loaded
16Rules::create('late_setup')
17    ->on('wp_loaded', 10)
18    ->when()->constant('DOING_AJAX', true)
19    ->then()->custom('setup_ajax_handlers')
20    ->register();

Frontend Hooks

 1// wp - Main query has been executed
 2Rules::create('after_query')
 3    ->on('wp', 10)
 4    ->when()->is_singular('post')
 5    ->then()->custom('track_post_view')
 6    ->register();
 7
 8// template_redirect - Before template is loaded
 9Rules::create('before_template')
10    ->on('template_redirect', 10)
11    ->when()
12        ->request_param('preview', 'true')
13        ->is_user_logged_in(false)
14    ->then()
15        ->custom('redirect_to_login')
16    ->register();
17
18// wp_enqueue_scripts - Enqueue frontend assets
19Rules::create('conditional_assets')
20    ->on('wp_enqueue_scripts', 10)
21    ->when()->is_singular(['post', 'page'])
22    ->then()->custom('enqueue_reading_mode_scripts')
23    ->register();

Admin Hooks

 1// admin_init - Admin initialization
 2Rules::create('admin_setup')
 3    ->on('admin_init', 10)
 4    ->when()->is_user_logged_in()
 5    ->then()->custom('setup_admin_features')
 6    ->register();
 7
 8// admin_menu - Add admin menu items
 9Rules::create('conditional_menu')
10    ->on('admin_menu', 10)
11    ->when()
12        ->is_user_logged_in()
13        ->custom('user_has_permission', ['permission' => 'manage_settings'])
14    ->then()
15        ->custom('add_settings_menu')
16    ->register();
17
18// admin_notices - Display admin notices
19Rules::create('warning_notice')
20    ->on('admin_notices', 10)
21    ->when()
22        ->constant('WP_DEBUG', true)
23        ->constant('WP_ENVIRONMENT_TYPE', 'production')
24    ->then()
25        ->custom('show_debug_warning')
26    ->register();

Content Hooks

 1// the_content - Filter post content
 2Rules::create('add_content_disclaimer')
 3    ->on('the_content', 10)
 4    ->when()
 5        ->is_singular('post')
 6        ->post_type('product')
 7    ->then()
 8        ->custom('prepend_disclaimer')
 9    ->register();
10
11// the_title - Filter post title
12Rules::create('modify_title')
13    ->on('the_title', 10)
14    ->when()
15        ->is_singular('post')
16        ->custom('is_featured_post')
17    ->then()
18        ->custom('add_featured_badge_to_title')
19    ->register();

Save Hooks

 1// save_post - After post is saved
 2Rules::create('post_save_notification')
 3    ->on('save_post', 10)
 4    ->when()
 5        ->post_type('post')
 6        ->custom('post_status_changed_to_published')
 7    ->then()
 8        ->custom('send_publication_notification')
 9    ->register();
10
11// wp_insert_post - When post is created/updated
12Rules::create('track_post_creation')
13    ->on('wp_insert_post', 10)
14    ->when()->post_type(['post', 'page'])
15    ->then()->custom('log_post_creation')
16    ->register();

Accessing Hook Arguments#Copied!

Many WordPress hooks pass arguments to their callbacks. MilliRules automatically captures these arguments and makes them available in the execution context under $context['wp']['hook'].

Hook Context Structure#Copied!

When a WordPress hook fires with arguments, they're added to the context:

 1$context = [
 2    'request' => [...],
 3    'wp' => [
 4        'post' => [...],
 5        'user' => [...],
 6        'query' => [...],            // Query variables (post_type, paged, s, etc.)
 7        'constants' => [...],
 8        'hook' => [
 9            'name' => 'save_post',      // The hook name
10            'args' => [                  // Array of hook arguments
11                0 => 123,                // First argument (post ID)
12                1 => WP_Post{...},       // Second argument (post object)
13                2 => true,               // Third argument (update flag)
14            ],
15        ],
16    ],
17];
Note: The 'query' context contains WordPress query variables from $wp_query->query_vars. For query conditionals (is_singular, is_home, etc.), use the dedicated is_* condition methods instead of checking context values.

Common Hook Signatures#Copied!

Different WordPress hooks pass different arguments:

save_post Hook

 1// WordPress signature: do_action('save_post', $post_id, $post, $update)
 2Rules::create('handle_post_save')
 3    ->on('save_post')
 4    ->when()->post_type('post')
 5    ->then()->custom('process_save', function(Context $context) {
 6        $post_id = $context['wp']['hook']['args'][0] ?? null;
 7        $post    = $context['wp']['hook']['args'][1] ?? null;
 8        $update  = $context['wp']['hook']['args'][2] ?? false;
 9
10        if ($update) {
11            error_log("Updated post: {$post->post_title} (ID: {$post_id})");
12        } else {
13            error_log("Created new post: {$post->post_title} (ID: {$post_id})");
14        }
15    })
16    ->register();

comment_post Hook

 1// WordPress signature: do_action('comment_post', $comment_id, $approved)
 2Rules::create('new_comment_notification')
 3    ->on('comment_post')
 4    ->then()->custom('notify_admin', function(Context $context) {
 5        $comment_id = $context['wp']['hook']['args'][0] ?? null;
 6        $approved   = $context['wp']['hook']['args'][1] ?? 0;
 7
 8        if ($approved === 1) {
 9            wp_mail(
10                get_option('admin_email'),
11                'New Comment Approved',
12                "Comment ID: {$comment_id}"
13            );
14        }
15    })
16    ->register();

transition_post_status Hook

 1// WordPress signature: do_action('transition_post_status', $new_status, $old_status, $post)
 2Rules::create('publish_notification')
 3    ->on('transition_post_status')
 4    ->when()->custom('status_changed_to_publish', function(Context $context) {
 5        $new_status = $context['wp']['hook']['args'][0] ?? '';
 6        $old_status = $context['wp']['hook']['args'][1] ?? '';
 7
 8        return $new_status === 'publish' && $old_status !== 'publish';
 9    })
10    ->then()->custom('send_notification', function(Context $context) {
11        $post = $context['wp']['hook']['args'][2] ?? null;
12
13        if ($post) {
14            error_log("Post published: {$post->post_title}");
15        }
16    })
17    ->register();

Using Hook Arguments in Conditions or Actions#Copied!

Create reusable conditions or actions that access hook arguments:

 1// Register a condition that checks post ID range
 2Rules::register_condition('post_id_in_range', function($args, Context $context) {
 3    $post_id = $context['wp']['hook']['args'][0] ?? 0;
 4    $min = $args['min'] ?? 0;
 5    $max = $args['max'] ?? PHP_INT_MAX;
 6
 7    return $post_id >= $min && $post_id <= $max;
 8});
 9
10// Use the condition
11Rules::create('process_specific_posts')
12    ->on('save_post')
13    ->when()->custom('post_id_in_range', ['min' => 100, 'max' => 200])
14    ->then()->custom('special_processing')
15    ->register();

Best Practices for Hook Arguments#Copied!

1. Always Provide Defaults

 1// ✅ Good - provides defaults for missing arguments
 2$post_id = $context['wp']['hook']['args'][0] ?? null;
 3$post    = $context['wp']['hook']['args'][1] ?? null;
 4
 5if (!$post_id || !$post) {
 6    return; // Safely handle missing data
 7}
 8
 9// ❌ Bad - assumes arguments exist
10$post_id = $context['wp']['hook']['args'][0];
11$post    = $context['wp']['hook']['args'][1];

2. Check Hook Name When Arguments Are Context-Specific

 1Rules::register_condition('is_post_being_published', function($args, Context $context) {
 2    $hook_name = $context['wp']['hook']['name'] ?? '';
 3
 4    // Different hooks have different argument structures
 5    if ($hook_name === 'transition_post_status') {
 6        $new_status = $context['wp']['hook']['args'][0] ?? '';
 7        return $new_status === 'publish';
 8    }
 9
10    if ($hook_name === 'save_post') {
11        $post = $context['wp']['hook']['args'][1] ?? null;
12        return $post && $post->post_status === 'publish';
13    }
14
15    return false;
16});

Hooks Without Arguments#Copied!

Hooks that don't pass arguments (like init, template_redirect, wp_loaded) will not have a hook key in the context:

 1Rules::create('init_hook')
 2    ->on('init')
 3    ->then()->custom('check_hook', function(Context $context) {
 4        if (isset($context['wp']['hook'])) {
 5            // This won't execute for 'init' hook
 6            error_log('Hook has arguments');
 7        } else {
 8            // This will execute
 9            error_log('Hook has no arguments');
10        }
11    })
12    ->register();

WordPress Conditions#Copied!

The WordPress package provides conditions for WordPress-specific scenarios.

User Conditions#Copied!

 1// Check if user is logged in
 2Rules::create('authenticated_only')
 3    ->when()->is_user_logged_in()
 4    ->then()->custom('show_dashboard')
 5    ->register();
 6
 7// Check user roles (custom condition)
 8Rules::register_condition('user_has_role', function($args, Context $context) {
 9    $required_role = $args['role'] ?? '';
10    $user_roles = $context['wp']['user']['roles'] ?? [];
11
12    return in_array($required_role, $user_roles);
13});
14
15Rules::create('admin_only')
16    ->when()
17        ->is_user_logged_in()
18        ->custom('user_has_role', ['role' => 'administrator'])
19    ->then()
20        ->custom('show_admin_tools')
21    ->register();

Query Conditions#Copied!

 1// Singular posts/pages
 2Rules::create('single_post_layout')
 3    ->when()->is_singular('post')
 4    ->then()->custom('apply_single_post_layout')
 5    ->register();
 6
 7// Home page
 8Rules::create('homepage_features')
 9    ->when()->is_home()
10    ->then()->custom('load_homepage_features')
11    ->register();
12
13// Archives
14Rules::create('archive_sidebar')
15    ->when()->is_archive()
16    ->then()->custom('show_archive_sidebar')
17    ->register();
18
19// Multiple post types
20Rules::create('content_enhancement')
21    ->when()->is_singular(['post', 'page', 'article'], 'IN')
22    ->then()->custom('enhance_content_display')
23    ->register();

Post Conditions#Copied!

 1// Check post type
 2Rules::create('product_features')
 3    ->when()->post_type('product')
 4    ->then()->custom('enable_product_features')
 5    ->register();
 6
 7// Custom post status check
 8Rules::register_condition('post_status', function($args, Context $context) {
 9    $expected = $args['value'] ?? '';
10    $actual = $context['wp']['post']['post_status'] ?? '';
11
12    return $actual === $expected;
13});
14
15Rules::create('draft_warning')
16    ->when()
17        ->post_type('post')
18        ->custom('post_status', ['value' => 'draft'])
19    ->then()
20        ->custom('show_draft_warning')
21    ->register();

WordPress Actions#Copied!

Create WordPress-specific actions for common operations.

Content Modification#Copied!

 1Rules::register_action('prepend_to_content', function($args, Context $context) {
 2    $text = $args['text'] ?? '';
 3    $priority = $args['priority'] ?? 10;
 4
 5    add_filter('the_content', function($content) use ($text) {
 6        return $text . $content;
 7    }, $priority);
 8});
 9
10Rules::create('add_reading_time')
11    ->when()->is_singular('post')
12    ->then()
13        ->custom('prepend_to_content', [
14            'text' => '<div class="reading-time">5 min read</div>',
15            'priority' => 10
16        ])
17    ->register();
 1Rules::register_action('add_menu_item', function($args, Context $context) {
 2    $menu_slug = $args['menu_slug'] ?? '';
 3    $title = $args['title'] ?? '';
 4    $capability = $args['capability'] ?? 'read';
 5    $url = $args['url'] ?? '#';
 6
 7    add_menu_page($title, $title, $capability, $menu_slug, function() use ($url) {
 8        wp_redirect($url);
 9        exit;
10    });
11});
12
13Rules::create('add_tools_menu')
14    ->on('admin_menu', 20)
15    ->when()->is_user_logged_in()
16    ->then()
17        ->custom('add_menu_item', [
18            'menu_slug' => 'custom-tools',
19            'title' => 'Custom Tools',
20            'capability' => 'manage_options',
21            'url' => admin_url('admin.php?page=custom-tools')
22        ])
23    ->register();

Widget Registration#Copied!

 1Rules::register_action('register_sidebar', function($args, Context $context) {
 2    $sidebar_config = wp_parse_args($config, [
 3        'name' => 'Custom Sidebar',
 4        'id' => 'custom-sidebar',
 5        'description' => 'A custom sidebar',
 6        'before_widget' => '<div class="widget">',
 7        'after_widget' => '</div>',
 8        'before_title' => '<h3>',
 9        'after_title' => '</h3>',
10    ]);
11
12    register_sidebar($sidebar_config);
13});
14
15Rules::create('conditional_sidebar')
16    ->on('widgets_init', 10)
17    ->when()->constant('ENABLE_CUSTOM_SIDEBAR', true)
18    ->then()
19        ->custom('register_sidebar', [
20            'name' => 'Product Sidebar',
21            'id' => 'product-sidebar'
22        ])
23    ->register();

User Meta Updates#Copied!

 1Rules::register_action('update_user_meta', function($args, Context $context) {
 2    $user_id = $context->get('user.id', 0) ?? 0;
 3    $meta_key = $args['key'] ?? '';
 4    $meta_value = $args['value'] ?? '';
 5
 6    if (!$user_id || !$meta_key) {
 7        return;
 8    }
 9
10    update_user_meta($user_id, $meta_key, $meta_value);
11});
12
13Rules::create('track_login_time')
14    ->on('wp_login', 10)
15    ->when()->is_user_logged_in()
16    ->then()
17        ->custom('update_user_meta', [
18            'key' => 'last_login',
19            'value' => time()
20        ])
21    ->register();

WooCommerce Integration#Copied!

WooCommerce Conditions#Copied!

 1Rules::register_condition('cart_total', function($args, Context $context) {
 2    if (!function_exists('WC')) {
 3        return false;
 4    }
 5
 6    $minimum = $args['minimum'] ?? 0;
 7    $cart_total = WC()->cart->get_total('');
 8
 9    return $cart_total >= $minimum;
10});
11
12Rules::register_condition('has_product_in_cart', function($args, Context $context) {
13    if (!function_exists('WC')) {
14        return false;
15    }
16
17    $product_id = $args['product_id'] ?? 0;
18
19    foreach (WC()->cart->get_cart() as $cart_item) {
20        if ($cart_item['product_id'] == $product_id) {
21            return true;
22        }
23    }
24
25    return false;
26});

WooCommerce Actions#Copied!

 1Rules::register_action('apply_coupon', function($args, Context $context) {
 2    if (!function_exists('WC')) {
 3        return;
 4    }
 5
 6    $coupon_code = $args['coupon'] ?? '';
 7
 8    if ($coupon_code && !WC()->cart->has_discount($coupon_code)) {
 9        WC()->cart->apply_coupon($coupon_code);
10    }
11});
12
13Rules::create('auto_apply_coupon')
14    ->when()
15        ->custom('cart_total', ['minimum' => 100])
16        ->is_user_logged_in()
17    ->then()
18        ->custom('apply_coupon', ['coupon' => 'LOYALTYDISCOUNT'])
19    ->register();

Plugin Integration Patterns#Copied!

Feature Flags#Copied!

 1// Enable/disable features based on rules
 2Rules::register_action('enable_feature', function($args, Context $context) {
 3    $feature = $args['feature'] ?? '';
 4
 5    if ($feature) {
 6        update_option("feature_enabled_{$feature}", true);
 7    }
 8});
 9
10Rules::create('enable_beta_features')
11    ->when()
12        ->is_user_logged_in()
13        ->custom('user_has_role', ['role' => 'administrator'])
14        ->constant('WP_ENVIRONMENT_TYPE', ['local', 'development'], 'IN')
15    ->then()
16        ->custom('enable_feature', ['feature' => 'beta_dashboard'])
17        ->custom('enable_feature', ['feature' => 'advanced_editor'])
18    ->register();

Access Control#Copied!

 1Rules::register_action('restrict_access', function($args, Context $context) {
 2    $message = $args['message'] ?? 'Access denied';
 3    $redirect = $args['redirect'] ?? home_url();
 4
 5    wp_die($message, 'Access Denied', [
 6        'link_url' => $redirect,
 7        'link_text' => 'Go back',
 8    ]);
 9});
10
11Rules::create('protect_admin_pages')
12    ->when()
13        ->request_url('/wp-admin/options-*.php')
14        ->is_user_logged_in()
15        ->custom('user_has_role', ['role' => 'administrator'])
16        ->match_none() // NOT administrator
17    ->then()
18        ->custom('restrict_access', [
19            'message' => 'Only administrators can access this page',
20            'redirect' => admin_url()
21        ])
22    ->register();

Conditional Plugin Loading#Copied!

 1// Conditionally load plugin features
 2add_action('plugins_loaded', function() {
 3    MilliRules::init();
 4
 5    Rules::register_action('load_plugin_module', function($args, Context $context) {
 6        $module = $args['module'] ?? '';
 7        $file = plugin_dir_path(__FILE__) . "modules/{$module}.php";
 8
 9        if (file_exists($file)) {
10            require_once $file;
11        }
12    });
13
14    Rules::create('load_api_module')
15        ->when()->request_url('/wp-json/myplugin/*')
16        ->then()->custom('load_plugin_module', ['module' => 'api'])
17        ->register();
18
19    Rules::create('load_admin_module')
20        ->when()->constant('WP_ADMIN', true)
21        ->then()->custom('load_plugin_module', ['module' => 'admin'])
22        ->register();
23}, 5);

Best Practices#Copied!

1. Hook Timing#Copied!

 1// ✅ Good - initialize early
 2add_action('init', function() {
 3    MilliRules::init();
 4    register_rules();
 5}, 1); // Early priority
 6
 7// ❌ Bad - too late, hooks may have fired
 8add_action('wp_footer', function() {
 9    MilliRules::init(); // Too late!
10    register_rules();
11});

2. WordPress Function Availability#Copied!

 1// ✅ Good - checks function availability
 2Rules::register_condition('wp_safe_condition', function($args, Context $context) {
 3    if (!function_exists('get_current_user_id')) {
 4        return false;
 5    }
 6
 7    $user_id = get_current_user_id();
 8    return $user_id > 0;
 9});
10
11// ❌ Bad - assumes WordPress is loaded
12Rules::register_condition('unsafe_condition', function($args, Context $context) {
13    $user_id = get_current_user_id(); // May not exist!
14    return $user_id > 0;
15});

3. Multisite Compatibility#Copied!

 1Rules::register_condition('is_main_site', function($args, Context $context) {
 2    if (!is_multisite()) {
 3        return true; // Not multisite, always main site
 4    }
 5
 6    return is_main_site();
 7});
 8
 9Rules::create('main_site_only_feature')
10    ->when()->custom('is_main_site')
11    ->then()->custom('enable_network_feature')
12    ->register();

4. Translation Ready#Copied!

 1Rules::register_action('show_message', function($args, Context $context) {
 2    $message = $args['message'] ?? '';
 3
 4    // Make translatable
 5    $translated = __($message, 'my-text-domain');
 6
 7    echo '<div class="notice">' . esc_html($translated) . '</div>';
 8});

Troubleshooting#Copied!

Rules Not Executing in WordPress#Copied!

Check initialization timing:

 1// Verify MilliRules is initialized
 2add_action('init', function() {
 3    if (!class_exists('MilliRulesMilliRules')) {
 4        error_log('MilliRules not loaded!');
 5        return;
 6    }
 7
 8    MilliRules::init();
 9    error_log('MilliRules initialized');
10}, 1);

Verify package loading:

 1$packages = MilliRules::get_loaded_packages();
 2error_log('Loaded packages: ' . implode(', ', $packages));
 3
 4if (!in_array('WP', $packages)) {
 5    error_log('WordPress package not loaded!');
 6}

Hook Conflicts#Copied!

 1// Check if hook has fired
 2add_action('init', function() {
 3    error_log('Init hook fired');
 4    MilliRules::init();
 5
 6    Rules::create('test_rule')
 7        ->on('template_redirect', 10)
 8        ->when()->request_url('*')
 9        ->then()->custom('log', ['value' => 'Template redirect fired'])
10        ->register();
11}, 1);
12
13add_action('template_redirect', function() {
14    error_log('template_redirect fired directly');
15}, 1);

Next Steps#Copied!


Ready for complete examples? Continue to Real-World Examples to see full WordPress implementations and use cases.