On Github slushman / dayton-2015
WordCamp Dayton 2015
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
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 pluginsCompletely rewritten
No more singleton
New Structure
http://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/** * @link http://example.com * @since 1.0.0 * @package Plugin_Name * * @wordpress-plugin * Plugin Name: WordPress Plugin Boilerplate * Plugin URI: http://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: http://example.com/ * License: GPL-2.0+ * License URI: http://www.gnu.org/licenses/gpl-2.0.txt * Text Domain: plugin-name * Domain Path: /languages */See Full Code 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.
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' );See Full Code register_activation & register_deactivation Both classes are blank
require plugin_dir_path( __FILE__ ) . 'includes/class-plugin-name.php'; function run_plugin_name() { $plugin = new Plugin_Name(); $plugin->run(); } run_plugin_name();See Full Code At the bottom of this file, We load up the primary plugin class and call the run method.
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(); }See Full Code 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:
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(); }See Full Code Load dependencies brings in all the other classes used by the plugin class. It also instantiates the Loader class.
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' ); }See Full Code The set_locale method uses the WordPress APIs to set the textdomain for the plugin for i18n.
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' ); }See Full Code 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.
public function __construct() { $this->actions = array(); $this->filters = array(); }See Full Code The constructor sets the actions and filters class variables as blank arrays.
public function add_action( $hook, $component, $callback, $priority = 10, $accepted_args = 1 ) { $this->actions = $this->add( $this->actions, $hook, $component, $callback, $priority, $accepted_args ); }See Full Code 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.
public function run() { foreach ( $this->actions as $hook ) { add_action( $hook['hook'], array( $hook['component'], $hook['callback'] ), $hook['priority'], $hook['accepted_args'] ); } }See Full Code 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.
public function __construct( $plugin_name, $version ) { $this->plugin_name = $plugin_name; $this->version = $version; }See Full Code - Admin See Full Code - Public 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.
public function enqueue_styles() { wp_enqueue_style( $this->plugin_name, plugin_dir_url( __FILE__ ) . 'css/plugin-name-admin.css', array(), $this->version, 'all' ); }See Full Code - Admin See Full Code - Public Then they have added two example methods within each class. The first is enqueue_styles, which enqueues the sample CSS file.
public function enqueue_scripts() { wp_enqueue_script( $this->plugin_name, plugin_dir_url( __FILE__ ) . 'js/plugin-name-admin.js', array( 'jquery' ), $this->version, false ); }See Full Code - Admin See Full Code - Public Enqueue scripts works the same basic way, calling the WordPress function to enqueue a script using the sample script file included with the boilerplate.
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.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' ); }See Full Code In the admin hooks method in the plugin class, we hook the new_cpt_jobs method onto init.
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 ); }See Full Code 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.
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' ); }See Full Code Taxonomies work the same basic way as a custom post type. We hook the new_taxonomy_type method from the admin class on init.
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 ); }See Full Code Then in the new_taxonomy_type method, we register the taxonomy. Again, I'm not showing the entire options array, just a snippet.
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' ); }See Full Code In the admin class, we hook the register_settings method to admin_init and the add_menu method to admin_menu.
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' ); }See Full Code You can see, I've registered the setting "now_hiring_options", added a display options section, and added one field called display_salary.
public function options_page() { echo '<h2>Now Hiring Settings</h2>'; echo '<form method="post" action="options.php">'; settings_fields( 'now_hiring_options' ); do_settings_sections( 'now-hiring' ); submit_button( 'Save Settings' ); echo '</form>'; }See Full Code Typical options page stuff here.
public function add_menu() { add_options_page( __( 'Now Hiring Settings', $this->i18n ), __( 'Now Hiring', $this->i18n ), 'manage_options', 'now-hiring', array( $this, 'options_page' ) ); }See Full Code
public function display_options_section( $params ) { echo '<p>' . $params['title'] . '</p>'; }See Full Code
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 }See Full Code
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()See Full Code
if ( ! defined( 'NOW_HIRING_BASENAME' ) ) { define( 'NOW_HIRING_BASENAME', plugin_basename( __FILE__ ) ); }See Full Code One of the little things I like including in my plugins are settings link and row links.
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 ); }See Full Code
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; }See Full Code
public function row_links( $links, $file ) { if ( $file == NOW_HIRING_BASENAME ) { $link = '<a href="http://grumpycats.com/">Grumpy Cat</a>'; array_push( $links, $link ); } return $links; }See Full Code
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 ); }See Full Code
public function add_metaboxes() { add_meta_box( 'now_hiring_job_location', __( 'Job Location', $this->i18n ), array( $this, 'callback_metabox_job_location' ), 'jobs', 'normal', 'default' ); }See Full Code
public function callback_metabox_job_location( $object, $box ) { include( plugin_dir_path( __FILE__ ) . 'partials/now-hiring-admin-display-metabox-job-location.php' ); }See Full Code
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 } }See Full Code
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' ); }See Full Code
public function register_shortcodes() { add_shortcode( 'nowhiring', array( $this, 'shortcode' ) ); }See Full Code
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; }See Full Code
if ( is_array( $items ) || is_object( $items ) ) { include( plugin_dir_path( __FILE__ ) . 'partials/now-hiring-public-display.php' ); }See Full Code
foreach ( $items->posts as $item ) { include( plugin_dir_path( __FILE__ ) . 'now-hiring-public-display-single.php' ); }See Full Code 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.
foreach ( $items->posts as $item ) { include( plugin_dir_path( __FILE__ ) . 'now-hiring-public-display-single-' . esc_attr( $options['layout'] ) . '.php' ); }See Full Code The separated files allow for the possibility of using a plugin option to determine which single display gets shown on the front-end.
?><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>See Full Code 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.
private function load_dependencies() { ... require_once plugin_dir_path( dirname( __FILE__ ) ) . 'includes/class-now-hiring-widget.php'; }See Full Code 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.
public function widgets_init() { register_widget( 'now_hiring_widget' ); }See Full Code Next is the widget registration method, which I do similarly to register metaboxes where I do them all at once.
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' ); } }See Full Code 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.
public function __construct( $plugin_name, $version ) { ... $this->define_widget_hooks(); }See Full Code 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.
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' ); }See Full Code 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.
public function __construct( $plugin_name, $version ) { ... $this->define_shared_hooks(); }See Full Code 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.
private function load_dependencies() { ... require_once plugin_dir_path( dirname( __FILE__ ) ) . 'includes/class-now-hiring-shared.php'; }See Full Code Since we're creating a new class, we'll need to load its file as a dependency.
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' ); }See Full Code 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.