I am messing around with WordPress, trying to get a good grasp of the thinking behind conventions used in WordPress. I came across a question, and my searches fell short of explaining what I was trying to figure out. I tried to look at the code for ‘wp-include/rewrite.php’, but I can’t seem to track down what’s going on for some reason. So I tried to black-box test it, and I am now confused.

When adding a custom route to WordPress, you can use add_rewrite_rule. But unlike add_action / remove_action and add_filter / remove_filter, there is no remove_rewrite_rule. Everything I have found online has said just use flush_rewrite_rules.

This ‘weirdness’ led to some experimentation with flush_rewrite_rules that in turn led to a concern that there is probably a better, ‘right’, way to do URL rewrites in a plugin. I will describe what I have done to confuse myself.

I created two plugins. The first ‘plugin-x’ adds a rewrite rule ‘^plugin-x/?$’ and then calls flush_rewrite_rules in the plugin activation hook. The second ‘plugin-y’ only calls flush_rewrite_rules in the activation hook. Both call flush_rewrite_rules in their deactivation hooks.

I wanted ‘plugin-y’ to interfere with ‘plugin-x’ by flushing the rules that plugin-x created, based on what I had seen up to this point. Activating ‘plugin-x’ put the rewrite rule in place, and activating ‘plugin-y’ removed the rule created by ‘plugin-x’.

The part that confuses me is that flush_rewrite_rules seems to be doing two different things. If it’s not called after add_rewrite_rule within the activation hook, then the route is never actually added to the database. Implying that flush_rewrite_rules is saving the existing rules before deletion and reload. But then how is flush_rewrite_rules called from ‘plugin-y’ deleting the rule created by ‘plugin-x’?

The only other explanation I can come up with is that add_rewrite_rules is deferring the insert into the database into the flush_rewrite_rules call, and marks it for removal on the next call to flush_rewrite_rules. I don’t think that is the case, but I am not sure how to explain it.

Since this is not how you are supposed to add a rewrite rule, I wanted to see if add_rewrite_rule called in the ‘init’ action would show up in the database. So I created a third plugin called ‘plugin-z’, that calls add_rewrite_rule in the ‘init’ action. The rewrite rule showed up in the database without the need to call flush_rewrite_rules, and was resilient to subsequent calls to flush_rewrite_rules.

So I have two questions

  1. What does flush_rewrite_rules actually do?
  2. Does calling add_rewrite_rule in the ‘init’ action result in an extra database call every page load?

plugin-x

<?php
/**
 * Plugin Name: plugin-x
 * Description: create a rewrite rule on activation, and remove it on deactivation.
 */

register_activation_hook(__FILE__, 'plugin_x_activation_hook');
register_deactivation_hook(__FILE__, 'plugin_x_deactivation_hook');

function plugin_x_activation_hook()
{
    add_rewrite_rule('^plugin-x/?$', 'index.php?plugin_x_custom_rewrite_rule=1', 'top');
    flush_rewrite_rules();
}

function plugin_x_deactivation_hook()
{
    flush_rewrite_rules();
}

/* necessary nonsense required to point the custom route at code within this plugin. */

// add custom query vars for rewrite rule to be associated with something custom
function plugin_x_query_vars($vars)
{
    $vars[] = 'plugin_x_custom_rewrite_rule';
    return $vars;
}
add_filter('query_vars', 'plugin_x_query_vars', 0);

// add a request parser to search for custom query var
function plugin_x_parse_request()
{
    /** @global WP $wp */
    global $wp;

    if(array_key_exists('plugin_x_custom_rewrite_rule', $wp->query_vars))
    {
        wp_die('plugin-x route active', 'plugin-x', array('response' => 200));
    }
}
add_action('parse_request', 'plugin_x_parse_request', 0);
?>

plugin-y

<?php
/**
* Plugin Name: plugin-y
* Description: call flush_rewrite_rules on activation and deactivation
*/

register_activation_hook(__FILE__, 'plugin_y_activation_hook');
register_deactivation_hook(__FILE__, 'plugin_y_deactivation_hook');

function plugin_y_activation_hook()
{
    flush_rewrite_rules();
}

function plugin_y_deactivation_hook()
{
    flush_rewrite_rules();
}
?>

plugin-z

<?php
/**
 * Plugin Name: plugin-z
 * Description: create a rewrite rule in the 'init' action, and remove it on deactivation.
 */

register_activation_hook(__FILE__, 'plugin_z_activation_hook');
register_deactivation_hook(__FILE__, 'plugin_z_deactivation_hook');

function plugin_z_activation_hook()
{
    // no-op
}

function plugin_z_deactivation_hook()
{
    flush_rewrite_rules();
}

function plugin_z_init()
{
    add_rewrite_rule('^plugin-z/?$', 'index.php?plugin_z_custom_rewrite_rule=1', 'top');
}
add_action('init', 'plugin_z_init');

/* necessary nonsense required to point the custom route at code within this plugin. */

// add custom query vars for rewrite rule to be associated with something custom
function plugin_z_query_vars($vars)
{
    $vars[] = 'plugin_z_custom_rewrite_rule';
    return $vars;
}
add_filter('query_vars', 'plugin_z_query_vars', 0);

// add a request parser to search for custom query var
function plugin_z_parse_request()
{
    /** @global WP $wp */
    global $wp;

    if(array_key_exists('plugin_z_custom_rewrite_rule', $wp->query_vars))
    {
        wp_die('plugin-z route active', 'plugin-z', array('response' => 200));
    }
}
add_action('parse_request', 'plugin_z_parse_request', 0);
?>

1 Answer
1

Flush rewrite rules deletes the option that stores the rewrite rules, then calls the method that regenerates rules and saves it back into the option. If this option is not empty, the rewrite rules will always be loaded from the option. This is because regenerating rules can be an expensive operation, so it should only be done when something changes.

Adding rules on init doesn’t result in extra database queries, it’s actually the correct way to add rewrite rules and the only way to be sure your rules will always be present. The cost of adding a rule is just some simple string and array manipulation code which adds your rule to the array stored in the rewrite class for the life of the request. If something else flushes rules and your rules aren’t added on init, they will disappear. If the option that stores rules is empty, the rules will come from this array when it’s repopulated.

Leave a Reply

Your email address will not be published. Required fields are marked *