Using WordPress Plugin Boilerplate – Chris Wilcoxson – plugin-name.php



Using WordPress Plugin Boilerplate – Chris Wilcoxson – plugin-name.php

0 0


dayton-2015

"Using WordPress Plugin Boileraplate" - a reveal.js-based presentation for WordCamp Dayton 2015

On Github slushman / dayton-2015

Using WordPress Plugin Boilerplate

Chris Wilcoxson

WordCamp Dayton 2015

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

Let's Jump In!

 

 

 

 

 

 

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

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

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

http://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              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.

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' );
See Full Code 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();
See Full Code At the bottom of this file, We load up the primary plugin class and call the run method.

Plugin Class

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();

}
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:

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();

}
See Full Code 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' );

}
See Full Code 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'
    );

}
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.

Loader Class

Constructor

public function __construct() {

    $this->actions = array();
    $this->filters = array();

}
See Full Code 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
    );

}
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.

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']
        );
    }
}
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.

Admin & Public Classes

Constructor

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.

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'
    );

}
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.

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
    );

}
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.

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' );

}
See Full Code 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 );

}
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.

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' );

}
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.

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 );

}
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.

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' );

}
See Full Code 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' );

}
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.

Plugin Settings

Admin Class - Options Page

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.

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' )
    );

}
See Full Code

Plugin Settings

Admin Class - Display Section

public function display_options_section( $params ) {

    echo '<p>' . $params['title'] . '</p>';

}
See Full Code

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

}
See Full Code

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()
See Full Code

Settings & Row Links

Settings & Row Links

Plugin File

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.

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
    );

}
See Full Code

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;

}
See Full Code

Settings & Row Links

Admin Class - Row Link

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

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 );

}
See Full Code

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'
    );

}
See Full Code

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' );

}
See Full Code

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

    }

}
See Full Code

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'
    );

}
See Full Code

Shortcode

Public Class - Register Shortcodes

public function register_shortcodes() {

    add_shortcode( 'nowhiring', array( $this, 'shortcode' ) );

}
See Full Code

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;

}
See Full Code

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' );

}
See Full Code

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' );

}
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.

Displays, Partials, Views, Oh My!

Public Class - Display Loop

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.

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>
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.

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';

}
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.

Init Widgets

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.

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' );

    }

}
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.

Method One

Plugin Class

Constructor

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.

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' );

}
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.

Method Two

Shared Class

Constructor

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.

Shared Class

Load Dependencies

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.

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' );

}
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.

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

http://wppb.io

http://wppb.me

http://dccmarketing.com

http://slushman.com

http://github.com/slushman

Questions?

Using WordPress Plugin Boilerplate Chris Wilcoxson WordCamp Dayton 2015