Developing for Multisite

When developing plugins or themes there are certain considerations one needs to make if you want to make sure your plugin or theme supports multisite. In this session, we’ll cover those considerations, look at some specific multisite APIs, and convert an existing plugin to support multisite.

Learning outcomes

  1. Useful functions to know when developing for multisite
  2. Useful hooks to know when developing for multisite
  3. Theme and child theme considerations
  4. Plugin development considerations

Comprehension questions

  1. Name three useful multisite-specific functions.
  2. Name three useful multisite-specific hooks.
  3. What considerations need to be taken when developing plugins for multisite networks?
View video transcription

Hey there, and welcome to Learn WordPress.

In this tutorial, you’ll learn about things to consider when developing themes or plugins for a WordPress multisite network. You’ll learn about useful multisite specific functions and hooks, and what to consider when developing themes and plugins to work on a multisite network. You’ll also learn where to find more information on developing for multisite.

Before we get started, it’s worth noting that there are a couple of different naming conventions used in the WordPress codebase when it comes to a multisite network. WordPress multisite was originally known as WordPressMU or WordPress multi user, and many multisite related functions and hooks still use the wpmu prefix. Additionally, some functions are named based on the old terminology, which describe multiple blogs on a site. This has since been updated to describe multiple sites on a network instead. But some old terminology still lives on in some function names.

When you’re developing a product to support a multisite network, there are some useful internal functions and hooks worth knowing about. The first is the is_multisite function. This function will return true if multisite is enabled, and is probably the most widely used function related to multisite. If you do a search through the WordPress code base for all the uses of the is_multisite function, you’ll see that it’s used in a number of places to either perform specific tasks in the context of multisite network, or to restrict functionality only to multisite networks. There are also some common functions that are useful in developing administration interfaces for the multisite network. is_super_admin can be used to check if the currently logged in user is a network administrator on the network. is_network_admin is the multisite equivalent of the is_admin function and determines whether the current request is for the network administrative interface. network_admin_URL is the multisite equivalent of the admin_url function, and allows you to create your roles relative to the admin area of the network. This is useful for redirecting to different areas of the network admin dashboard.

When working with site content, there are some functions that are widely used. is_main_site determines whether the current site is the main site of the network or not. Next, there is the get_sites function, which will return a list of sites matching requested arguments. Then there is the switch_to_blog function, which allows you to switch to a different site on the network. restore_current blog, which restores the current site after you switch to a different site and get_current_blog_id which returns the ID of the current site. Using these functions, you can perform actions across the network. For example, let’s say you wanted to create a function that updated an option on a specific site on the network. Here, you’re using the switch_to_blog function to switch to that site by its ID, then using the update_option function to update the option by name passing in the value, and then finally restoring to the current site on the network. However, if you wanted to extend that update the same option across all sites, you could use the get_sites function and loop through all the sites on the network.

So we could start by removing the site ID parameter. And get a list of sites using the get_sites function. And then loop through all the sites in a foreach loop. We can move this code into the loop and update the site by the blog ID property on the site object. You could also use the update_blog_option function to update an option on a specific site without having to switch to that site. So instead of doing the switching, you can use update_blog_option and repass, pass it in the site ID and the option name and value

When developing multisite plugins there is the is_network_only_plugin function. This is a plugin specific function that checks for the Network:true value in the plugin header to see if there should be activated only as a network wide plugin. This is useful if you want to restrict a plugin to only be activated on the network and not on individual sites. There are also a couple of useful hooks that you can use when developing for multisite. First is the network_admin_menu hook which allows you to add a menu item to the network admin dashboard. The second is the network_admin_notices hook which allows you to add notices to the network admin dashboard. This is the multisite equivalent of the admin_notices hook that is used for single site notices. The signup_blogform filter is a filter that allows you to modify the signup form for new sites. You can use this to add additional fields to the signup form. Finally, wp_intialize_site is an action that is fired when a new site is created on the network. This is useful if you want to perform actions when a new site is created. For example, if you wanted to assign a custom top level domain to a new single site.

When you are rendering any content in the scope of a site on the network, WordPress Core is clever enough to know that you’re working inside the scope of that site. This means that any functions that you use to retrieve information, or any functions that you might use to add or update information will get add or update the correct tables for the site that you’re currently working with. Additionally, if you use functions like register_post_type or register_taxonomy, these will also be registered for the current site only. Generally, themes and child themes work exactly the same on a multisite network as they do on a single site. Once a theme or child theme is network activated, it can be activated on any single site on the network.

