Best of Wptuts 2011: Every week through January, we're going to revisit some of our favorite posts from 2011. Plugin development can often feel like the wild west if you're creating something from scratch without a boilerplate or a similar plugin to work from - Tom's 2 part series on Maintainable WordPress Widgets/Plugins offers some practical guidelines that should keep you on the tracks!
When it comes to software development, frameworks and libraries are popular because they're helpful, right? They provide a consistent way to write and organize code in hopes of making development and maintenance as easy as possible.
The thing is, these very same principles that apply to larger, enterprise level systems are just as applicable to smaller projects - such as WordPress plugins - developed by a small teams. And just as larger systems are full of moving parts, such is the case with WordPress plugins.
For example: you've got the core code that is responsible for communicating with WordPress (via filters, actions, and hooks), administration dashboards, client-side views, JavaScript files, style sheets, localization files, and so on all of which are achieved using at least four different programming languages.
During my time spent in WordPress Development, I've created a few boilerplates that I use to begin each of my projects. This tutorial will take a look at my WordPress Widget Boilerplate code, how to leverage it in new projects, and an example application in hopes of helping you get your next WordPress project off to a solid start.
A Widget Boilerplate
Organization
When it comes to development, I typically try to keep things as simple as possible by planning only for the necessary features; however, this is one case in which I aim to be exhaustive. It's almost always easier to begin planning a boilerplate when you know all of the components that may go into the system.
A plugin can ultimately consist of the following:
- Core plugin code
- Style sheets
- Java Scripts
- Localization files
- Markup
- Images
Taking all of the above into consideration, the widget boilerplate directory is laid out like this:
We'll take a look at each directory in detail later in the article.
The Skeleton
In addition to file organization, I also like to stub out the code used to drive the widget. The WordPress Codex[1] has a detailed explanation of the Widget API[2] and because there is a suggest way to craft them, I try to follow it.
Additionally, I'm a fan of writing my code in an object-oriented manner along with code comments to help explain what's going on in each area of the code. As such, the initial widget code looks like this:
<?php /* Plugin Name: TODO Plugin URI: TODO Description: TODO Version: 1.0 Author: TODO Author URI: TODO Author Email: TODO License: Copyright 2011 TODO ([email protected]) 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 */ // TODO: change 'Plugin_Name' to the name of your actual plugin class Plugin_Name extends WP_Widget { /*--------------------------------------------------*/ /* Constructor /*--------------------------------------------------*/ /** * The widget constructor. Specifies the classname and description, instantiates * the widget, loads localization files, and includes necessary scripts and * styles. */ // TODO: This should match the title given in the class definition above. function Plugin_Name() { // Define constnats used throughout the plugin $this->init_plugin_constants(); // TODO: update classname and description $widget_opts = array ( 'classname' => PLUGIN_NAME, 'description' => __('Short description of the plugin goes here.', PLUGIN_LOCALE) ); $this->WP_Widget(PLUGIN_SLUG, __(PLUGIN_NAME, PLUGIN_LOCALE), $widget_opts); load_plugin_textdomain(PLUGIN_LOCALE, false, dirname(plugin_basename( __FILE__ ) ) . '/lang/' ); // Load JavaScript and stylesheets $this->register_scripts_and_styles(); } // end constructor /*--------------------------------------------------*/ /* API Functions /*--------------------------------------------------*/ /** * Outputs the content of the widget. * * @args The array of form elements * @instance */ function widget($args, $instance) { extract($args, EXTR_SKIP); echo $before_widget; // TODO: This is where you retrieve the widget values // Display the widget include(WP_PLUGIN_DIR . '/' . PLUGIN_SLUG . '/views/widget.php'); echo $after_widget; } // end widget /** * Processes the widget's options to be saved. * * @new_instance The previous instance of values before the update. * @old_instance The new instance of values to be generated via the update. */ function update($new_instance, $old_instance) { $instance = $old_instance; // TODO Update the widget with the new values return $instance; } // end widget /** * Generates the administration form for the widget. * * @instance The array of keys and values for the widget. */ function form($instance) { // TODO define default values for your variables $instance = wp_parse_args( (array)$instance, array( '' => '' ) ); // TODO store the values of widget in a variable // Display the admin form include(WP_PLUGIN_DIR . '/' . PLUGIN_SLUG . '/views/admin.php'); } // end form /*--------------------------------------------------*/ /* Private Functions /*--------------------------------------------------*/ /** * Initializes constants used for convenience throughout * the plugin. */ private function init_plugin_constants() { /* TODO * * This provides the unique identifier for your plugin used in * localizing the strings used throughout. * * For example: wordpress-widget-boilerplate-locale. */ if(!defined('PLUGIN_LOCALE')) { define('PLUGIN_LOCALE', 'plugin-name-locale'); } // end if /* TODO * * Define this as the name of your plugin. This is what shows * in the Widgets area of WordPress. * * For example: WordPress Widget Boilerplate. */ if(!defined('PLUGIN_NAME')) { define('PLUGIN_NAME', 'Plugin Name'); } // end if /* TODO * * this is the slug of your plugin used in initializing it with * the WordPress API. * This should also be the * directory in which your plugin resides. Use hyphens. * * For example: wordpress-widget-boilerplate */ if(!defined('PLUGIN_SLUG')) { define('PLUGIN_SLUG', 'plugin-name-slug'); } // end if } // end init_plugin_constants /** * Registers and enqueues stylesheets for the administration panel and the * public facing site. */ private function register_scripts_and_styles() { if(is_admin()) { $this->load_file(PLUGIN_NAME, '/' . PLUGIN_SLUG . '/js/admin.js', true); $this->load_file(PLUGIN_NAME, '/' . PLUGIN_SLUG . '/css/admin.css'); } else { $this->load_file(PLUGIN_NAME, '/' . PLUGIN_SLUG . '/js/admin.css', true); $this->load_file(PLUGIN_NAME, '/' . PLUGIN_SLUG . '/css/widget.css'); } // end if/else } // end register_scripts_and_styles /** * Helper function for registering and enqueueing scripts and styles. * * @name The ID to register with WordPress * @file_path The path to the actual file * @is_script Optional argument for if the incoming file_path is a JavaScript source file. */ private function load_file($name, $file_path, $is_script = false) { $url = WP_PLUGIN_URL . $file_path; $file = WP_PLUGIN_DIR . $file_path; if(file_exists($file)) { if($is_script) { wp_register_script($name, $url); wp_enqueue_script($name); } else { wp_register_style($name, $url); wp_enqueue_style($name); } // end if } // end if } // end load_file } // end class add_action('widgets_init', create_function('', 'register_widget("Plugin_Name");')); // TODO remember to change this to match the class definition above ?>
Notice there are a number of TODO's throughout the code. These are useful especially in the context of writing your code on top of the boilerplate.
Note that there are three primary sections of code, as well:
- Constructor. This function is responsible for initializing the widget, importing localizing files, and including JavaScript sources and style sheets.
- API Functions. These functions are the three functions required for administering, displaying, and updating the widget.
- Helper Functions. These are private functions that I use to help with often repetitive or required tasks.
The three most important functions above, the API functions are required for developing your plugin.
- widget() extracts the stored values and rendering the public view
- update() is responsible for updating the previously saved values with the values provided by the user
- form() renders the administration form and provides functionality necessary for storing new values.
Because plugins are often divided between the administration functionality and the client-facing functionality, I divide my JavaScript source, style sheets, and HTML accordingly. I name these files accordingly and stub them out appropriately:
JavaScript Sources:
admin.js:
jQuery(function($) { // Place your administration-specific code here });
widget.js:
jQuery(function($) { // Place your public facing JavaScript here });
Style Sheets:
admin.css:
/* This style sheet is used to style the admin option form of the widget. */
widget.css:
/* This style sheet is used to style the public view of the widget. */
Views:
<!-- This file is used to markup the administration form of the widget. --> <!-- This file is used to markup the public facing widget. -->
Easy, right? You can view (and fork!) this entire boilerplate including the localization files and the README on GitHub.
There's now a place for everything and when it comes time to ship, you just exclude certain files from the final build..
A Working Example With Your Social Networks
When it comes to programming, practice helps in learning a new language or tip so here's a quick example of how to use the above boilerplate to create a simple widget for making it easy to share your Twitter, Facebook, and Google+ links.
First, we'll list out the requirements:
- An administration view for entering values. This includes markup and styles.
- A public facing view for displaying links to social networks. This also includes markup and styles.
- Options for storing a Twitter username, Facebook username, and Google+ ID
Secondly, let's open up the boilerplate and begin stubbing out the necessary parts.
First, we define out plugin name, slug, and locale values. These are used repeatedly throughout the code so it's nice to store them as constants for easily retrieving them. Locate the init_plugin_constants() function and make sure that your code looks like this:
private function init_plugin_constants() { if(!defined('PLUGIN_LOCALE')) { define('PLUGIN_LOCALE', 'my-social-network-locale'); } // end if if(!defined('PLUGIN_NAME')) { define('PLUGIN_NAME', 'My Social Networks'); } // end if if(!defined('PLUGIN_SLUG')) { define('PLUGIN_SLUG', 'My-Social-Networks'); } // end if } // end init_plugin_constants
After that, we need to prepare the constructor:
function My_Social_Network() { // Define constants used throughout the plugin $this->init_plugin_constants(); $widget_opts = array ( 'classname' => PLUGIN_NAME, 'description' => __('A simple WordPress widget for sharing a few of your social networks.', PLUGIN_LOCALE) ); $this->WP_Widget(PLUGIN_SLUG, __(PLUGIN_NAME, PLUGIN_LOCALE), $widget_opts); load_plugin_textdomain(PLUGIN_LOCALE, false, dirname(plugin_basename( __FILE__ ) ) . '/lang/' ); // Load JavaScript and stylesheets $this->register_scripts_and_styles(); } // end constructor
And stub out the API functions:
function widget($args, $instance) { extract($args, EXTR_SKIP); echo $before_widget; $twitter_username = empty($instance['twitter_username']) ? '' : apply_filters('twitter_username', $instance['twitter_username']); $facebook_username = empty($instance['facebook_username']) ? '' : apply_filters('facebook_username', $instance['facebook_username']); $google_plus_id = empty($instance['google_plus_id']) ? '' : apply_filters('google_plus_id', $instance['google_plus_id']); // Display the widget include(WP_PLUGIN_DIR . '/' . PLUGIN_SLUG . '/views/widget.php'); echo $after_widget; } // end widget function update($new_instance, $old_instance) { $instance = $old_instance; $instance['twitter_username'] = strip_tags(stripslashes($new_instance['twitter_username'])); $instance['facebook_username'] = strip_tags(stripslashes($new_instance['facebook_username'])); $instance['google_plus_id'] = strip_tags(stripslashes($new_instance['google_plus_id'])); return $instance; } // end widget function form($instance) { $instance = wp_parse_args( (array)$instance, array( 'twitter_username' => '', 'facebook_username' => '', 'google_plus_id' => '' ) ); $twitter_username = strip_tags(stripslashes($new_instance['twitter_username'])); $facebook_username = strip_tags(stripslashes($new_instance['facebook_username'])); $google_plus_id = strip_tags(stripslashes($new_instance['google_plus_id'])); // Display the admin form include(WP_PLUGIN_DIR . '/' . PLUGIN_SLUG . '/views/admin.php'); } // end form
The final version of the plugin should look like this:
<?php /* Plugin Name: My Social Network Plugin URI: http://github.com/tommcfarlin/My-Social-Network Description: A simple WordPress widget for sharing a few of your social networks. Version: 1.0 Author: Tom McFarlin Author URI: http://tommcfarlin.com Author Email: [email protected] License: Copyright 2011 My Social Network ([email protected]) 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 */ class My_Social_Network extends WP_Widget { /*--------------------------------------------------*/ /* Constructor /*--------------------------------------------------*/ /** * The widget constructor. Specifies the classname and description, instantiates * the widget, loads localization files, and includes necessary scripts and * styles. */ function My_Social_Network() { // Define constants used throughout the plugin $this->init_plugin_constants(); $widget_opts = array ( 'classname' => PLUGIN_NAME, 'description' => __('A simple WordPress widget for sharing a few of your social networks.', PLUGIN_LOCALE) ); $this->WP_Widget(PLUGIN_SLUG, __(PLUGIN_NAME, PLUGIN_LOCALE), $widget_opts); load_plugin_textdomain(PLUGIN_LOCALE, false, dirname(plugin_basename( __FILE__ ) ) . '/lang/' ); // Load JavaScript and stylesheets $this->register_scripts_and_styles(); } // end constructor /*--------------------------------------------------*/ /* API Functions /*--------------------------------------------------*/ /** * Outputs the content of the widget. * * @args The array of form elements * @instance */ function widget($args, $instance) { extract($args, EXTR_SKIP); echo $before_widget; $twitter_username = empty($instance['twitter_username']) ? '' : apply_filters('twitter_username', $instance['twitter_username']); $facebook_username = empty($instance['facebook_username']) ? '' : apply_filters('facebook_username', $instance['facebook_username']); $google_plus_id = empty($instance['google_plus_id']) ? '' : apply_filters('google_plus_id', $instance['google_plus_id']); // Display the widget include(WP_PLUGIN_DIR . '/' . PLUGIN_SLUG . '/views/widget.php'); echo $after_widget; } // end widget /** * Processes the widget's options to be saved. * * @new_instance The previous instance of values before the update. * @old_instance The new instance of values to be generated via the update. */ function update($new_instance, $old_instance) { $instance = $old_instance; $instance['twitter_username'] = strip_tags(stripslashes($new_instance['twitter_username'])); $instance['facebook_username'] = strip_tags(stripslashes($new_instance['facebook_username'])); $instance['google_plus_id'] = strip_tags(stripslashes($new_instance['google_plus_id'])); return $instance; } // end widget /** * Generates the administration form for the widget. * * @instance The array of keys and values for the widget. */ function form($instance) { $instance = wp_parse_args( (array)$instance, array( 'twitter_username' => '', 'facebook_username' => '', 'google_plus_id' => '' ) ); $twitter_username = strip_tags(stripslashes($new_instance['twitter_username'])); $facebook_username = strip_tags(stripslashes($new_instance['facebook_username'])); $google_plus_id = strip_tags(stripslashes($new_instance['google_plus_id'])); // Display the admin form include(WP_PLUGIN_DIR . '/' . PLUGIN_SLUG . '/views/admin.php'); } // end form /*--------------------------------------------------*/ /* Private Functions /*--------------------------------------------------*/ /** * Initializes constants used for convenience throughout * the plugin. */ private function init_plugin_constants() { if(!defined('PLUGIN_LOCALE')) { define('PLUGIN_LOCALE', 'my-social-network-locale'); } // end if if(!defined('PLUGIN_NAME')) { define('PLUGIN_NAME', 'My Social Networks'); } // end if if(!defined('PLUGIN_SLUG')) { define('PLUGIN_SLUG', 'My-Social-Networks'); } // end if } // end init_plugin_constants /** * Registers and enqueues stylesheets for the administration panel and the * public facing site. */ private function register_scripts_and_styles() { if(is_admin()) { $this->load_file(PLUGIN_NAME, '/' . PLUGIN_SLUG . '/js/admin.js', true); $this->load_file(PLUGIN_NAME, '/' . PLUGIN_SLUG . '/css/admin.css'); } else { $this->load_file(PLUGIN_NAME, '/' . PLUGIN_SLUG . '/js/admin.css', true); $this->load_file(PLUGIN_NAME, '/' . PLUGIN_SLUG . '/css/widget.css'); } // end if/else } // end register_scripts_and_styles /** * Helper function for registering and enqueueing scripts and styles. * * @name The ID to register with WordPress * @file_path The path to the actual file * @is_script Optional argument for if the incoming file_path is a JavaScript source file. */ private function load_file($name, $file_path, $is_script = false) { $url = WP_PLUGIN_URL . $file_path; $file = WP_PLUGIN_DIR . $file_path; if(file_exists($file)) { if($is_script) { wp_register_script($name, $url); wp_enqueue_script($name); } else { wp_register_style($name, $url); wp_enqueue_style($name); } // end if } // end if } // end load_file } // end class add_action('widgets_init', create_function('', 'register_widget("My_Social_Network");')); ?>
Next, let's add some styles to the administration form. Locate /css/admin.css and add the following code:
.wrapper fieldset { border: 1px solid #ddd; width: 90%; padding: 5%; } .option { margin: 12px 0 12px 0; } .option input { width: 100%; }
And let's write the markup that will render the view of the administration form:
<div class="wrapper"> <fieldset> <legend> <?php _e('My Social Networks', PLUGIN_LOCALE); ?> </legend> <div class="option"> <label for="twitter"> <?php _e('Twitter Username', PLUGIN_LOCALE); ?> </label> <input type="text" id="<?php echo $this->get_field_id('twitter_username'); ?>" name="<?php echo $this->get_field_name('twitter_username'); ?>" value="<?php echo $instance['twitter_username']; ?>" class="" /> </div> <div class="option"> <label for="facebook"> <?php _e('Facebook Username', PLUGIN_LOCALE); ?> </label> <input type="text" id="<?php echo $this->get_field_id('facebook_username'); ?>" name="<?php echo $this->get_field_name('facebook_username'); ?>" value="<?php echo $instance['facebook_username']; ?>" class="" /> </div> <div class="option"> <label for="google_plus"> <?php _e('Google+ ID', PLUGIN_LOCALE); ?> </label> <input type="text" id="<?php echo $this->get_field_id('google_plus_id'); ?>" name="<?php echo $this->get_field_name('google_plus_id'); ?>" value="<?php echo $instance['google_plus_id']; ?>" class="" /> </div> </fieldset> </div><!-- /wrapper -->
Finally, we need to write some markup to render the public-facing view of the widget when it's live on the actual blog:
<h3> <?php _e('My Social Networks', PLUGIN_LOCALE); ?> </h3> <ul class="my-social-networks"> <?php if(strlen(trim($twitter_username)) > 0) { ?> <li> <a href="http://twitter.com/<?php echo $twitter_username; ?>"> <?php _e('Twitter', PLUGIN_LOCALE); ?> </a> </li> <?php } // end if ?> <?php if(strlen(trim($facebook_username)) > 0) { ?> <li> <a href="http://facebook.com/<?php echo $facebook_username; ?>"> <?php _e('Facebook', PLUGIN_LOCALE); ?> </a> </li> <?php } // end if ?> <?php if(strlen(trim($google_plus_id)) > 0) { ?> <li> <a href="http://plus.google.com/<?php echo $google_plus_id; ?>"> <?php _e('Google+', PLUGIN_LOCALE); ?> </a> </li> <?php } // end if ?> </ul><!-- /my-social-networks -->
Done and done. Not bad, huh? A fair amount of work and functionality done relatively quickly.
You can download the working source code (including an associated README) for this widget on GitHub or right here at Wptuts.
Ultimately, maintaining a software projects amounts to trying to organize complexity. Although the above boilerplate is not *the* way the organize or manage code, it's an *effective* way to organize code and I've found is extremely helpful in many of my projects and hopefully it helps you with your future work.
Remember, you can grab a copy of both the boilerplate and the example project from their respective GitHub repositories. I also highly recommend bookmarking the WordPress Codex[1]. It's a tremendous resource for anyone seeking to do advanced WordPress development.
Moving on to Part Two...
Check out the second part of this tutorial series where we'll be digging deeper into creating maintainable plugins! We'll be looking at how to use hooks
within WordPress - and then we'll actually put our boilerplate to use to create another useful plugin. Ready for Part Two?
Comments