A Wordpress Plugin Boilerplate
- 1/30/2012
- ·
- #index
WordPress’s decision to provide only minimal guidelines for third party developers has had two significant consequences. On the one hand, low barriers to entry have spawned an enormous community of plugin contributors. Let’s face it—writing a loadable plugin is easy. Balancing the equation, though, are the extremely poor odds that any two developers left up to their own devices will make similar architectural decisions. That’s a small problem for the end user, but it can also turn into mountains of frustration for anyone that has to sort out conflicts or maintain inconsistent code.
So let’s make it better. Let’s outline a process for creating plugins that:
- Minimizes the risk of conflict with other plugins
- Saves time by recycling key code
- Encourages techniques that improve maintainability
The demo project is available on github.
Organization
MyPlugin
|--include/
| `--MY_Plugin.php
|--views/
| `--hello_world.php
|--MyDemoPlugin.php
`--README.txt
WordPress’ pedigree aside—I don’t have numbers, but behind the Linux kernel it has to be one of the most widely-used pieces of procedural code ever to touch the web—plugins are one place where a nominally object-oriented approach makes sense. Organizing plugins into singleton classes encourages consistency and provides pseudo-namespacing while providing all the usual benefits (read: inheritance!).
The two classes in use here are:
My_Plugin
(MY_Plugin.php
) contains the boilerplate parent classMyDemoPlugin
(MyDemoPlugin.php
) contains the plugin class
The Parent Class
First things first: we’ll get started by consolidating the foundational methods used by Every Single Plugin Ever into a single, inheritable parent class. Instead of creating a monolithic interface that provides every feature a plugin author could dream of, the idea here is to create a boilerplate that implements only the pieces that show up over and over again. Call it WP_Plugin, if you like; in the interests of avoiding naming conflicts (and in the fervent hope that such a class will emerge in the future) I’ve opted to go with the slightly-less predictable MY_Plugin
.
The contents of the MY_Plugin
class address three repeated tasks in plugin development:
- Declaring hooks and callbacks
- Separating content and code
- Creating, saving, and modifying plugin settings
If you want to skip ahead to the results, the completed boilerplate is available here.
1. Hooking WordPress
WordPress provides filters and actions to allow plugins to interact with the core. A plugin can’t do much without them, so the boilerplate assumes they’ll get used. It also assumes that each plugin class will declare them in the protected $actions
and $filters
arrays at the beginning of the class and implement them later on:
// in class MyDemoPlugin
protected
$actions = array(
'widget_init'
);
public function widgets_init() {
// do startup-y things
}
2. Separating Content from Code
WordPress has the bad habit of confusing “content” and “delivery”. The inclusion of load_view
in an otherwise spartan boilerplate is indulgent, but the huge improvements in maintainbility that arise from consistent use of content templates make it worth doing. The idea behind this function will be pretty familiar to anyone who has worked in an MVC framework. It’s pretty simple: rather than including HTML output inside the plugin itself, content views should be implemented as stand-alone templates and populated with variable data. For instance,
// in views/hello_world.php
<h1><?php echo $title; ?></h1>
<p><?php echo $message; ?></p>
Assuming views are stored in the MyPlugin/views
directory, the load_view
function will load a view and either echo it or returns its content:
// in class MyDemoPlugin
public function test_view() {
$data = array(
'message' => 'hello, world!',
'title' => 'A message'
);
$this->load_view('hello_world', $data);
}
Methods:
load_view($view, $data = array(), $echo = true)
: render the specified view ($echo = true
) or return it as an HTML string ($echo = false
)
3. Settings & Options
Polluting WordPress’s options table with more than one option per plugin isn’t just bad form. It’s also a great, big, glowing beacon asking for a collision with someone else’s data. Using namespaces and implementing plugin settings through the settings API can help avoid this, but namespaces don’t reduce pollution any more than the settings API is designed to handle internal configurations. The boilerplate offers a minimalist alternative, serializing plugin settings into a single object in wp_options
that’s conveniently keyed to the unique name of the plugin class.
As an added bonus for maintainability, the boilerplate strongly encourages authors to enumerate all settings used by the plugin in the $options
variable at the top of the class. There’s no functional bonus for doing this, but it can greatly improve the finished plugin’s legibility. Consider:
// in class MyDemoPlugin
protected
$options = array(
'foo' => 'bar', // just a variable
'active' => 'yes', // another setting
'etc' => 'etc' // one more...
);
These options may then be accessed through the boilerplate’s get_option()
, update_option()
, and save_options()
functions:
// for example:
public function muddle_an_option() {
if ($this->get_option('foo') == 'bar') {
$this->update_option('foo', '!bar');
}
echo $this->get_option('foo'); // !bar
}
Properties:
$options
: (array) a name/value manifest describing the plugin’s default options. Readable by man, accessible to machine.
Methods:
get_option($key)
: (returns string) retrieve a stored settingsave_options()
: save the plugin’s current settingsupdate_option($key, $value)
: update a setting and save the resultupdate_options($instance = array())
: update all settings based on the values described in aninstance
array (e.g., $_POST after an admin form is submitted)
Putting it all together
A completed plugin derived from the boilerplate will end up looking something like this:
<?php
/*
Plugin Name: My Demo Plugin
Description: A plugin built on a nice friendly boilerplate
Version: 1.0
Author: RJ Zaworski
Author URI: http://rjzaworski.com
License: JSON
*/
if (!class_exists('MyDemoPlugin')):
require( 'includes/MY_Plugin.php' );
class MyDemoPlugin extends MY_Plugin {
protected
// the action hooks this plugin describes
$actions = array(
'widgets_init'
),
// the default options this plugin set/uses
$options = array(
'foo' => 'bar'
);
//
// Called manually to update the plugin's options
//
public function muddle_an_option() {
if ($this->get_option('foo') == 'bar') {
$this->update_option('foo', '!bar');
}
echo $this->get_option('foo'); // !bar
}
//
// Demonstrate loading a view
//
public function test_view() {
$data = array(
'message' => 'hello, world!'
);
$this->load_view('hello_world', $data);
}
//
// Called by the WP 'widgets_init' hook
//
public function widgets_init() {
$this->muddle_an_option();
$this->test_view();
}
}
new MyDemoPlugin();
endif; // class_exists
?>