Conditional activation for plugins

In this post, I present a simple way to check for things on the server before allowing a plugin to be activated.

The users are pesky. They try to do things that the programmer cannot foresee. They happily install plugins without reading the readme files or the instructions. Then they complain, they downvote, they critizice. If only there was a way to check for these things: is the user running the plugin under the correct WordPress version, or if the necessary dependendencies are installed, or if PHP was compiled with such-and-such extensions. Then we’d let the user know about all this at the plugin activation time. What if this ability was coded as an easy-to-add drop-in class, instead of bloating existing plugin code? I couldn’t find it, so I made it, and now I’m sharing it with you.

I got started on this topic in February 2014, but then I was too busy with other projects. Back then, I couldn’t find anything on Google about conditional plugin activation. But while I was busy with other stuff, the world had moved on: There’s a good article on the the topic at the pressingmatters.io blog. There’s also a post by Gary Pendergast called “Don’t Let Your Plugin Be Activated on Incompatible Sites“, which introduces a similar approach as the one presented in this post.

I felt it was necessary, however, to use an Object-Oriented approach. Personally, I have a hard time understanding how anyone might not use classes for their plugins, not even as a simple container class. Anyway, the approach I present here shows one of the benefits of Object-Oriented Programming: extending one class with another. When done in this manner there’s no need to add the same bits of code to each and every plugin. We can just make the plugin inherit this capability from a parent class.

Below is the code for the abstract parent class. Why is it called abstract? To put it short: because it won’t work on its own. It must be extended.

Extending a class means creating another class that automatically inherits the methods from its parent. The abstract keyword in the following class definition makes sure no one tries to use this class without creating a child class first.

The code of the base class is explained in the comments.

/**
 * A base class for WordPress plugins with activation-time prerequisites.
 *
 * The method check_plugin_requirements() must be extended in a child class.
 * This function must return a string or an array of strings containing the
 * descriptions of unmet requirements for the plugin activation. If the
 * requirements are met, the method must return an empty value (e.g. an empty
 * array or false).
 *
 * The child class must call the add_activation_hooks() method. The filepath
 * of the plugin main file must be given as a parameter. Typically, this would
 * be the __FILE__ magic constant of the child class.
 *
 * For the benefit of pretty messages, the child class should define a
 * PLUGIN_NAME constant (for PHP > 5.3) or call the add_activation_hooks()
 * method with the second $plugin_name parameter set to the same effect.
 */
abstract class Enrique_Lib_PluginRequirements {

	private $_plugin_main_filepath;
	private $_req_check_option;
	private $_plugin_name = '';

	/**
	 * Register the necessary activation hooks.
	 * This method should be called in the main plugin constructor.
	 * @param 	string 	$filepath 		The path to the plugin main file
	 * @param 	string 	$plugin_name	(Optional) Human readable plugin name
	 */
	public function add_activation_hooks( $filepath, $plugin_name = '' ) {
		if ( ! file_exists( $filepath ) )
			throw new InvalidArgumentException( 'Invalid filepath' );
		$this->_plugin_main_filepath = $filepath;
		$this->_req_check_option = basename( $filepath ) . '-reqs-failed';
		if ( $plugin_name )
			$this->_plugin_name = $plugin_name;
		register_activation_hook(
			$this->_plugin_main_filepath,
			array( $this, 'activate_plugin_callback' )
		);
		register_deactivation_hook(
			$this->_plugin_main_filepath,
			array( $this, 'deactivate_plugin_callback' )
		);
		if ( get_site_option( $this->_req_check_option ) ) {
			add_action(
				'admin_notices',
				array( $this, 'show_activation_error' )
			);
			add_action(
				'admin_init',
				array( $this, 'force_deactivate' )
			);
		}
	}

	/**
	 * Callback for the plugin activation.
	 * Calls the check_plugin_requirements() method. You may extend this method
	 * in a child class, but remember to call parent::activate_plugin_callback()
	 */
	public function activate_plugin_callback() {
		$reqs_failed = $this->check_plugin_requirements();
		if ( !empty( $reqs_failed ) )
			update_site_option( $this->_req_check_option, $reqs_failed );
		else
			delete_site_option( $this->_req_check_option );
	}

	/**
	 * Callback for plugin deactivation.
	 * Included here for the sake of completeness.
	 */
	public function deactivate_plugin_callback() {
	}

	/**
	 * Deactivate plugin callback.
	 * For the admin_init hook.
	 */
	public function force_deactivate() {
		deactivate_plugins( $this->_plugin_main_filepath );
	}

