Developer Diaries – WP Teams and Multisite Architecture

low angle photography of gray building at daytime
Photo by Anders Jildén on Unsplash

I went over the features that I wanted for WP Teams in the last developer diary. One question that keeps lingering in the back of my mind was if I should add multisite support for it.

This changes a lot of the data architecture. I originally figured that each Team could be a hidden post type, and that each team member was part of another hidden post type with a taxonomy attached for categories.

However, on multisite, things get complicated. I could theoretically do the architecture on the main site with the post types and taxonomies, but there are performance implications with always having to pull the data from the parent site.

I’m trying to ponder this and avoid a lot of custom database tables, but this may have to be the way to go.

Data Architecture

If multtisite is supported, there will have to be a global table for each team. There’s already going to be one for social networks, so that’s not a huge deal.

For the taxonomies, I’ll essentially have to re-create the way WordPress handles taxonomies at a more abstract level. Each taxonomy term will be attached to a taxonomy and team.

Then there’s the issue of the team members. There’s not really any concept in multisite of a network-wide media library. So I’d have to fake it. I can create a custom folder for the network in the uploads directory, and store this information in another global table.

This type of architecture will be hard to achieve and there’s a lot of risk in doing things wrong, so if you have any better ideas, please comment below.

I can always get rid of the multisite support, but I feel a Network Admin will not want to have to re-create each team from scratch for each site in the network. That would be cumbersome. Plus, with multisite support, I can theoretically integrate with other third-party plugins such as BuddyPress, BuddyBoss, and other network-friendly plugins.

Adding Single and Multisite Support

In order to make the plugin multisite compatible, I had to ensure that if the plugin is activated on a network, then the plugin can only be network activated and not active on the single-site level. This is pretty easy. All you have to to do is add a flag to the top of the plugin header.

<?php
/**
 * Team
 *
 * @package   teams
 * @copyright Copyright(c) 2020, MediaRon LLC
 * @license http://opensource.org/licenses/GPL-2.0 GNU General Public License, version 2 (GPL-2.0)
 *
 * Plugin Name: Teams
 * Plugin URI: https://wpteams.pro
 * Description: A way to display your teams off on a stylistic way.
 * Version: 1.0.0
 * Author: MediaRon LLC
 * Author URI: https://mediaron.com
 * License: GPL2
 * License URI: http://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain: mr-teams
 * Domain Path: languages
 * Network: true
 */Code language: PHP (php)

The Network: true simply states that if the plugin is installed on a network, it can only be network-activated, which is what I want in order to make it multisite compatible.

There’s other caveats such as options, so let’s go over that first.

Single vs. Multisite Options

On single-site, you can simply use get_option to retrieve the options for your plugin. On multisite, you’d want to use get_site_option. Since I’m completely fine with only a network-activated plugin on multisite, I just use get_site_option everywhere since it also works for single-site just fine without any extra crazy conditionals.

Testing for Multisite

To test for multisite, I came up with the following static method. It needs some more refactoring, but here’s a rough example.

/**
 * Checks if the plugin is on a multisite install.
 *
 * @since 1.0.0
 *
 * @param bool $network_admin Check if in network admin.
 *
 * @return true if multisite, false if not.
 */
public static function is_multisite( $network_admin = false ) {
	if ( ! function_exists( 'is_plugin_active_for_network' ) ) {
		require_once ABSPATH . '/wp-admin/includes/plugin.php';
	}
	$is_network_admin = false;
	if ( $network_admin ) {
		if ( is_network_admin() ) {
			if ( is_multisite() && is_plugin_active_for_network( MR_TEAMS_SLUG ) ) {
				return true;
			}
		} else {
			return false;
		}
	}
	if ( is_multisite() && is_plugin_active_for_network( MR_TEAMS_SLUG ) ) {
		return true;
	}
	return false;
}Code language: PHP (php)

The above checks if we’re in a multisite install and that plugin is active for the network. It also has an optional flag called $network_admin, which will ensure we’re in the network admin when performing the check.

Single vs. Multisite Admin Menus

Since I’m creating a top-level menu item, I don’t have to worry too much about targeting the right parent menu item in single vs. multisite.

Instead, I just use the same bit of code to register the top-level menu item.

if ( Functions::is_multisite() ) {
	add_action( 'network_admin_menu', array( $this, 'register_admin_menu' ) );
} else {
	add_action( 'admin_menu', array( $this, 'register_admin_menu' ) );
}Code language: PHP (php)

After Plugin Row Action

One thing I wanted to add was a license prompt after the plugin row if a license isn’t active.

After Plugin Row Example
After Plugin Row Example

I just decided to use the same method for both single and multisite.

add_action( 'after_plugin_row_' . MR_TEAMS_SLUG, array( $this, 'after_plugin_row' ), 10, 3 );Code language: PHP (php)

And here’s the method that’s called:

/**
 * Adds license information
 *
 * @since 1.0.0
 * @access public
 * @see __construct
 * @param string $plugin_file Plugin file.
 * @param array  $plugin_data Array of plugin data.
 * @param string $status      If plugin is active or not.
 * @return void HTML Settings.
 */
