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];
$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();
Navigation Menu Modification#Copied!
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!
- API Reference - Complete method documentation
- Real-World Examples - WordPress integration examples
- Advanced Patterns - Advanced WordPress patterns
Ready for complete examples? Continue to Real-World Examples to see full WordPress implementations and use cases.