Francis Yaconiello

How to write a WordPress plugin

January 31, 2013

I've noticed that a bunch of the how-to-write-a-plugin articles out there focus on demonstrating the minimum amount of code needed to get a plugin going. Not many focus on good plugin structure or convention. This tutorial explains how to create a class based WordPress plugin that makes sense.

Start with a Directory

WordPress plugins can be defined in a single file or a directory of files. Starting with a directory gives you more flexibility and allows you to use common structures and folder layouts between projects and will add a level of maintainability.

Generally, I use the following folder tree:

  • wp-plugin-name/
  • wp-plugin-name.php --main plugin class
  • readme.txt --optional, describes the plugin for use in the wordpress.org plugin directory.
  • post-types/ --define each post type separately in this directory.
    • custom-post-type.php --each post type is defined separately.
  • templates/ --all of your admin side templates.
    • custom-post-type-inner-metabox.php --every custom post type that your plugin defines will need a inner-metabox template this keeps your presentation logic out of your business logic.
    • settings.php --this is the plugin settings page template.

wp-plugin-name.php

Every plugin begins with a docblock PHP comment describing the plugin. This comment describes the plugin and its licensing.

<?php
/*
Plugin Name: WP Plugin Template
Plugin URI: http://fyaconiello.github.com/wp-plugin-template
Description: A simple WordPress plugin template
Version: 1.0
Author: Francis Yaconiello
Author URI: http://www.yaconiello.com
License: GPL2
*/
/*
Copyright 2012  Francis Yaconiello  (email : francis@yaconiello.com)

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License, version 2, as 
published by the Free Software Foundation.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
*/

After the plugin definition comment, you need to define your main plugin logic. I prefer to write class based plugins. In my case, a class named WP_Plugin_Template. WP_Plugin_Template must have several methods. In the class constructor register all of the actions that your base plugin needs. There are also static class functions activate and deactivate that get called on plugin activation/deactivation. We'll be adding a bunch to this later, but at a very basic level, this is what your plugin class consists of.

<?php
if(!class_exists('WP_Plugin_Template'))
{
    class WP_Plugin_Template
    {
        /**
         * Construct the plugin object
         */
        public function __construct()
        {
            // register actions
        } // END public function __construct

        /**
         * Activate the plugin
         */
        public static function activate()
        {
            // Do nothing
        } // END public static function activate

        /**
         * Deactivate the plugin
         */     
        public static function deactivate()
        {
            // Do nothing
        } // END public static function deactivate
    } // END class WP_Plugin_Template
} // END if(!class_exists('WP_Plugin_Template'))

At the bottom of the plugin class register the activation/deactivation hooks and instantiate the plugin class by calling the constructor.

<?php
if(class_exists('WP_Plugin_Template'))
{
    // Installation and uninstallation hooks
    register_activation_hook(__FILE__, array('WP_Plugin_Template', 'activate'));
    register_deactivation_hook(__FILE__, array('WP_Plugin_Template', 'deactivate'));

    // instantiate the plugin class
    $wp_plugin_template = new WP_Plugin_Template();
}

Add a settings page

To add a settings page we need to start adding actions and callbacks to the main plugin class.

In your main plugin class's contructor function, public function __construct(), add the following code to hook into the admin_init and admin_menu actions.

<?php
add_action('admin_init', array(&$this, 'admin_init'));
add_action('admin_menu', array(&$this, 'add_menu'));

The above code calls the current class instance's admin_init and add_menu functions. In order for these callbacks to work, you need to add those functions:

<?php
/**
 * hook into WP's admin_init action hook
 */
public function admin_init()
{
    // Set up the settings for this plugin
    $this->init_settings();
    // Possibly do additional admin_init tasks
} // END public static function activate

When you register plugin specific settings, make sure to namespace them with your plugin name to avoid clashes with other plugins. see http://codex.wordpress.org/Function_Reference/register_setting

<?php
/**
 * Initialize some custom settings
 */     
public function init_settings()
{
    // register the settings for this plugin
    register_setting('wp_plugin_template-group', 'setting_a');
    register_setting('wp_plugin_template-group', 'setting_b');
} // END public function init_custom_settings()

Add your menu with a callback to a class function. Do not embed HTML into your menu page's callback function. Remember the directory structure I suggested above? put your settings HTML into a settings.php file in that directory. see http://codex.wordpress.org/Function_Reference/add_options_page

<?php
/**
 * add a menu
 */     
public function add_menu()
{
    add_options_page('WP Plugin Template Settings', 'WP Plugin Template', 'manage_options', 'wp_plugin_template', array(&$this, 'plugin_settings_page'));
} // END public function add_menu()

/**
 * Menu Callback
 */     
public function plugin_settings_page()
{
    if(!current_user_can('manage_options'))
    {
        wp_die(__('You do not have sufficient permissions to access this page.'));
    }

    // Render the settings template
    include(sprintf("%s/templates/settings.php", dirname(__FILE__)));
} // END public function plugin_settings_page()

In your settings.php template file use the name-spaced settings group name that you created above as arguments for several of the functions. Those functions handle all validation and storage for this settings page. see http://codex.wordpress.org/Function_Reference/settings_fields and http://codex.wordpress.org/Function_Reference/do_settings_fields

<div class="wrap">
    <h2>WP Plugin Template</h2>
    <form method="post" action="options.php"> 
        <?php @settings_fields('wp_plugin_template-group'); ?>
        <?php @do_settings_fields('wp_plugin_template-group'); ?>

        <table class="form-table">  
            <tr valign="top">
                <th scope="row"><label for="setting_a">Setting A</label></th>
                <td><input type="text" name="setting_a" id="setting_a" value="<?php echo get_option('setting_a'); ?>" /></td>
            </tr>
            <tr valign="top">
                <th scope="row"><label for="setting_b">Setting B</label></th>
                <td><input type="text" name="setting_b" id="setting_b" value="<?php echo get_option('setting_b'); ?>" /></td>
            </tr>
        </table>

        <?php @submit_button(); ?>
    </form>
</div>

Lastly, to add a settings link next to the plugin's activate/deactivate link in the Plugin Management list add the following to the bottom of your main plugin file directly after the $wp_plugin_template = new WP_Plugin_Template(); line that instantiates your plugin class.

<?php
// Add a link to the settings page onto the plugin page
if(isset($wp_plugin_template))
{
    // Add the settings link to the plugins page
    function plugin_settings_link($links)
    { 
        $settings_link = '<a href="options-general.php?page=wp_plugin_template">Settings</a>'; 
        array_unshift($links, $settings_link); 
        return $links; 
    }

    $plugin = plugin_basename(__FILE__); 
    add_filter("plugin_action_links_$plugin", 'plugin_settings_link');
}

Add a custom post type

For every custom post type you want to add to your plugin you need to define a post type class. For this example, wp-plugin-name/post-types/post_type_template.php.

In this class add a constant POST_TYPE this is the name that your post type will be registered as. Also, add a private array $_meta this is an array of all the post types additional metafield names.

<?php
if(!class_exists('PostTypeTemplate'))
{
    /**
     * A PostTypeTemplate class that provides 3 additional meta fields
     */
    class PostTypeTemplate
    {
        const POST_TYPE = "post-type-template";
        private $_meta  = array(
            'meta_a',
            'meta_b',
            'meta_c',
        );
    } // END class PostTypeTemplate
} // END if(!class_exists('PostTypeTemplate'))

This class needs a constructor to set up the initial action hooks.

<?php
/**
 * The Constructor
 */
public function __construct()
{
    // register actions
    add_action('init', array(&$this, 'init'));
    add_action('admin_init', array(&$this, 'admin_init'));
} // END public function __construct()

In our post-type class's init() function, call a function to create the post type, and set up a custom function to hook into save_post. see http://codex.wordpress.org/Function_Reference/register_post_type and http://codex.wordpress.org/Plugin_API/Action_Reference/save_post and http://codex.wordpress.org/Function_Reference/update_post_meta

<?php
/**
 * hook into WP's init action hook
 */
public function init()
{
    // Initialize Post Type
    $this->create_post_type();
    add_action('save_post', array(&$this, 'save_post'));
} // END public function init()

/**
 * Create the post type
 */
