Building a WordPress Series Plugin: Basic Functionality

Milestone 1

By Sol in Lab

With the basic specification for the plugin out of the way, it’s now time to dive in and start writing some code! When starting a new plugin, you may feel compelled to plan out the entire project to the smallest detail first, but it is often a much better idea to simply get started on actually making the plugin without delay. There will always be things you didn’t consider during the planning phase that will come up naturally during the development phase, so the sooner you can start writing code to vet your ideas, the better.

Setting up the plugin

Today’s goal was to implement the basic functionality of a series plugin, but first we need to set up the plugin.

Wrap it up

To avoid conflicts with external code (WordPress core, other plugins, etc.), it’s a good idea to wrap all of your plugin’s code in a class (or multiple classes). This isolates your code from external code and makes it far easier to manage. The plugin’s class names are also prefixed to avoid conflicts with external code.

/**
 * Model (Core functionality)
 * @package AR Series
 * @author Archetyped
 */
class ARS_Series_Model extends ARS_Base {

}

ARS_Base is a class containing core functionality from a custom framework that I use in all of my plugins. The nice thing about using a common framework is that improvements made to the framework while working on one plugin can be easily integrated into other plugins as well.

Next, we handle initialization of class instances:

/**
 * Constructor
 */ 
function __construct() {
    parent::__construct();

    //Init instance
    $this->init();
}

parent::__construct() and init() both come from the ARS_Base class and handle low-level initialization tasks such as internationalization, options setup, etc.

With initialization out of the way, we can start hooking into WordPress’ processes:

/**
 * Register hooks
 */
function register_hooks() {
    parent::register_hooks();

    /* Init */
    add_action('init', $this->m('setup'));
}

For now, I just need to hook into WordPress’ init action to perform some additional setup tasks (via the setup() method) once WordPress is fully loaded.

Note: The m() method in the above code is a simple utility method that creates a valid callback to an instance method. Instead of having to write…

//Callback for instance method
array( &$this, 'method_name' );

…for every single callback, I can simply write:

$this->m('method_name')

Much simpler!

Custom Taxonomies for $100, Alex

A series is basically a way to group posts, similar in some ways to tags and categories, WordPress’ built-in taxonomies. Therefore, using a custom taxonomy seemed like a natural fit for implementing series functionality.

Pro tip: WordPress has many APIs for implementing common functionality, and you should always use these built-in APIs whenever possible. Aside from saving you from having to write redundant code, it’s also guaranteed to be compatible with WordPress. It’s a win-win if a built-in API fits your requirements.

WordPress’ taxonomy API is fairly robust, but as previously noted, there are some definite differences between a series and WordPress’ built-in taxonomies (e.g. tags and categories), so some custom development will surely be needed. Nonetheless, I was interested to see how close we could get to what we needed using only WordPress’ taxonomy API .

The first thing we do in setup() is create a custom taxonomy using register_taxonomy:

public function setup() {
    //Create custom taxonomy
    register_taxonomy('series', 'post', array(
        'labels'        => array(
            'name'          => 'Series',
            'all_items'     => 'All Series',
            'edit_item'     => 'Edit Series',
            'update_item'   => 'Update Series',
            'add_new_item'  => 'Add New Series',
            'new_item_name' => 'New Series Name',
            'search_items'  => 'Search Series',
            'popular_items' => 'Popular Series',
        ),
        'public'        => true,
        'show_ui'       => true,
        'show_tagcloud' => false,
        'hierarchical'  => false,
        'show_admin_column' => true,
    ));
}

Pro tip: All methods called by actions/filters in WordPress need to be marked as public so that WordPress can execute them.

It almost seems too simple, but that’s really all that’s necessary to create a custom taxonomy in WordPress! Now we can create some series and add posts to them.

Series management UI

Series management UI

Series post editor UI

Series post editor UI

You’ll notice that the series assignment UI in the post editor looks strikingly similar to the tag assignment UI. That’s because they are essentially the same at this point. Setting hierarchical to false in register_taxonomy() creates a UI that looks like the tag assignment UI, while setting it true creates a UI like the category assignment UI.

We’ve arrived at the first point where custom development will be required to match the requirements for series functionality. Using the default post editor UI, it is possible to assign multiple series to a single post. However, per our requirements, a post should only be in a single series at once, so the post editor UI will need to be modified to accommodate this.

It’s not really an issue at the moment as we are simply “sketching” out the rough functionality right now. UI refinements like this will be a focus of a later stage.

Modifying Post Content

After a mere couple dozen lines of code, we have the basic functionality of a series plugin implemented. We can create new series and we can assign posts to those series. The next step is to add series information to posts when they are displayed. For this, we add a filter to the register_hooks() method:

add_filter('the_content', $this->m('update_content'));