All specific functionality that you may want to code in a function so PHP file will work in the scope of the current theme. For example, if you wanted to display the site name in the footer of a theme, you could use the standard get_blog_info function to retrieve the site name.

Inside of our child theme’s functions.php we can say site_name and then just return name from get_blog_info. Then anywhere where we want to get that name, for example in the footer pattern, we can simply use that function call and the site name will appear in the footer. If we test this on our sites. At the bottom of the Bob Press site we can see proudly powered by WordPress. And at the bottom of the multi press main site we can see proudly powered by MultiPress.

However, let’s say you wanted to include the main site name in the footer of the theme, regardless of which site was currently being viewed. You could use the switch_to_blog function to switch to the main site, retrieve that site name and then restore the current sites to get the current site name.

If we go back to our function, we can say current_site_name is going to be get_blogino name. And then we could switch_to_blog. And because we want to work with the main site that will be ID one if we know that ID and then we can get the main site name using the same get_bloginfo call because we’re now in the scope of the main site and then use restore_current_blog to restore back to the main site. And then we can do something like return the site_name. And because that’s part of the main site name, network. And now that should appear in the footer of both sites. So let’s test that. Let’s check out the MultiPress site. Proudly powered by MultiPress part of the MultiPress network. And BobPress, proudly powered by BobPress part of the MultiPress network.

Taking this one step further, perhaps you want to exclude the main site only from this custom functionality, you could use the is_main_site function to check whether the current site is the main site. And if so just return that site name.

Back in our function right at the top of the function, and we could say, if is main site, then just return bloginfo name. Otherwise, if it’s any other site on the network, build the custom name. And so if we test that on the MultiPress site, the main site just says proudly powered by MultiPress. But on the BobPress site, it includes the custom functionality. And all of this is possible just within one functions.php file inside of our child theme.

As discussed, most plugin functionality will work the same on a single site as well as multisite. Functions like register_post_type or get_posts will function in the same way, just in the scope of the specific site in question. However, there are two things to consider when developing plugins for multisite.

Plugins will often have a settings page, which is usually accessible from the admin dashboard. This is fine for single site plugins, but in a multisite network, you need to consider where the settings page should be located. Should it be in the network admin dashboard or on the individual site dashboard. If you need to have the settings page on the network admin dashboard, you need to use the network_admin_menu hook to add the menu item to the dashboard. If you need to have a settings page on the individual site dashboard, you use the regular admin_menu hook to hook the menu item to the individual site dashboard.

The other option to consider is whether or not your plugin uses any kind of custom tables or custom data. If you use something like the wpdb prefix variable to prefix your table names, you’ll end up with a table name that is prefixed with the site ID on a multisite network. So if you need to have a custom table for this functionality on a per site basis, you need to plan for it. Let’s look at an example.

Here we have the plugin from the Introduction to Securely developing plugins tutorial. It has a form submissions table being created when the plugin is activated, which is used to store the form submissions. If you look at the code, you’ll see that the table name is prefixed with the prefix property from the global wpdb object. In a single sites install this means that it will create one table using the prefix that is defined in the wp-config file. In this example that will be wp_form_submissions. However, on a multisite network depending on how the plugin is installed, it will create different tables. If the plugin is activated on a single site on the network, the table prefix will include the site ID. So for site ID to the table name will be wp_2_form_submissions. However, if the plugin is network activated, the activation process is running in the scope of the main site. And it creates the same table as if it’s activated on a single site install. So for network activation, the table name is wp_form_submissions. The problem comes in when you look at the code that stores the form submissions, because this uses the same prefix property from the global wpdb object. When this code is run in the scope of the main site, it will look for the wp_form_submissions table to store the data. But for example, when it’s run in the scope of site 2 on the network, it will look for wp_2_form_submissions in the database to store the data, which does not exist.

To fix this we need to update the plugin activation routine to take this into account.

To start with it’s a good idea to move that table creation routine into a separate function and then call this function from the activation hook.

Let’s take all of this code and create a new function, which just creates the table. In the notes in the doc for the register_activation_hook, you’ll notice that the callback function accepts a network_wide parameter. This is a boolean value that is passed to the activation hook callback, and indicates whether the plugin has been activated network wide or not. You can then update the callback to first check if the site is a multisite network. And if the plugin has been activated network wide. So we’ll pass in the network wide parameter to receive either true or false. And then we’ll check if is multisite. In other words, this is a multisite network. And whether this is being activated network wide or not.

If these two checks are true fetch and loop through all the sites on the network switch to each site in turn, and create the table for each site. So we can use the get_sites function that we used earlier. To get all the sites on the network. Set up a foreach loop to loop through the array of sites returned and fetch each individual site as an object. Then use the switch_to_blog function to switch to the site in question. And the property on the site object is blog ID to get the ID. Then we can run wp_learn_create_table to create the table in the context of the specific site in question. And then use the restore_current_blog function to restore back to the last blog. Alternatively, if the plugin is not being activated network wide or not on a multisite network, you can just create the table for the current single set. So here we will do an else and just call the create_table function.

Test this out the activating this plugin on the network, you should see all the right tables being created.

So let’s actually network activate this plugin. And then if we refresh the database, there is wp_2_form submissions. And there’s the regular wp_form_submissions.

But what happens when a new site is created. In this case, you’ll need to use a hook like wp_initialize_site to create the table for any new sites on the network.

So let’s set up that functionality. We’ll say add_action pass in the wp_initialize_site hook and then register our new site function. Create the callback function. It receives a site object. And then we can use the switch_to_blog. create_table and restore_current_blog. Only difference being that the site ID is inside of the site object in this case, as Site ID. And the reason we need to do this is the user is probably registering the new site from the main site. So we need to make sure we switch to the new site ID has just been created, create the table and restore the blog.

Test that out by creating a new site. It should create a new table for that site’s form submissions in the database.

So to do this, we need to make sure that we have allowed sites and user accounts to be registered. save those changes and then if we log out and register on the network

and then say, give me a site

We now have a new site created and once the site has been activated if we check the database, the table has been created for the new site.

By making these changes, you allow your plugin to work on a multisite network, both when it’s activated for the first time on the network, taking into account any existing sites, but also future proofing for any new sites.

Besides the documentation on creating a network, and things to consider before creating a network, there is not a lot of developer focused documentation specific to developing for multisite. However, it is possible to view a list of all multisite related functionality by browsing to the multisite package URL section of the WordPress code reference.

From there you can filter by classes, functions, hooks, and class methods.

Happy coding

Introduction

Hey there, and welcome to Learn WordPress.

In this tutorial, you’ll learn about things to consider when developing themes or plugins for a WordPress multisite network.

You’ll learn about useful multisite-specific functions and hooks, and what to consider when developing themes and plugins to work on a multisite network.

A note on naming conventions

Before we get started, it’s worth noting that there are a couple of different naming conventions used in the WordPress codebase when it comes to multisite.

WordPress multisite was originally known as WordPress MU (or WordPress multi-user), and many multisite-related functions and hooks use the wpmu_ prefix.

Additionally, some functions are named based on the old terminology which described multiple “blogs” on a “site”. This has since been updated to describe multiple “sites” on a “network” instead, but some old terminology still lives on in some function names.

Useful functions

When you’re developing a product to support a multisite network, there are some useful internal functions and hooks worth knowing.

The first is the is_multsite function. This function will return true if multisite is enabled, and is probably the most widely used function related to multisite. If you do a search through the WordPress codebase for uses of the is_multsite function, you’ll see that it’s used in a number of places, to either perform specific tasks in the context of a multisite network, or to restrict functionality only to multisite networks.

There are also some common functions that are useful when developing administration interfaces for a multisite network:

  • is_super_admin can be used to check if the currently logged-in user is a Network Administrator on the network.
  • is_network_admin is the multisite equivalent of the is_admin function and determines whether the current request is for the network administrative interface.
  • network_admin_url is the multisite equivalent of the admin_url function and allows you to create URLs relative to the admin area of the network. This is useful for redirecting to different areas of the network admin dashboard.

When working with site content, there are some functions that are widely used.

is_main_site determines whether the current site is the main site of the current network or not.

Next, there is the get_sites function, which will return a list of sites matching the requested arguments.

Then there is switch_to_blog, which allows you to switch to a different site in the network, restore_current_blog which restores the current site after you’ve switched to a different site, and get_current_blog_id, which returns the ID of the current site.

