Making your plugin routines multisite-compatible

Making Your Plugin Routines Multisite Compatible

If you’ve been getting your way around with WordPress, you have probably heard of that thing called Multisite. Multiple web sites in one WordPress installation, that is. You may also call it a network of sites. If you haven’t actually used it, that’s another issue – maybe you have not (yet) come across a project where Multisite would have been the right fit. (In any case, I would encourage you to try it out on your dev environment then.)

This post is not about Multisite though. It’s about how you can make your regular plugin that you would like to write or might have written years ago compatible with Multisite. Because even if your plugin does not do anything related to Multisite in any way, there are some things to take care of, in particular you need to take care of your plugin’s activation / deactivation / uninstallation routines (if you have something like it in your plugin). Otherwise you are locking out some users from using your plugin, and you certainly don’t want that, I’m sure. Now that you have read this, please don’t run away, it’s not something you need to spend days for – it might only take a few minutes, and if you don’t have any of these routines, there actually is nothing else to do to make the plugin compatible (at least not for the scope of this tutorial). But now, let’s get started!

Setup your installation routine

First of all, note that “installation” here is synonymous with “activation”. As you might already know, when you want to install your plugin in WordPress, you use the activation routine for that.

You probably run your plugin’s installation/activation process inside a function which you have passed to register_activation_hook(). Let’s imagine your plugin needs a custom database table which needs to be setup when the plugin is activated for the first time. The code for it could look something like this:

This works perfectly fine on a regular site, but you are likely to run into problems on a Multisite. That is because on a Multisite it is possible to network activate a plugin, meaning it will be activated on all sites in the network at once.