Now a post’s content will be passed through update_content() prior to being displayed:

/**
 * Filters post content
 * @param string $content Post content to modify
 * @return string Modified post content
 */
public function update_content($content = '') {
    //Check if post in series
    $s = $this->get_series();
    if ( false !== $s ) {
        //Add series link/note
        $tpl = '<div class="ar-series-note">This post is part of <a href="%2$s">%1$s</a></div>';
        $note = sprintf($tpl, $s->name, get_term_link($s));
        $content = $note . $content;
        //Add series listing
        if ( !$this->has_shortcode($content) ) {
            $content = $this->add_shortcode($content);
        }
    }

    return $content;
}

The update_content() method makes use of WordPress’ built in taxonomy objects and functions to handle most of the heavy lifting. This is why it’s a great idea to use WordPress’ APIs whenever possible.

If the current post is part of a series, update_content() first adds some text above the content to tell visitors that this post is part of a larger series:

This post is part of Series Test

At the end of the post, a list of links is added to provide visitors with simple access to all of the other parts of a series. However, you may have noticed that the code above does not appear to do any sort of list building, and you would be right. Instead, add_shortcode() is used to insert a shortcode (e.g. [series]) into the post’s content that will be processed in a later step.

The series listing could have been abstracted in many different ways. A common list_series() method could have been created that was then referenced in both update_content() and the shortcode handler and would have probably worked just as well. However, since the post’s content will already be processed for shortcodes, it makes sense to simply add the shortcode to the content and let it get processed along with the other shortcodes.

You may have also noticed the use of has_shortcode() in the above code. Authors can choose to manually insert a series listing shortcode anywhere in the post’s content if desired. Therefore if a post already has a series shortcode in it, another one is not added to the end of the post.

Series Listing Shortcode

With the series shortcode placed (either manually or automatically) in the post’s content, we now need to register and process the shortcode. The shortcode is registered in setup():

add_shortcode('series', $this->m('handle_shortcode'));

This registers the handle_shortcode() method as the handler for the [series] shortcode.

/**
 * Processes shortcode in content
 * Builds listing of items in the same series as the current post
 * @see do_shortcode() for more information
 * @param array $atts Shortcode attributes
 * @return string Shortcode output
 */
public function handle_shortcode($atts) {
    //Build shortcode output
    $out = array();
    //Get current series
    $items = $this->get_items();
    $p = get_post();
    $fmt_link = '<a href="%2$s" title="%3$s">%1$s</a>';
    $fmt_curr = '<strong class="item-current">%1$s</strong>';
    foreach ( $items as $item ) {
        $title = get_the_title($item);
        if ( $item == $p ) {
            $out[] = sprintf($fmt_curr, $title);
        } else {
            $out[] = sprintf($fmt_link, $title, get_permalink($item), esc_attr($title));
        }
    }

    //Format output
    $out = ( !empty($out) ) ? '<div><ul><li>' . implode('</li><li>', $out) . '</li></ul></div>' : '';
    return $out;
}

This shortcode handler is pretty straightforward. It finds all posts in the same series as the current post and creates a list of links to each of them. To further improve usability, the current post is not linked (not necessary) and it is highlighted so that it easy for visitors to easily ascertain their current position in the series.

Now that the post’s content has been updated with series information, this is what we have at the end of day one:

Post content updated

Updated post content

Issues

Though we have the basic functionality of the series plugin implemented, there are a few issues that make a custom taxonomy less than ideal as the foundation for robust series management.

No limits

The first “issue” with custom taxonomies is that they are too flexible. While flexibility is usually a good thing, not being able to limit the number of series that can be added to a post presents a small problem that would need to be resolved with a bit of custom code. Swapping out the default series assignment UI in the post editor with a custom UI is thankfully pretty straightforward.

Maintaining Order

A bigger issue with taxonomies in WordPress is that they currently have no control over the order of posts. There has been a bit of talk about adding this functionality to WordPress at some point in the future, but for now, controlling the order of posts in a custom taxonomy would require a fair amount of custom code.

Rich Content

While posts enjoy the ability to create richly formatted content, custom taxonomies are currently restricted to simple plain-text descriptions. Furthermore, there is currently no facility for assigning custom metadata to taxonomies. Without a liberal dose of custom code (e.g. work), taxonomies cannot be easily extended and customized.

Onward

While WordPress’ custom taxonomies are fairly robust and are surely a perfect fit for many situations, it is pretty clear that it would require a lot of custom code to make taxonomies work for truly robust series management. Instead, I’m now looking toward custom post types as a more likely solution. Like custom taxonomies, WordPress’ custom post type API has become quite robust in recent updates and looks like a truly viable option for managing series (rich content, metadata, extensibility, etc.).

As a result, tomorrow I’ll be evaluating custom post types for managing series.