public function after_plugin_row( $plugin_file, $plugin_data, $status ) {
	$options        = Options::get_options();
	$license        = $options['license'];
	$license_status = $options['license_status'];
	if ( Functions::is_multisite( true ) ) {
		$options_url = add_query_arg(
			array(
				'page' => 'mr-teams',
				'tab'  => 'license',
			),
			network_admin_url( 'admin.php' )
		);
	} else {
		$options_url = add_query_arg(
			array(
				'page' => 'mr-teams',
				'tab'  => 'license',
			),
			admin_url( 'admin.php' )
		);
	}
	if ( ! Functions::is_multisite( true ) ) {
		return;
	}
	if ( empty( $license ) || false === $license_status ) {
		echo sprintf( '<tr class="active"><td colspan="3">%s <a href="%s">%s</a></td></tr>', esc_html__( 'Please enter a license to receive automatic updates.', 'mr-teams' ), esc_url( $options_url ), esc_html__( 'Enter License.', 'mr-teams' ) );
	}
}Code language: PHP (php)

The goal here is to show the plugin row on single-site, but avoid showing the plugin row on a sub-site when the plugin is network-activated.

Plugin Status Links

You may have noticed I have an Options and Add Ons link in the plugin status area. In multisite, I want a sub-site to show the options if the user is a network admin and link to the network admin options. On single site, I want the options to point to the single-site options. And then in the network admin area, I want to link to the correct option pages as well.

Let’s start with initialization:

add_filter( 'plugin_action_links_' . plugin_basename( MR_TEAMS_FILE ), array( $this, 'add_settings_link' ) );
add_filter( 'network_admin_plugin_action_links_' . plugin_basename( MR_TEAMS_FILE ), array( $this, 'add_settings_link' ) );Code language: PHP (php)

They both point to the same method. The code below appears to be working for me at the moment.

/**
 * Add a settings link to the plugin's options.
 *
 * Add a settings link on the WordPress plugin's page.
 *
 * @since 1.0.0
 * @access public
 *
 * @see run
 *
 * @param array $links Array of plugin options.
 * @return array $links Array of plugin options
 */
public function add_settings_link( $links ) {
	if ( Functions::is_multisite() && current_user_can( 'manage_network' ) ) {
		$options_link = sprintf( '<a href="%s">%s</a>', esc_url( network_admin_url( 'admin.php?page=mr-teams' ) ), _x( 'Options', 'Plugin settings link on the plugins page', 'mr-teams' ) );
		$addons_link  = sprintf( '<a href="%s">%s</a>', esc_url( network_admin_url( 'admin.php?page=mr-teams-add-ons' ) ), _x( 'Add-Ons', 'Plugin settings link on the plugins page', 'mr-teams' ) );
		array_unshift( $links, $addons_link );
		array_unshift( $links, $options_link );
	} elseif ( ! Functions::is_multisite() && ! is_network_admin() ) {
		$options_link = sprintf( '<a href="%s">%s</a>', esc_url( admin_url( 'admin.php?page=mr-teams' ) ), _x( 'Options', 'Plugin settings link on the plugins page', 'mr-teams' ) );
		$addons_link  = sprintf( '<a href="%s">%s</a>', esc_url( admin_url( 'admin.php?page=mr-teams-add-ons' ) ), _x( 'Add-Ons', 'Plugin settings link on the plugins page', 'mr-teams' ) );
		array_unshift( $links, $addons_link );
		array_unshift( $links, $options_link );
	}

	return $links;
}Code language: PHP (php)

Script Enqueueing

I have several JavaScript and CSS files I need to include on the options screen. But how do I make them both single and multisite compatible?

It’s as simple as checking for the screen base.

/**
 * Add React Script to Options Screen.
 */
public function add_options_react_script() {
	$screen = get_current_screen();
	wp_register_script(
		'jquery.block.ui',
		MR_TEAMS_URL . 'js/block.ui.js',
		array( 'jquery' ),
		MR_TEAMS_VERSION,
		true
	);
	if ( 'toplevel_page_mr-teams' === $screen->base || 'toplevel_page_mr-teams-network' === $screen->base ) {
		// Enqueue scripts and styles.
	}
}Code language: PHP (php)

Next Steps

I still need to figure out the data architecture that is scalable and flexible. I’ll post another entry with the data structure I decide on and my rationale behind it.

Thanks for reading.

Ronald Huereca
Ronald Huereca

Ronald Huereca

Ronald has been part of the WordPress community since 2006, starting off writing and eventually diving into WordPress plugin development and writing tutorials and opinionated pieces.

No stranger to controversy and opinionated takes on tough topics, Ronald writes honestly when he covers a topic.

Ronald Huereca
MediaRon - Ronald Huereca

Ronald created MediaRon in 2011 and has more than fifteen years of releasing free and paid WordPress plugins.

Quick Links

Say Hi