	/**
	 * Check the plugin requirements
	 *
	 * This method must be overridden in a child class. This is where
	 * the plugin activation requirements are checked. The method must
	 * return either a string or an array of strings containing the
	 * descriptions of the FAILED requirements, e.g. "PHP version must
	 * be greater than 5.3". On success, return an empty array or an
	 * empty string.
	 *
	 * @return 	mixed	A string or an array of strings containing
	 *					descriptions of failed requirements. Return empty
	 *					if everything is OK to proceed with activation.
	 */
	abstract protected function check_plugin_requirements();

	/**
	 * Print the activation error.
	 */
	public function show_activation_error() {
		$error_list = (array) get_site_option( $this->_req_check_option );
		if ( ! $this->_plugin_name && function_exists( 'get_called_class' ) ) {
			$called_class = get_called_class();
			if ( defined( $called_class . '::PLUGIN_NAME' ) )
				$this->_plugin_name = $called_class::PLUGIN_NAME;
		}
		echo "<div class='error'><p>";
		if ( $this->_plugin_name ) {
			printf(
				"The plugin <strong>%s</strong> could not be activated.",
				$this->_plugin_name
			);
		} else {
			printf(
				"Plugin <strong>activation failed</strong> in <code>%s</code>.",
				basename( $this->_plugin_main_filepath )
			);
		}
		echo "</p><ul class='ul-disc'>\n";
		foreach ( $error_list as $error ) {
			echo "<li>$error</li>\n";
		}
		echo "</ul></div>";
		remove_action( 'admin_notices', array( $this, 'show_activation_error' ) );
		delete_site_option( $this->_req_check_option );
		unset( $_GET['activate'] );	// Prevents printing the "Plugin activated" message.
	}

}

The above is the code for the base class. As I mentioned earlier, the base class is not meant to be used on its own – in fact, it cannot be – so to use this code, we need to make a child class. The child class would be the main class of the plugin. (If you have no idea about what Object-Oriented plugin development might be, I suggest you Google it now and take some time to understand how to move forward from the awful procedural plugin coding style.) So, we would have in the main plugin file a class declaration like this:

/**
 * Plugin Name: A Demanding Plugin
 */

require( 'path/to/file/containing/the/parent.php' );

class DemandingPlugin extends Enrique_Lib_PluginRequirements {

	const PLUGIN_NAME = "A Demanding Plugin";

	public function __construct() {
		$this->add_activation_hooks( __FILE__ );
	}

	protected function check_plugin_requirements() {
		$failed = array();
		if ( ! extension_loaded( 'foobar_extension' ) )
			$failed[] = 'The PHP Foobar extension must be installed on the server.';
		if ( version_compare( PHP_VERSION, '8.0.0', '<' ) )
			$failed[] = 'The PHP version must be at least 8.0.0';
		return $failed;
	}

}

$DemandingPlugin = new DemandingPlugin();

The DemandingPlugin has now inherited the add_activation_hooks() method (and other methods) from the parent class. We don’t have to repeat any of the code. The only thing that we have to put in the plugin class is the definition of the check_plugin_requirements() method. Remember, it was marked as abstract in the parent to really force us to do so!

In the above example, the check_plugin_requirements() initializes an empty array. Then it checks some requirements and adds a string to the array for every failed requirement.

The DemandingPlugin is very demanding indeed: there will be an activation error message if the “Foobar Extension” is not loaded. It’s also demanding PHP version 8 (the current state-of-the-art version is the 5.6). So, these requirements won’t be fulfilled on any system. What happens? When the users tries to activate the Demanding Plugin, he gets a nice message like this:

Plugin not activated

This way, it’s easy for you, the plugin developer, to test for whatever conditions on the server, allowing the plugin to activate only when the conditions are right. You might test for PHP extensions, safe mode, permissions, availability of some functions, the WordPress version, the presence of other plugins… Also, you avoid a bunch of user complaints if you make something that only works under a certain configuration.

It’s also possible to do plugin-specific activation stuff. We only need to extend the activate_plugin_callback() method of the base class and include a call to the parent. If the plugin was adding rewrite rules, for example, we could do something like this in the plugin:

	public function activate_plugin_callback() {
		parent::activate_plugin_callback();
		$this->add_rewrite_rule();
		$this->flush_rewrite_rules();
	}

The call to the parent::activate_plugin_callback() method in the extended child method runs the code from the base class before doing its own custom stuff. Basics of Object-Oriented Programming, really, and something that makes WordPress plugin development much more modular and gives your code the reusability that is one of the hallmarks of good coding practices.

Advertisements
Tagged , ,

One thought on “Conditional activation for plugins

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: