Lessons learned in WordPress plugin development

facebooktwittergoogle_pluslinkedin

The last couple of weeks I got into WordPress plugin development. Although I have been using WordPress for several occasions, I never really got beyond the point where some simple theme customizations did not get there. In general I am not a big fan of php. Nevertheless I admired WordPress for its coding standards and their through thought architecture. So I was quite excited when I could actually delve into the API of WordPress and start writing my own plugins.

To make things a bit more challenging, my first plugin was actually set to work in a network environment. This facet of WordPress is not that known as most WordPress sites only host exactly one blog or site and do not use any of the network features. Plugin development for a network environment has some pitfalls though. First, it is less well documented due to above mentioned reason. Second, some functions are simply missing. E.g. while it is perfectly easy to add a menu page in the admin area for a single site, the same gets more complex for the network admin area. And third, the system obviously gets more complex. Suddenly you have to worry more about security, roles and users than you usually have to for a single site plugin.

But enough introduction, lets share some things I learned during the last couple of weeks. These lessons are things that I wish somebody would have told me before I started coding all night. It could have saved me a lot of trouble and effort.

PHP Versions

The first thing I struggled with was the abundance of PHP versions out there that are still in use even though they are not even supported anymore. When I set up my local environment I installed version 5.5. When I pushed my code to production for the first time, it colossally failed, because the server was still using 5.2. The main reason was function array dereferencing which was added in 5.4 I believe. When developing plugins for a existing framework you obviously have to comply with the minmal requirements of this framework. In case of WordPress this is PHP 5.2. This was kind of new to me, as I have never yet encountered such issues yet in other technologies.

Summary: If you are writing plugins for WordPress, make sure that all runs on old PHP versions as well (can go as far back as 5.2.4 according to wordpress.org but usually 5.3 is enough).

Naming stuff is difficult

Well, I should have known it, right? When hosting your plugin at wordpress.org you get a Subversion repository and then it gets listed in their plugin directory. When registering for plugin, make sure that you know how the plugin will be called in the future because there is no way to change the name afterwards. Well, I was not that sure and suddenly had the urge to rename it. The only way to do so is deleting the old repository and creating a new one. Thus, you would lose any existing users.

As a side note: Naming stuff also seems to be an issue with the WordPress core. I got confused so often, what a site is. Most often it is a single blog. But suddenly it is the network. And then the single blogs are called blogs and not sites. And so on…

Summary: Think twice about the name of your plugin. It is a pain to change it afterwards.

Writing code is easy, writing it OOD is hard

Well, it is not that hard. But you kind of have to enforce it otherwise it will get lost. When browsing through various tutorials, they usually do not show it in a object-oriented way and php allows that as well. Sure, there are some exceptions (e.g. Widgets), but most of the time you are free to structure and design your plugin as you wish. This gets chaotic very fast. And recommend anybody to start this way in the first place. This is how that looks like:

<?php
/* Plugin header stuff */
include_once dirname( __FILE__ ) . '/class-network-summary.php';

if ( class_exists( 'Network_Summary' ) ) {
    register_activation_hook( __FILE__, array('Network_Summary', 'activate') );
    register_deactivation_hook( __FILE__, array('Network_Summary', 'deactivate') );

    $network_overview = new Network_Summary();
}

network-summary.php

This would be the main plugin file. It simply checks whether we have already run the setup or not. If not, we register the activation and deactivation hooks and then create the object. When taking a closer look at the hooks, you see that we are referencing it through an array which first takes the name of the class and then the method on that class. These methods have to be static methods (see below).

<?php

class Network_Summary
{
	const SITE_SETTING_NAME = 'network_summary';
	const NETWORK_SETTING_NAME = 'network_summary';
	const CURRENT_VERSION = '1.1.2';

	/**
	* Construct the plugin object by registering actions and shortcodes.
	*/
	public function __construct() {
		add_action( 'admin_init', array(&$this, 'admin_init') );
		add_action( 'admin_init', array(&$this, 'maybe_update') );
		add_action( 'network_admin_menu', array(&$this, 'add_network_admin_page') );

		add_shortcode( 'netview', array(&$this, 'netview_overview') );
		add_shortcode( 'netview-single', array(&$this, 'netview_single') );
		add_shortcode( 'netview-all', array(&$this, 'netview_all') );

		add_action( 'wpmu_new_blog', array(&$this, 'add_new_site') );

		add_action( 'admin_post_update_network_summary_network_settings', array(&$this, 'update_network_settings') );

		add_action( 'widgets_init', function () {
			register_widget( 'Site_Description_Field_Widget' );
		} );
	}

	/**
	* Performs the required setup on activation. Setting default values for the settings.
	*/
	public static function activate() {
		$site_list = Network_Summary::list_sites();
		foreach ( $site_list as $site_id ) {
			update_blog_option( $site_id, Network_Summary::SITE_SETTING_NAME, array(
				'share_site' => '0',
				'site_description' => '')
			);
		}
		update_site_option( Network_Summary::NETWORK_SETTING_NAME, array('deciding_role' => 'network_admin') );
		update_site_option( 'network_summary_version', Network_Summary::CURRENT_VERSION );
	}

	/**
	* Tear down on deactivation. Deletes all the settings.
	*/
	public static function deactivate() {
		foreach ( Network_Summary::list_sites() as $site_id ) {
			delete_blog_option( $site_id, Network_Summary::SITE_SETTING_NAME );
		}
		delete_site_option( Network_Summary::NETWORK_SETTING_NAME );
	}

	/* Other functions. */
}

class-network-summary.php

This is just a small excerpt for the functions that are directly referenced in the first file. As you can see, the references for all hooks are now also arrays with the first element being a reference to the object and the second the function name to call on the object. I have been told, that this should rather not be provided by reference but by value. So this might change in the future. You can also see some constants in the beginning. I will come to that next.

Summary: Try to encapsulate the logic of your plugin with object-oriented principles already early on. The migration might get trickier the longer you wait.

Settings API

The WordPress Settings API is a rather neat piece of API. Sure, they also have some naming difficulties (why is the table called options, when you are adding settings…and so on). But there are some pitfalls. You start writing your plugin and it has a handful of settings, that you want to register. No big deal, just register each of them and you are done. But now your plugin grows and so does the list of settings. It seems ugly to pollute the settings table with so many settings from just one plugin. That is why many plugins just have one setting and store all their stuff in an array with the details. It is not difficult to implement but quite some work if you have to refactor it later on. So I suggest, you start doing it from the first setting on.

To get rid of all the array dereferencing I wrote a simple helper method on my class to retrieve all the options for both single sites and the network.

public static function get_plugin_option( $tag, $blog_id = null ) {
	if ( $blog_id != null ) {
		$option = get_blog_option( $blog_id, Network_Summary::SITE_SETTING_NAME );
	} else {
		$option = get_option( Network_Summary::SITE_SETTING_NAME );
	}
	if ( isset($option[$tag]) ) {
		return $option[$tag];
	} else {
		return null;
	}
}

class-network-summary.php

In addition to that, to store the plugin option name in a const is also highly recommended for renaming purposes.

Summary: Store your settings in a array and one single option. Make sure you have a way to quickly rename all of them.

Caching and transient values

So, last but not least we can touch on the second hard thing: Caching, or in WordPress terms the Transients API. It makes caching really easy. As it is possible to output large chunks of html with my plugin and the construction of this html can result in my database queries, I though it might be a smart idea to cache the output.

This is done like that:

public function netview_overview( $atts ) {
	$params = shortcode_atts( array(
		'include' => $this->get_shared_sites(),
		'exclude' => array(),
		'numposts' => 2,
		'sort' => 'abc',
		'layout' => 'table'
	), $atts, 'netview' );

	$param_hash = md5( serialize( $params ) );

	if ( false === ($result = get_transient( 'netview_overview_' . $param_hash )) ) {
		/* Construction of result */
		set_transient( 'netview_overview_' . $param_hash, $result, 7200 );
	}
	return $result;
}

class-network-summary.php

This works quite well. For each set of parameter a new cache entry gets generated. But for debug, development and testing purposes this is terrible. So if I had to do this again, I would have added global option for whether caching is enabled or not. Another approach would be to check for the WP_DEBUG variable in the config and if enabled, disable the cache. But then again, what if you would like to test the caching but you have to be in debug mode in order to do so?

Summary: Wrap the caching in some sort of option – even it is only a static field you manually change in your code.

Okay, that is it for now. Quite possible, that I will add some stuff later on in future posts.

If you want to check out the source code of this plugin, feel free to head over to the plugin page.

2 thoughts on “Lessons learned in WordPress plugin development

  1. Pingback: OOD in WordPress Plugins | alea iacta est

Comments are closed.