Using these functions, you can perform actions across the network.

For example, let’s say you wanted to create a function that updated an option on a specific site on the network.

function update_site_option( $site_id,  $option_name, $option_value ){
    switch_to_blog( $site_id );
    update_option(  $option_name, $option_value );
    restore_current_blog();
}

However, if you wanted to extend that to update the same option across all sites, you could use the get_sites function, and loop through all sites on the network.

function update_option_on_all_sites( $option_name, $option_value ) {
    $sites = get_sites();
    foreach ( $sites as $site ) {
        switch_to_blog( $site->blog_id );
        update_option( 'my_option', 'my_value' );
        restore_current_blog();
    }
}

You could also use the update_blog_option function to update an option on a specific site, without having to switch to that site.

function update_site_option( $site_id,  $option_name, $option_value ){
    switch_to_blog( $site_id );
    update_option(  $option_name, $option_value );
    restore_current_blog();
}

When developing multisite plugins, there is the is_network_only_plugin function. This is a plugin-specific function that checks for the “Network: true” in the plugin header to see if this should be activated only as a network-wide plugin. This is useful if you want to restrict a plugin to only be activated on a network, and not on individual sites.

Useful hooks

There are also a couple of useful hooks that you can use when developing for multisite.

The first is the network_admin_menu hook, which allows you to add menu items to the network admin dashboard. This is useful if you want to add a custom menu item to the network admin dashboard.

The second is the network_admin_notices hook, which allows you to add notices to the network admin dashboard. This can be used to display a notice to network admins, in the same way that admin_notices is used for single-site notices.

signup_blogform is a filter that allows you to modify the signup form for new sites. You can use this to add additional fields to the signup form.

wp_initialize_site is an action that is fired when a new site is created. This is useful if you want to perform actions when a new site is created, for example, if you wanted to assign a custom top-level domain to a sub-site.

Developing for sites in a network

When you are rendering any content in the scope of a site on the network, WordPress core is clever enough to know that you are working inside the scope of that site. This means that any functions that you use to retrieve information, such as get_bloginfo, get_option, get_posts, or get_post_meta, and any functions you might use to add or update information, like update_option, wp_insert_post or update_post_meta, will get, add or update the correct tables for the site that you are currently working with. Additionally, if you use functions like register_post_type or register_taxonomy, these will be registered for the current site only.

Developing themes and child themes

Generally, themes and child themes work exactly the same on a multisite network as they do on a single site. Once a theme or child theme is network activated, it can be activated on any single site on the network.

All specific functionality that you may want to code into a functions.php file will work in the scope of the current theme. For example, if you wanted to display the site name in the footer of the theme, you could use the standard get_bloginfo function to retrieve the site name.

if ( ! function_exists( 'tt3c_get_site_name' ) ) {
    function tt3c_get_site_name() {
        return get_bloginfo( 'name' );
    }
}

However, let’s say you wanted to include the main site name in the footer of the theme, regardless of which site was currently being viewed. You could use the switch_to_blog function to switch to the main site, retrieve the site name, and then restore the current site.

if ( ! function_exists( 'tt3c_get_site_name' ) ) {
    function tt3c_get_site_name() {
        $site_name = get_bloginfo( 'name' );
        switch_to_blog( 1 );
        $main_site_name = get_bloginfo( 'name' );
        restore_current_blog();

        return $site_name . ' (part of the ' . $main_site_name . ' network)';
    }
}

Taking this one step further, perhaps you want to exclude the main site only from this custom functionality. You could use the is_main_site function to check whether the current site is the main site, and if so, just return the site name.

if ( ! function_exists( 'tt3c_get_site_name' ) ) {
    function tt3c_get_site_name() {
        if ( is_main_site() ) {
            return get_bloginfo( 'name' );
        }

        $current_site = get_bloginfo( 'name' );
        switch_to_blog( 1 );
        $main_site_name = get_bloginfo( 'name' );
        restore_current_blog();
        return $current_site . ' (part of the ' . $main_site_name . ' network)';
    }
}

All of this is possible from just one functions.php file inside of a single child theme.

Developing plugins

As discussed, most plugin functionality will work the same in a single site as well as a multisite. Functions like register_post_type or get_posts will function in the same way, just in the scope of the specific site in question.