public function create_post_type()
{
    register_post_type(self::POST_TYPE,
        array(
            'labels' => array(
                'name' => __(sprintf('%ss', ucwords(str_replace("_", " ", self::POST_TYPE)))),
                'singular_name' => __(ucwords(str_replace("_", " ", self::POST_TYPE)))
            ),
            'public' => true,
            'has_archive' => true,
            'description' => __("This is a sample post type meant only to illustrate a preferred structure of plugin development"),
            'supports' => array(
                'title', 'editor', 'excerpt', 
            ),
        )
    );
}

/**
 * Save the metaboxes for this custom post type
 */
public function save_post($post_id)
{
    // verify if this is an auto save routine. 
    // If it is our form has not been submitted, so we dont want to do anything
    if(defined('DOING_AUTOSAVE') && DOING_AUTOSAVE)
    {
        return;
    }

    if(isset($_POST['post_type']) && $_POST['post_type'] == self::POST_TYPE && current_user_can('edit_post', $post_id))
    {
        foreach($this->_meta as $field_name)
        {
            // Update the post's meta field
            update_post_meta($post_id, $field_name, $_POST[$field_name]);
        }
    }
    else
    {
        return;
    } // if($_POST['post_type'] == self::POST_TYPE && current_user_can('edit_post', $post_id))
} // END public function save_post($post_id)

Add metaboxes to our custom post type's admin pages on admin_init call.

<?php
/**
 * hook into WP's admin_init action hook
 */
public function admin_init()
{           
    // Add metaboxes
    add_action('add_meta_boxes', array(&$this, 'add_meta_boxes'));
} // END public function admin_init()

/**
 * hook into WP's add_meta_boxes action hook
 */
public function add_meta_boxes()
{
    // Add this metabox to every selected post
    add_meta_box( 
        sprintf('wp_plugin_template_%s_section', self::POST_TYPE),
        sprintf('%s Information', ucwords(str_replace("_", " ", self::POST_TYPE))),
        array(&$this, 'add_inner_meta_boxes'),
        self::POST_TYPE
    );                  
} // END public function add_meta_boxes()

Our post type's inner meta box includes a template file to keep metabox templating out of the business logic.

<?php
/**
 * called off of the add meta box
 */     
public function add_inner_meta_boxes($post)
{       
    // Render the job order metabox
    include(sprintf("%s/../templates/%s_metabox.php", dirname(__FILE__), self::POST_TYPE));         
} // END public function add_inner_meta_boxes($post)

The template for include(sprintf("%s/../templates/%s_metabox.php", dirname(__FILE__), self::POST_TYPE)); in my case templates/post-type-template_metabox.php should look something like the following. Note: each of your posts meta fields should be represented here.

<table> 
    <tr valign="top">
        <th class="metabox_label_column">
            <label for="meta_a">Meta A</label>
        </th>
        <td>
            <input type="text" id="meta_a" name="meta_a" value="<?php echo @get_post_meta($post->ID, 'meta_a', true); ?>" />
        </td>
    </tr>
    <tr valign="top">
        <th class="metabox_label_column">
            <label for="meta_a">Meta B</label>
        </th>
        <td>
            <input type="text" id="meta_b" name="meta_b" value="<?php echo @get_post_meta($post->ID, 'meta_b', true); ?>" />
        </td>
    </tr>
    <tr valign="top">
        <th class="metabox_label_column">
            <label for="meta_a">Meta C</label>
        </th>
        <td>
            <input type="text" id="meta_c" name="meta_c" value="<?php echo @get_post_meta($post->ID, 'meta_c', true); ?>" />
        </td>
    </tr>                
</table>

Lastly, go back to the main plugin class's constructor. Before the end of the function call the following to incorporate your post type into the plugin.

<?php
require_once(sprintf("%s/post-types/post_type_template.php", dirname(__FILE__)));
$PostTypeTemplate = new PostTypeTemplate();

Congrats! you have created a class based WordPress plugin that does the following:

  1. Has a settings page
  2. Creates a custom post type with metaboxes
  3. Separates business and template logic

Download all of the codes here: https://github.com/fyaconiello/wp_plugin_template

Code Tutorials WordPress


comments powered by Disqus