So how do you as a plugin developer know when a plugin is being activated for an entire network? Luckily, there is parameter $network_wide which is automatically passed to your callback function (in the above case to myplugin_install() as the first (and only) parameter. This variable is a boolean and will be true if the plugin is being activated for the entire Multisite. What you need to do in your plugin now is check whether the parameter is true, and if so, not only run the above installation routine once, but for all sites in your network instead. To prepare the installation routine for that, let’s outsource the relevant parts that will need to be executed for every site into a separate function. In the below example, that function is called myplugin_install_single_site(). (Note that I left out the myplugin_register_table() function below.)

As you can see, all that has changed is that almost everything from the myplugin_install()  was moved to the new myplugin_install_single_site() function. The only exception is the myplugin_register_table() call because that is something that only needs to happen once. Be aware that from now on, the myplugin_install_single_site() will no longer be included in the following code snippets as that will not change.

Okay, we’ll get to the next code snippet straight away. In the following snippet, we make use of the $network_wide parameter to check whether the plugin is being activated as usual or for the entire Multisite. If it should be installed for the entire network, we will query all sites in the network, iterate through them (using the function switch_to_blog()) and call our myplugin_install_single_site() for each site. If it’s just a regular activation, we simply call myplugin_install_single_site once.

In the SQL query above you might be confused about reading the terms “Blog” and “Site” there. These are the old naming conventions for Multisite: If I had written this tutorial years ago, I wouldn’t have told you about “Networks of Sites”, but about “Sites of Blogs”. Yes, the old naming conventions probably don’t make any sense to you as well. That’s why they were changed. However, due to WordPress being committed to backwards-compatibility, the old terminology is still widely used in Core (in particular the database tables use the old conventions). To sum this up, in the SQL query $wpdb->blogs is the database table for sites, blog_id  is the ID of a site, and site_id is the ID of a network. Furthermore, the functions switch_to_blog() and restore_current_blog() are actually related to sites as well. I know, it’s really confusing, and I wish it would be different.

Another thing you should definitely keep in mind is that you should not remove the WHERE site_id = $wpdb->siteid bit. While a regular Multisite is only one network of sites, WordPress allows to have multiple networks (I won’t address this here, but be aware it can exist). Therefore this check is required – otherwise the plugin would be installed on all sites in all networks which is not what “Network Activate” means.

That’s basically it for the installation. A minor, but possibly useful hint. In the upcoming WordPress version 4.6 it will be a lot easier to get the site IDs you need in the above code snippet. WordPress will introduce a new function get_sites() (yay! This time it uses the current naming conventions) which you can use instead. To be backwards-compatible, this should only happen in a conditional statement though (unless you don’t care for users below the latest version). See the snippet below for an example that uses get_sites() plus another new function called get_current_network_id(), but remains backwards-compatible.

Now you’re really prepared, even for something that will only be available in an upcoming version of WordPress (at this point, WordPress 4.6 is a little more than a month away)!

Handling deactivation

The good thing about the above tutorial is that it applies to the deactivation routine of a plugin as well. It’s a lot less common to have deactivation routine in a plugin anyway since you shouldn’t remove anything from your plugin on deactivation. You might still have one for things like flushing rewrite rules. In a case like that, it works just like the above. You create a function myplugin_deactivate_single_site() and another function myplugin_deactivate(). The latter will be passed as a callback to register_deactivation_hook() (as the second parameter, just as in the activation hook function). Your function myplugin_deactivate will get passed a $network_wide  parameter to determine whether the plugin is being deactivated network wide or not. Then you can act on it as in the above tutorial.

Handling uninstallation

Uninstallation works a little differently. It is not as critical as not removing your plugin’s data won’t break anything. Still, you should probably do it right as otherwise your plugin’s data will remain on the WordPress site as garbage (don’t get me wrong, your plugin is not garbage, just its data when it’s not being used anymore 🙂 ).

For uninstallation, you still need to have a function myplugin_uninstall_single_site() and another function myplugin_uninstall() and pass the latter to register_uninstall_hook(), similar like you have for installation/activation and deactivation. The difference here is that there is no $network_wide  parameter. Which makes sense because your plugin is being uninstalled when its files are completely deleted from the server. That means that in your code in myplugin_uninstall(), you need to check for this another way. A good way to currently do this is simply to check whether the current site is a multisite or not. If it is, you uninstall the plugin for all sites (this time for all sites in all networks, since the plugin is completely removed), otherwise you only uninstall it for the single site. The following snippet provides an example.

Note that in your myplugin_uninstall_single_site() function you should check whether the plugin is installed at all before trying to uninstall it, for example by using an option like myplugin_installed (which was also used in the first installation code snippets).

And that’s it – all your plugin’s important routines are now compatible with Multisite, and even Multinetwork (that’s the thing when there are multiple networks in one WordPress setup). If you’re a plugin developer, I strongly encourage you to follow these (or similar) steps if you haven’t yet ensured your plugin’s installation/deactivation/uninstallation routines are compatible with Multisite. Users will be grateful for it, and your plugin will be a good example (feel free to browse through GitHub to see how many plugins are _doing_it_wrong()). And if you see other plugins that use incompatible routines, you might wanna submit a pull-request to them, or point them to this post. 🙂

2 thoughts on “Making your plugin routines multisite-compatible

  • Hi Felix, this is a great post, thanks for writing and sharing!

    Am I right if I assume that all this foreach/switch_to_blog technique is necessary only if my plugin creates its own tables?

    Or, in other words: if my plugin solely uses add_option/delete_option, I do not need to iterate through all multisite blogs.
    Is that correct?

    • Posted on
    • AuthorFelix Arntz

    Hi Christian!

    You need to use that technique for anything that happens in the installation routine that applies to a single site, so that would cover setting options as well.

    Something important that I didn’t include in this post though (because I hadn’t considered it back then): If a multisite network is very large, this procedure may not work reliably, because only a maximum of 100 sites are queried by default. Depending on which audience the plugin is intended to serve, this may not be an issue. Otherwise you might consider the alternative of not using the activation hook at all and instead run a hook on something like admin_init to check if some “installed” flag (that could be an internal option) is set for the current site, and if not, run the installation on-the-fly. This will ensure functionality is there even if it’s a network with like ~10000 sites.

Leave a Reply

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