However, there are two things to consider when developing plugins for multisite.

Plugins often have a settings page, which is usually accessible from the admin dashboard. This is fine for single-site plugins, but on a multisite network, you need to consider where the settings page should be located. Should it be on the network admin dashboard, or on the individual site dashboard? If you need to have a settings page on the network admin dashboard, you can use the network_admin_menu hook to add a menu item to the network admin dashboard. If you need to have a settings page on the individual site dashboard, you can use the admin_menu hook to add a menu item to the individual site dashboard.

Plugins might have to add custom tables to store custom data. If you use something like the $wpdb->prefix variable to prefix your table names, you’ll end up with a table name that is prefixed with the site ID. So if you need to have a custom table for this functionality on a per-site basis, you need to plan for it.

Let’s look at an example.

Here we have the example plugin from the Introduction to securely developing plugins tutorial. It has a form_submissions table being created when the plugin is activated, which is used to store form submissions. If you look at the code, you’ll see that the table name is prefixed with the prefix property from the global $wpdb object.

In a single site install, this means that it will create one table using the prefix that is defined in the wp-config.php file, in this example wp_form_submissions

However, on a multisite network, depending on how the plugin is installed, it will create different tables.

If it’s activated on a single site on the network, the table prefix will include the site ID.

So for site 2 the table name will be wp_2_form_submissions.

However, if the plugin is network activated, the activation process is running in the scope of the main site, and it creates the same table as if it’s activated on a single site install.

So for network activation the table name will be wp_form_submissions.

The problem comes in when you look at the code that stores the form submissions.

Because this uses the same prefix property from the global $wpdb object when this code is run in the scope of the main site, it will look for the wp_form_submissions table to store the data, but, for example, when it’s run in the scope of site 2 on the network, it will look for wp_2_form_submissions to store the data, which does not exist.

To fix this we need to update the plugin activation routine, to take this into account:

To start, move the table creation code into a separate function, and then call this function from the activation hook.

function wp_learn_create_table(){
    global $wpdb;
    $table_name = $wpdb->prefix . 'form_submissions';

    $sql = "CREATE TABLE $table_name (
      id mediumint(9) NOT NULL AUTO_INCREMENT,
      name varchar (100) NOT NULL,
      email varchar (100) NOT NULL,
      PRIMARY KEY  (id)
    )";

    require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
    dbDelta( $sql );
}

In the docs for the register_activation_hook hook, you’ll notice the callback function accepts a $network_wide parameter. This is a boolean value that is passed to the activation hook callback and indicates whether the plugin is being activated network-wide or not.

Then you can update the callback to first check if the site is a multisite network, and if the plugin is being activated network-wide.

If it is, fetch and loop through all sites on the network, switch to each site in turn, and create the table for each site.

Alternatively, if the plugin is not being activated network-wide, you can just create the table for the current site.

register_activation_hook( __FILE__, 'wp_learn_setup_table' );
function wp_learn_setup_table( $network_wide ) {
    if ( is_multisite() && $network_wide ) {
        $sites = get_sites();
        foreach ( $sites as $site ) {
            switch_to_blog( $site->blog_id );
            wp_learn_create_table();
            restore_current_blog();
        }
    } else {
        wp_learn_create_table();
    }
}

Test this out by activating the plugin on the network, you should see all the right tables have been created.

But what happens when a new site is created? In that case, you’d need to use a hook like wp_initialize_site, to create the table for any new sites.

add_action( 'wp_initialize_site', 'wp_learn_setup_newsite_table' );
function wp_learn_setup_newsite_table( $site ) {
    switch_to_blog( $site->id );
    wp_learn_create_table();
    restore_current_blog();
}

Test that out by creating a new site. It should create a new table for that site’s form submissions in the database.

In this way you allow your plugin to work on a multisite network, both when it’s activated for the first time on the network, taking into account any existing sites, but also future-proofing for any new sites.

Where to find more information

Besides the documentation on Creating a network and things to consider before creating a network, there’s not a lot of developer-focused documentation specific to developing for multisite.

However, it is possible to view a list of all multisite-related functionality by browsing to the multisite package section of the WordPress code reference.

From there you can filter by classes, functions, hooks, and class methods.

Happy coding!

Length 21 minutes
Language English
Subtitles English

Suggestions

Found a typo, grammar error or outdated screenshot? Contact us.