slushman
Started using WordPress in 2007 with WordPress.com.
Self-hosted WordPress in 2008 and wrote an eBook to help musicians build their own sites.
Started developing for WordPress in 2011.
Published 3 plugins: ArtistDataPress, BuddyPress Profile Widgets, and BuddyBar
DCC Marketing
- Decatur, IL
- dccmarketing.com
6 months ago, my family and I moved to my wife's family farm near Decatur, IL
Lead developer
Full-service, boutique marketing agency
Based in Decatur, IL with an office in Chicago
WordPress Plugin Boilerplate
- Tom McFarlin
- Devin Vinson
Brief history
Written by Tom McFarlin of Atlanta, GA in 2011
As of March 4, 2015, taken over by Devin Vinson of Tampa Bay, FL
Why Use It?
- Keeps files organized
- WordPress Coding Standards
- WordPress Documentation Standards
- WordPress APIs
- Translatable
Like building a house, start with a good foundation:
Organization helps with maintainability
WP Coding standards
WP documentation standards
Uses WP APIs
Starts with a blank .pot to encourage developers to add internationalization to their plugins
What's New?
- Completely rewritten
- No more singleton
- New Structure
- https://wppb.io
The entire project was rewritten from scratch using feedback and contributions fro several other developers
In this rewrite, Tom moved away from using the singleton pattern, which is hotly debated practice among developers
The plugin features a new structure (which we'll go over in more detail)
And the biggest new feature is wppb.io, which serves as the homepage for the project and will house all the documentation coming soon
Structure
Comes ready for WP plugin directory with Markdown docs for README and ChangeLog at the root
The assets folder with an example icon, screenshot, and banner image files for the plugin directory.
Inside the trunk folder...
Admin & Public
Both the admin and public classes have identical structures.
Each has a CSS folder for the related styled
A Javascript folder for any scripts
and a Partials folder for HTML views
The main file in each folder contains all the code needed for that section. We'll go into details of each later.
Includes
Holds all the operational files. This is where the magic happens.
But seriously, thee are the core of your future plugin.
Before we got into all the bits and pieces, let me give you some advice:
Use a Generator
https://wppb.me
Seriously, save yourself a ton of time by using this generator. It takes care of renaming everything for you and prevents alot of troubleshooting from typos.>
plugin-name.php
/**
* @link https://example.com
* @since 1.0.0
* @package Plugin_Name
*
* @wordpress-plugin
* Plugin Name: WordPress Plugin Boilerplate
* Plugin URI: https://example.com/plugin-name-uri/
* Description: This is a short description of what the plugin does.
* Version: 1.0.0
* Author: Your Name or Your Company
* Author URI: https://example.com/
* License: GPL-2.0+
* License URI: https://www.gnu.org/licenses/gpl-2.0.txt
* Text Domain: plugin-name
* Domain Path: /languages
*/
Like any WordPress plugin, it starts with comments describing what it does, who authored it, the current version, etc.
Boilerplate also uses the PHPDoc versions of those same properties.
plugin-name.php
if (!defined( 'WPINC')) { die; }
function activate_plugin_name() {
require_once plugin_dir_path( __FILE__ ) . 'includes/class-plugin-name-activator.php';
Plugin_Name_Activator::activate();
}
function deactivate_plugin_name() {
require_once plugin_dir_path(__FILE__) . 'includes/class-plugin-name-deactivator.php';
Plugin_Name_Deactivator::deactivate();
}
register_activation_hook(__FILE__, 'activate_plugin_name');
register_deactivation_hook(__FILE__, 'deactivate_plugin_name');
register_activation & register_deactivation
Both classes are blank
plugin-name.php
require plugin_dir_path(__FILE__) . 'includes/class-plugin-name.php';
function run_plugin_name() {
$plugin = new Plugin_Name();
$plugin->run();
}
run_plugin_name();
At the bottom of this file, We load up the primary plugin class and call the run method.
Plugin Class
See Full Code
The plugin class is the core of the plugin. Here's where everything ties together. Let's take a look.
Plugin Class
Constructor
public function __construct() {
$this->plugin_name = 'plugin-name';
$this->version = '1.0.0';
$this->load_dependencies();
$this->set_locale();
$this->define_admin_hooks();
$this->define_public_hooks();
}
The constructors sets the class variables, the plugin_name is used for i18n, the version is used for cache busting scripts and stylesheets.
Then calls these four methods:
Plugin Class
Load Dependencies
private function load_dependencies() {
require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-plugin-name-loader.php';
require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-plugin-name-i18n.php';
require_once plugin_dir_path(dirname(__FILE__)) . 'admin/class-plugin-name-admin.php';
require_once plugin_dir_path(dirname(__FILE__)) . 'public/class-plugin-name-public.php';
$this->loader = new Plugin_Name_Loader();
}
Load dependencies brings in all the other classes used by the plugin class. It also instantiates the Loader class.
Plugin Class
i18n
private function set_locale() {
$plugin_i18n = new Plugin_Name_i18n();
$plugin_i18n->set_domain($this->get_plugin_name());
$this->loader->add_action('plugins_loaded', $plugin_i18n, 'load_plugin_textdomain');
}
The set_locale method uses the WordPress APIs to set the textdomain for the plugin for i18n.
Plugin Class
Hooks
private function define_admin_hooks() {
$plugin_admin = new Plugin_Name_Admin(
$this->get_plugin_name(), $this->get_version()
);
$this->loader->add_action(
'admin_enqueue_scripts', $plugin_admin, 'enqueue_styles'
);
$this->loader->add_action(
'admin_enqueue_scripts', $plugin_admin, 'enqueue_scripts'
);
}
There are two methods where the WordPress hooks and filters are setup. This is the admin hooks method, the public-facing method is exactly the same, just referencing different hooks.
This is one of the more interesting parts of the 3.0 rewrite. It instantiates the Admin class, then you can see it calls the add_action method with the loader class.
The add_action method is really just a wrapper for a standard add_action like anywhere else in WordPress. Basically, its gathering all those calls and running them all at once. This is part of keeping your plugin organized and providing a stable, consistant structure.
Loader Class
Constructor
public function __construct() {
$this->actions = array();
$this->filters = array();
}
The constructor sets the actions and filters class variables as blank arrays.
Loader Class
Add Action
public function add_action($hook, $component, $callback, $priority = 10, $accepted_args = 1) {
$this->actions = $this->add(
$this->actions,
$hook,
$component,
$callback,
$priority,
$accepted_args
);
}
They work the same way: they take the arguments from your call in the plugin class and load each one into their respective arrays.
They use a helper method call "Add", which simply adds a new subarray to either the actions array.
There's also an add_filter method, which appears exactly the same way.
Those are processed in the run method.
Loader Class
Run Method
public function run() {
foreach ($this->actions as $hook) {
add_action(
$hook['hook'],
array($hook['component'], $hook['callback']),
$hook['priority'],
$hook['accepted_args']
);
}
}
These are simple foreach loops where each subarray uses the standard add_action WordPress function to setup all the various bits and piece of your plugin.
There's also a loop for adding filters, I ran out of room on the slide, but it works exactly the same way.
That's the loader class.
Admin & Public Classes
Constructor
public function __construct($plugin_name, $version) {
$this->plugin_name = $plugin_name;
$this->version = $version;
}
The admin and public classes are structured the same and have the same methods, but keep the pulic-facing code and admin-facing code separated.
The constructor just sets the i18n and version class variables.
Admin & Public Classes
Enqueue Styles
public function enqueue_styles() {
wp_enqueue_style(
$this->plugin_name,
plugin_dir_url(__FILE__) . 'css/plugin-name-admin.css',
array(),
$this->version,
'all'
);
}
Then they have added two example methods within each class.
The first is enqueue_styles, which enqueues the sample CSS file.
Admin & Public Classes
Enqueue Scripts
public function enqueue_scripts() {
wp_enqueue_script(
$this->plugin_name,
plugin_dir_url(__FILE__) . 'js/plugin-name-admin.js',
array( 'jquery' ),
$this->version,
false
);
}
Enqueue scripts works the same basic way, calling the WordPress function to enqueue a script using the sample script file included with the boilerplate.
Other Files
- index.php files
- Languages Folder & blank .pot file
- license.txt
- README.txt
- uninstall.php
index.php files - security measure
languages folder & blank pot file
license.txt - copy of GPL2 license
readme - text for the plugin directory
uninstall.php - blank
Examples
- Enqueue styles and scripts
- Custom Post Type
- Taxonomy
- Plugin Settings
- Metaboxes
- Shortcode
- Displays/Views
- Widgets
I wrote up an example plugin to show how this stuff works in practice. While explaining it might help you, seeing working code helps more.
Custom Post Type
Plugin Class
private function define_admin_hooks() {
$plugin_admin = new Now_Hiring_Admin(
$this->get_i18n(),
$this->get_version()
);
$this->loader->add_action('init', $plugin_admin, 'new_cpt_jobs');
}
In the admin hooks method in the plugin class, we hook the new_cpt_jobs method onto init.
Custom Post Type
Admin Class
public function new_cpt_jobs() {
$cap_type = 'post';
$plural = 'Jobs';
$single = 'Job';
$opts['show_ui'] = TRUE;
$opts['supports'] = array('title', 'editor', 'thumbnail');
$opts['capabilities']['edit_post'] = "edit_{$cap_type}";
$opts['labels']['add_new'] =
__("Add New {$single}", $this->i18n);
...
register_post_type(strtolower($plural), $opts);
}
Then in the admin class, the new_cpt_jobs method registers the custom post type. I can't fit the entire array on the slide, but you can see part of it there before the register_post_type call.
Taxonomy
Plugin Class
private function define_admin_hooks() {
$plugin_admin = new Now_Hiring_Admin(
$this->get_i18n(),
$this->get_version()
);
$this->loader->add_action('init', $plugin_admin, 'new_tax_type');
}
Taxonomies work the same basic way as a custom post type. We hook the new_taxonomy_type method from the admin class on init.
Taxonomy
Admin Class
public function new_taxonomy_type() {
$plural = 'Types';
$single = 'Type';
$tax_name = 'job_type';
$opts['query_var'] = $tax_name;
$opts['capabilities']['assign_terms'] = 'edit_posts';
$opts['labels']['add_new_item'] =
__("Add New {$single}", $this->i18n);
...
register_taxonomy($tax_name, 'jobs', $opts);
}
Then in the new_taxonomy_type method, we register the taxonomy. Again, I'm not showing the entire options array, just a snippet.
Plugin Settings
Plugin Class
private function define_admin_hooks() {
$plugin_admin = new Now_Hiring_Admin(
$this->get_i18n(),
$this->get_version()
);
$this->loader->add_action('admin_menu', $plugin_admin, 'add_menu');
$this->loader->add_action('admin_init', $plugin_admin, 'register_settings');
}
In the admin class, we hook the register_settings method to admin_init and the add_menu method to admin_menu.
Plugin Settings
Admin Class - Register Settings
public function register_settings() {
register_setting('now_hiring_options', 'now_hiring_options',
array($this, 'validate_options'));
add_settings_section('now_hiring_display_options',
'Display Options',
array($this, 'display_options_section'), 'now-hiring');
add_settings_field('display_salary', 'Display Salary',
array($this, 'display_options_field'),
'now-hiring', 'now_hiring_display_options');
}
You can see, I've registered the setting "now_hiring_options", added a display options section, and added one field called display_salary.
Plugin Settings
Admin Class - Options Page
public function options_page() {
echo '<h2>Now Hiring Settings';
echo '<form method="post" action="options.php">';
settings_fields('now_hiring_options');
do_settings_sections('now-hiring');
submit_button('Save Settings');
echo '</form>';
}
Typical options page stuff here.
Plugin Settings
Admin Class - Add Menu
public function add_menu() {
add_options_page(
__('Now Hiring Settings', $this->i18n),
__('Now Hiring', $this->i18n),
'manage_options',
'now-hiring',
array($this, 'options_page')
);
}
Plugin Settings
Admin Class - Display Section
public function display_options_section($params) {
echo '<p>' . $params['title'] . '</p>';
}
Plugin Settings
Admin Class - Display Field
public function display_options_field() {
$options = get_option('now_hiring_options');
?><input
type="checkbox"
id="now_hiring_options[display_salary]"
name="now_hiring_options[display_salary]"
value="1" <?php checked(1, $options['display_salary'], false); ?>
/><?php
}
Plugin Settings
Admin Class - Validate Options
public function validate_options($input) {
$display_salary = trim($input['display_salary']);
$valid['display_salary'] = isset($display_salary) ? 1 : 0;
if ($valid['display_salary'] != $input['display_salary']) {
add_settings_error(
'display_salary',
'display_salary_error',
'Display salary error.',
'error'
);
}
return $valid;
} // validate_options()
Settings & Row Links
Settings & Row Links
Plugin File
if (!defined('NOW_HIRING_BASENAME')) {
define('NOW_HIRING_BASENAME', plugin_basename(__FILE__));
}
One of the little things I like including in my plugins are settings link and row links.
Settings & Row Links
Plugin Class
private function define_admin_hooks() {
$plugin_admin = new Now_Hiring_Admin($this->get_i18n(), $this->get_version());
$this->loader->add_action(
'plugin_action_links_' . NOW_HIRING_BASENAME,
$plugin_admin,
'settings_link'
);
$this->loader->add_action(
'plugin_row_meta',
$plugin_admin,
'row_links', 10, 2
);
}
Settings & Row Links
Admin Class - Settings Link
public function settings_link($links) {
$settings_link = sprintf(
'<a href="%s">%s</a>',
admin_url('options-general.php?page=now-hiring'),
__('Settings')
);
array_unshift($links, $settings_link);
return $links;
}
Settings & Row Links
Admin Class - Row Link
public function row_links($links, $file) {
if ($file == NOW_HIRING_BASENAME) {
$link = '<a href="https://grumpycats.com/">Grumpy Cat</a>';
array_push($links, $link);
}
return $links;
}
Metaboxes
Plugin Class
private function define_admin_hooks() {
$plugin_admin = new Now_Hiring_Admin($this->get_i18n(), $this->get_version());
$this->loader->add_action('add_meta_boxes', $plugin_admin, 'add_metaboxes');
$this->loader->add_action('save_post_jobs', $plugin_admin, 'save_meta', 10, 2);
}
Metaboxes
Admin Class - Add Metaboxes
public function add_metaboxes() {
add_meta_box(
'now_hiring_job_location',
__('Job Location', $this->i18n),
array($this, 'callback_metabox_job_location'),
'jobs',
'normal',
'default'
);
}
Metaboxes
Admin Class - Metabox Method
public function callback_metabox_job_location($object, $box) {
include(
plugin_dir_path(__FILE__) . 'partials/now-hiring-admin-display-metabox-job-location.php'
);
}
Metaboxes
Admin Class - Save Meta
public function save_meta($post_id, $object) {
// check for autosave, post type, capability, & set nonce
if (!wp_verify_nonce( $_POST['job_location_nonce'], NOW_HIRING_BASENAME)) {
return $post_id;
}
$custom = get_post_custom($post_id);
$metas = array('job-location');
foreach ($metas as $meta) {
// sanitize data
// update meta
}
}
Shortcode
Plugin Class
private function define_public_hooks() {
$plugin_public = new Now_Hiring_Public(
$this->get_i18n(),
$this->get_version()
);
$this->loader->add_action(
'init',
$plugin_public,
'register_shortcodes'
);
}
Shortcode
Public Class - Register Shortcodes
public function register_shortcodes() {
add_shortcode( 'nowhiring', array( $this, 'shortcode' ) );
}
Shortcode
Public Class - Shortcode Method
public function shortcode($atts) {
ob_start();
$defaults['order'] = 'date';
$defaults['quantity'] = -1;
$args = shortcode_atts($defaults, $atts, 'nowhiring');
$items = $this->get_job_posts($args);
...
$output = ob_get_contents();
ob_end_clean();
return $output;
}
Displays, Partials, Views, Oh My!
Shortcode Method
if (is_array($items) || is_object($items)) {
include(plugin_dir_path(__FILE__) . 'partials/now-hiring-public-display.php');
}
Displays, Partials, Views, Oh My!
Public Class - Display Loop
foreach ($items->posts as $item) {
include(plugin_dir_path(__FILE__) . 'now-hiring-public-display-single.php');
}
I set these up in separate files so I can use a plugin option to switch out the display, so I keep each display's code separated.
Displays, Partials, Views, Oh My!
Public Class - Display Loop
$plugin_path = plugin_dir_path(__FILE__);
$layout = esc_attr($options['layout']);
foreach ($items->posts as $item) {
include(
$plugin_path . 'now-hiring-public-display-single-' . $layout . '.php'
);
}
The separated files allow for the possibility of using a plugin option to determine which single display gets shown on the front-end.
Displays, Partials, Views, Oh My!
Public Class - Display Single
?><div class="job-wrap">
<h1 class="job-title">
<a href="<?php echo get_permalink($item->ID); ?>"><?php
echo esc_attr($item->post_title);
?></a>
</h1>
<div class="job-content"><?php echo $item->post_content; ?></h1>
</div>
This is the display I've created for the example. I could see having a plugin option for choosing a different single job posting display. In that case, I'd put the option logic in the loop display file, which would then load one of the single displays based on the plugin option.
Widgets
Where?
I found two ways of incorporating a widget into the plugin.
Both options use these two methods, just in different places.
Plugin Class
Load Dependencies
private function load_dependencies() {
...
require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-now-hiring-widget.php';
}
In the plugin class, add your widget class file in the load_dependencies method.
If you have more than one widget, load each file as a dependency here.
Init Widgets
public function widgets_init() {
register_widget('now_hiring_widget');
}
Next is the widget registration method, which I do similarly to register metaboxes where I do them all at once.
Flush Widget Cache
public function flush_widget_cache($post_id) {
if (wp_is_post_revision($post_id)) { return; }
$post = get_post($post_id);
if ($post->post_type == 'jobs') {
wp_cache_delete($this->i18n, 'widget');
}
}
Lastly, based on the Tom McFarlin's widget boilerplate, I also include a flush_widget_cache, which is triggered when changes within WordPress affect the output of a widget.
Method One
Plugin Class
Constructor
public function __construct($plugin_name, $version) {
...
$this->define_widget_hooks();
}
Option 1 is putting all the widget methods in the plugin class. To kick it off, add a method call in the contructor, I called mine define_widget_hooks.
Plugin Class
Define Widget Hooks
private function define_widget_hooks() {
$this->loader->add_action('widgets_init', $this, 'widgets_init');
$this->loader->add_action('save_post_jobs', $this, 'flush_widget_cache');
$this->loader->add_action('deleted_post', $this, 'flush_widget_cache');
$this->loader->add_action('switch_theme', $this, 'flush_widget_cache');
}
Option 1 is putting all the widget methods in the plugin class. To kick it off, add a method call in the contructor, I called mine define_widget_hooks. You can see, we hook the widgets_init method so the widgets will be recognized by WordPress and we flush the widget cache when posts are saved, which would end up changing the output of our widget.
Method Two
Shared Class
Constructor
public function __construct($plugin_name, $version) {
...
$this->define_shared_hooks();
}
The second option is to create a shared class, where the widgets_init, flush_widget_cache methods are located. In the constructor, we declare a different hooks method.
Shared Class
Load Dependencies
private function load_dependencies() {
...
require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-now-hiring-shared.php';
}
Since we're creating a new class, we'll need to load its file as a dependency.
Shared Class
Define Widget Hooks
private function define_shared_hooks() {
$plugin_shared = new Now_Hiring_Shared($this->get_i18n(), $this->get_version());
$this->loader->add_action('widgets_init', $plugin_shared, 'widgets_init');
$this->loader->add_action('save_post_jobs', $plugin_shared, 'flush_widget_cache');
$this->loader->add_action('deleted_post', $plugin_shared, 'flush_widget_cache');
$this->loader->add_action('switch_theme', $plugin_shared, 'flush_widget_cache');
}
This operates the same way as the admin and public hooks methods, but refers to our new shared class. From there, the shared class looks just like the admin and public classes, so we don't need to review it.
Review
This plugin is on github and I'm hoping to keep developing it and make it ready or the WordPress plugin directory. These are basic examples, but I think you'll get a good idea about how to work with the boilerplate.
Links