Developer Diaries – WP Teams and Multisite Architecture

Posted by Ronald Huereca / April 25, 2020 /

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.flywheelsites.com
 * License: GPL2
 * License URI: http://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain: mr-teams
 * Domain Path: languages
 * Network: true
 */

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() &amp;&amp; is_plugin_active_for_network( MR_TEAMS_SLUG ) ) {
				return true;
			}
		} else {
			return false;
		}
	}
	if ( is_multisite() &amp;&amp; is_plugin_active_for_network( MR_TEAMS_SLUG ) ) {
		return true;
	}
	return false;
}

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' ) );
}

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 );

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' ) );
	}
}

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' ) );

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() &amp;&amp; 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() &amp;&amp; ! 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;
}

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.
	}
}

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.

Developer at MediaRon
Ronald Huereca is the CEO of MediaRon LLC and enjoys WordPress plugin and theme development.

Connect with the Author

Posted in

Leave a Comment

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

Scroll to Top