web_development:wordpress:plugin-boilerplate

Plugin Boilerplate

WORDPRESS PLUGIN BOILERPLATE GENERATOR: http://wppb.me/

WORDPRESS PLUGIN BOILERPLATE: http://wppb.io/

Setting up the boilerplate in UNIX: https://github.com/DevinVinson/WordPress-Plugin-Boilerplate/wiki/Setting-up-the-boilerplate-in-UNIX

To start:

define('WP_DEBUG', true);

This will help us check for any errors while coding our plugin.

Here’s a little explanation of what each file and folder does:

  • .gitignore Provides a sane default .gitignore to most of the stuff that should not exist in your git repository.
  • CHANGELOG.md Standard changelog of changes to the boilerplate with date of change.
  • README.md A useful starting guide that lists the installation instructions, among other few other sections such as tools recommendations and credits.
  • assets This directory contains the recommended resources that you need to provide whenever you decide to publish your plugin to the WordPress plugin repository. All of the images contained in this directory is the recommended resolution for publishing.
  • trunk This is the actual plugin that you are going to develop. There are a few folders that separate the codebase between admin and public facing functionality. We’ll go through in details of what each subdirectory represents.
    • admin There are three directories contained within the admin directory, namely css, js and partials. As its name suggests, all admin facing functionality should be placed here. By default, the plugin-name-admin.js and plugin-name-admin.css is enqueued to your wp-admin. class-plugin-name-admin.php will provide generic functionality where you can define your admin specific hooks.
    • public This directory is pretty much similar to what the admin directory has to offer, the only difference is that the public directory should be used to store all of your public facing functionality codebase.
    • languages A starting .pot file where you can provide translation functionality with your plugin.
    • includes This is probably where pretty much all the magic happens. There are five starting classes included by default, which we will discuss later in the next section.
    • LICENSE.txt A copy of GPL v2 license is included by default.
    • README.txt A starting point for your plugin README file. This file pretty covers all of the sections that you can further fill in to provide a good plugin page on WordPress plugin repository.
    • plugin-name.php The point of entry for your plugin. In here, a general plugin file header is included that you can modify to your own liking. The register_activation_hook and register_deactivation_hook are also registered in this file if you ever need to include some sort of functionality on plugin activation and/or deactivation.

Included Classes

  • class-plugin-name-activator.php This class is instantiated during the plugin activation. It only has one static method, activate() which is registered to the register_activation_hook. Use this class whenever you find yourself in need to do something on plugin activation such as creating custom tables or saving default options.
  • class-plugin-name-deactivator.php The counterpart for class-plugin-name-deactivator.php. It also only contains one static method, deactivate() which can be used to run any functionality during plugin deactivation.
  • class-plugin-name-i18n.php A starting point for i18n functionality of your plugin. It has one property, $domain which stores your plugin text domain. This property can be set using the public method set_domain(). Finally, the method load_plugin_textdomain() in this class will be called whenever the plugin is loaded.
  • class-plugin-name-loader.php Probably the most important class in the boilerplate. It contains two properties, $actions and $filters where all the hooks registered in the plugin will be stored. It provides two simple wrapper functions, add_action() and add_filter that are used to add actions or filters into the $actions and $filters properties. This should not be confused with the WordPress default add_action() and add_filter() function as this class does not actually register them directly. All of the hooks will only be registered during another method called run().
  • class-plugin-name.php The class that glues all the pieces together. It holds important information about the plugin such as the plugin name and version. Plus, it will load the dependencies using the method load_dependencies() which will include all the above four classes and plugin text domain will be set using set_locale() method. All admin and public hooks that were previously registered can also be defined here. This class also provides simple get methods, such as get_plugin_name() to return the plugin name, get_version() to return the current plugin version and get_loader() that keeps an instance of class-plugin-name-loader.php.

Now that the boilerplate of our plugin is ready and installed, let’s review a bit about the plugin folder structure before we begin with coding it.

First thing you might notice, is that we have 4 main folders:
folder-structure-sublime

Quick overview:

The folder admin is where all our admin facing code will live; including CSS, JS and partials folders and our PHP admin class file class-wp-cbf-admin.php.

Here you will find:

  • The main plugin PHP class class-wp-cbf.php where we will add all our actions and filters.
  • The activator file class-wp-cbf-activator.php.
  • The deactivator file class-wp-cbf-desactivator.php, the internationalization file class-wp-cbf-i18n.php
  • The loader file class-wp-cbf-loader.php which will basically call all our actions in the main class file.
  • The languages folder which is a ready to use .pot file to make your plugin in muliple languages.
  • The public folder is the same as our admin folder except for public facing functionalities.

This leaves us with 4 files:

  • LICENCE.txt: GPL-2 license.
  • README.txt: This will include your plugin name, compatibility version, and description as seen on the plugin page in the WordPress repository. This is the first file we will edit.
  • uninstall.php: This script is called when the user clicks on the Delete link in the WordPress plugin backend.
  • wp-cbf.php: This is the main plugin bootstrap file. You will likely edit this file with the version number and the short description of your plugin.

Now that all this is cleared, it’s time to get our hands dirty. Let’s add some code to our brand new plugin!

plugins-admin-firstIf you go to the plugins page in your WordPress back-end, you will see our plugin with its title, a description, and Activate, Edit and Delete links.

If you click on Activate, it will work thanks to the activator and deactivator classes in the includes folder. This is great, but once activated, nothing really will happen yet.

We need to add a settings page where we will add our plugin options. You might also notice here that we still have a very generic description – let’s fix that first.

This short description is written in the comments of the main plugin class: wp-cbf/wp-cbf.php

short-descriptionSince we are at the root of our plugin, let’s update the README.txt file. You will want this to be pretty detailed explanation, especially since this is what people will see when they reach your plugin webpage. You’ll also notice installation and FAQ sections. The more you cover here, the less you might need to explain during possible support later.

If you reload your Plugins admin page now, you will see your new description.

plugin-with-short-descNext, let’s add a setting page so we will be able to edit our plugin’s options.

Open the admin/class-wp-cbf-admin.php where we have 3 functions already here:

  • __construct which is instantiated whenever this class is called
  • And 2 enqueueing functions: enqueue_styles and enqueue_scripts which are used where we will add our admin related CSS and JS

After these functions, add these following 3 functions. You don’t need to add the huge comment blocks since they’re just there to help you.

/**
*
* admin/class-wp-cbf-admin.php - Don't add this
*
**/

/**
 * Register the administration menu for this plugin into the WordPress Dashboard menu.
 *
 * @since    1.0.0
 */
 
public function add_plugin_admin_menu() {

    /*
     * Add a settings page for this plugin to the Settings menu.
     *
     * NOTE:  Alternative menu locations are available via WordPress administration menu functions.
     *
     *        Administration Menus: http://codex.wordpress.org/Administration_Menus
     *
     */
    add_options_page( 'WP Cleanup and Base Options Functions Setup', 'WP Cleanup', 'manage_options', $this->plugin_name, array($this, 'display_plugin_setup_page')
    );
}

 /**
 * Add settings action link to the plugins page.
 *
 * @since    1.0.0
 */
 
public function add_action_links( $links ) {
    /*
    *  Documentation : https://codex.wordpress.org/Plugin_API/Filter_Reference/plugin_action_links_(plugin_file_name)
    */
   $settings_link = array(
    '<a href="' . admin_url( 'options-general.php?page=' . $this->plugin_name ) . '">' . __('Settings', $this->plugin_name) . '</a>',
   );
   return array_merge(  $settings_link, $links );

}

/**
 * Render the settings page for this plugin.
 *
 * @since    1.0.0
 */
 
public function display_plugin_setup_page() {
    include_once( 'partials/wp-cbf-admin-display.php' );
}

Let’s review and explain those 3 functions:

add_plugin_admin_menu(), as its name says, will add a menu item in the Settings sub-menu items. This is called by the add_options_page(). This function takes five arguments:

  • The page title: Here ‘WP Cleanup and Base Options Functions Setup’.
  • The menu title: Here ‘WP Cleanup’ as you might want to keep it small to span on just one line.
  • Capabilities: Who will be able to access this menu item (Admin, Editor, etc..).
  • The menu slug: Here as for mostly everything we will reference in this plugin we will use the plugin short name (we will access it with $this→plugin_name).
  • The callback function: If you look closely here, we are calling our 3rd function display_plugin_setup_page(). This is where our options will be displayed.

This function adds a “Settings” link to the “Deactivate | Edit” list when our plugin is activated. It takes one argument, the $links array to which we will merge our new link array.

This one is called inside our first add_plugin_admin_menu() function. It just includes the partial file where we will add our Options. It will be mostly HTML and some little PHP logic.

All this is great, but if you just save that file and go back to your plugin page, nothing new will appear yet. We first need to register these functions into your define_admin_hook.

Go to the includes folder and open includes/class-wp-cbf.php. We need to add the following define_admin_hooks() private function to get this started:

/**
*
* include/class-wp-cbf.php - Don't add this
*
**/

// Add menu item
$this->loader->add_action( 'admin_menu', $plugin_admin, 'add_plugin_admin_menu' );

// Add Settings link to the plugin
$plugin_basename = plugin_basename( plugin_dir_path( __DIR__ ) . $this->plugin_name . '.php' );
$this->loader->add_filter( 'plugin_action_links_' . $plugin_basename, $plugin_admin, 'add_action_links' );

Each one of these lines are calling the loader file, actions, or filter hooks. From the includes/wp-cbf-loader.php file, we can get the way we have to add our arguments for example for the first action:

  • $hook (‘admin_menu’), this is the action/filter hook we will add our modifications to
  • $component ($plugin_admin), this is a reference to the instance of the object on which the action is defined, more simply, if we had a hook to the admin_hooks it will be $plugin_admin on the public hooks it will be $plugin_public
  • $callback (add_plugin_admin_menu), the name of our function
  • $priority (not set here – default is 10), priority at which the function is fired with the default being 10
  • $accepted_args (not set here – default is 1), number of arguments passed to our callback function

You can also see that we are setting up a $plugin_basename variable. It will give us the plugin main class file and is needed to add the action_links.

Now, if you refresh your plugins admin page and activate the plugin you will now see the “Settings” link and also the menu link in there.

menu_links_added

Now we have a page to display our settings and that’s pretty good, but it’s empty. You can verify that by jumping on this page by either clicking on the “Settings” link on the “WP Cleanup” menu item.

Before you go and add all your options fields, you might want to write all your plugin options on paper with the type of field you will add. For this particular plugin, most of these will be checkboxes to enable/disable functionalities, a couple of text inputs, selects that we will cover below, and some other very specific fields (color-pickers and image uploads that we will talk about in part 2.

I would also recommend using another utility plugin to grab all the admin-specific markup that we will use. It’s not available on the WordPress repository, so you will need to get it from GitHub: WordPress Admin Style

wp-admin-styleNow, with our list of fields and some admin related markup, we can go on and add our first inputs. For our plugin’s purpose, we will be adding 4 checkboxes to start.

Open admin/partials/wp-cbf-admin-display.php since it’s the file that will display our settings page (as stated in our add_options_page()). Now add the following:



<?php
/**
*
* admin/partials/wp-cbf-admin-display.php - Don't add this comment
*
**/
?>

<!-- This file should primarily consist of HTML with a little bit of PHP. -->
<div class="wrap">

    <h2><?php echo esc_html(get_admin_page_title()); ?></h2>
    
    <form method="post" name="cleanup_options" action="options.php">
    
        <!-- remove some meta and generators from the <head> -->
        <fieldset>
            <legend class="screen-reader-text"><span>Clean WordPress head section</span></legend>
            <label for="<?php echo $this->plugin_name; ?>-cleanup">
                <input type="checkbox" id="<?php echo $this->plugin_name; ?>-cleanup" name="<?php echo $this->plugin_name; ?> [cleanup]" value="1"/>
                <span><?php esc_attr_e('Clean up the head section', $this->plugin_name); ?></span>
            </label>
        </fieldset>

        <!-- remove injected CSS from comments widgets -->
        <fieldset>
            <legend class="screen-reader-text"><span>Remove Injected CSS for comment widget</span></legend>
            <label for="<?php echo $this->plugin_name; ?>-comments_css_cleanup">
                <input type="checkbox" id="<?php echo $this->plugin_name; ?>-comments_css_cleanup" name="<?php echo $this->plugin_name; ?>[comments_css_cleanup]" value="1"/>
                <span><?php esc_attr_e('Remove Injected CSS for comment widget', $this->plugin_name); ?></span>
            </label>
        </fieldset>

        <!-- remove injected CSS from gallery -->
        <fieldset>
            <legend class="screen-reader-text"><span>Remove Injected CSS for galleries</span></legend>
            <label for="<?php echo $this->plugin_name; ?>-gallery_css_cleanup">
                <input type="checkbox" id="<?php echo $this->plugin_name; ?>-gallery_css_cleanup" name="<?php echo $this->plugin_name; ?>[gallery_css_cleanup]" value="1" />
                <span><?php esc_attr_e('Remove Injected CSS for galleries', $this->plugin_name); ?></span>
            </label>
        </fieldset>

        <!-- add post,page or product slug class to body class -->
        <fieldset>
            <legend class="screen-reader-text"><span><?php _e('Add Post, page or product slug to body class', $this->plugin_name); ?></span></legend>
            <label for="<?php echo $this->plugin_name; ?>-body_class_slug">
                <input type="checkbox" id="<?php echo $this->plugin_name;?>-body_class_slug" name="<?php echo $this->plugin_name; ?>[body_class_slug]" value="1" />
                <span><?php esc_attr_e('Add Post slug to body class', $this->plugin_name); ?></span>
            </label>
        </fieldset>
        
        <!-- load jQuery from CDN -->
        <fieldset>
            <legend class="screen-reader-text"><span><?php _e('Load jQuery from CDN instead of the basic wordpress script', $this->plugin_name); ?></span></legend>
            <label for="<?php echo $this->plugin_name; ?>-jquery_cdn">
                <input type="checkbox"  id="<?php echo $this->plugin_name; ?>-jquery_cdn" name="<?php echo $this->plugin_name; ?>[jquery_cdn]" value="1" />
                <span><?php esc_attr_e('Load jQuery from CDN', $this->plugin_name); ?></span>
            </label>
                    <fieldset>
                        <p>You can choose your own cdn provider and jQuery version(default will be Google Cdn and version 1.11.1)-Recommended CDN are <a href="https://cdnjs.com/libraries/jquery">CDNjs</a>, <a href="https://code.jquery.com/jquery/">jQuery official CDN</a>, <a href="https://developers.google.com/speed/libraries/#jquery">Google CDN</a> and <a href="http://www.asp.net/ajax/cdn#jQuery_Releases_on_the_CDN_0">Microsoft CDN</a></p>
                        <legend class="screen-reader-text"><span><?php _e('Choose your prefered cdn provider', $this->plugin_name); ?></span></legend>
                        <input type="url" class="regular-text" id="<?php echo $this->plugin_name; ?>-cdn_provider" name="<?php echo $this->plugin_name; ?>[cdn_provider]" value=""/>
                    </fieldset>
        </fieldset>

        <?php submit_button('Save all changes', 'primary','submit', TRUE); ?>

    </form>

</div>

Nothing should be very surprising here – it’s just a basic form and a couple of checkboxes.

first-fields-addedI you try to check one of these checkboxes now and hit save, you will get redirected to the options.php page. This is because if you look at our form, the action attribute is linked to options.php. So let’s go on and save those options.

At this point, you might be thinking that before saving any of these options, that we should probably be first validating and sanitizing them. Well that’s exaclty what we’re going to do.

So let’s validate and sanitize those options:

Let’s open admin/class-wp-cbf.php in our editor and add a new validation function. So after our display_plugin_setup_page() function jump a couple of lines and add the following:


/**
*
* admin/class-wp-cbf-admin.php
*
**/
public function validate($input) {
    // All checkboxes inputs        
    $valid = array();

    //Cleanup
    $valid['cleanup'] = (isset($input['cleanup']) && !empty($input['cleanup'])) ? 1 : 0;
    $valid['comments_css_cleanup'] = (isset($input['comments_css_cleanup']) && !empty($input['comments_css_cleanup'])) ? 1: 0;
    $valid['gallery_css_cleanup'] = (isset($input['gallery_css_cleanup']) && !empty($input['gallery_css_cleanup'])) ? 1 : 0;
    $valid['body_class_slug'] = (isset($input['body_class_slug']) && !empty($input['body_class_slug'])) ? 1 : 0;
    $valid['jquery_cdn'] = (isset($input['jquery_cdn']) && !empty($input['jquery_cdn'])) ? 1 : 0;
    $valid['cdn_provider'] = esc_url($input['cdn_provider']);
    
    return $valid;
 }

As you can see here, we just created a function called validate, and we are passing it an $input argument. We then add some logic for the checkboxes to see if the input is valid.

We’re doing this with isset and !empty which checks for us if the checkbox as been checked or not. It will assign the valid[] array the value we get from that verification. We also checked our url input field with the esc_url for a simple text field. We used a sanitize_text_field instead, but the process is the same.

We are now going to add the saving/update function for our options.

In the same file, right before the previous code, add:

/**
*
* admin/class-wp-cbf-admin.php
*
**/
 public function options_update() {
    register_setting($this->plugin_name, $this->plugin_name, array($this, 'validate'));
 }

Here we use the register_setting() function which is part of the WordPress API. We are passing it three arguments:

  • Our option group: Here we will use our $plugin_name as it’s unique and safe.
  • Option name: You can register each option as a single, We will save all our options at once – so we will use the $plugin_name again.
  • A callback function: This is used to sanitize our options with the validation function we just created.

Now that we have registered our settings, we need to add a small line of php to our form in order to get it working properly. This line will add a nonce, option_page, action, and a http_referer field as hidden inputs.

So open up the form and update it so it look like the below code:


<?php
/**
*
* admin/partials/wp-cbf-admin-display.php - Don't add this comment
*
**/
?>

<div class="wrap">

    <h2><?php echo esc_html( get_admin_page_title() ); ?></h2>

    <form method="post" name="cleanup_options" action="options.php">
    
    <?php settings_fields($this->plugin_name); ?>

    <!-- This file should primarily consist of HTML with a little bit of PHP. -->

    ...

Great – we are almost there! We’re just missing one last step. We need to register the options_update() to the admin_init hook.

Open includes/class-wp-cbf.php and register our new action:

/**
*
* include/class-wp-cbf.php
*
**/

// Save/Update our plugin options
$this->loader->add_action('admin_init', $plugin_admin, 'options_update');

Let’s try our option page now. On save, the page should refresh, and you should see a notice saying “Settings saved”.

settings-savedVictory is ours!

But wait… If you had a checkbox checked, it’s no longer showing as checked now…

It because we now just need to grab our “options” values and add a small condition to our inputs to reflect this.

Open again the admin/partials/wp-cbf-admin.php file and update it as follow


    <h2 class="nav-tab-wrapper">Clean up</h2>

    <form method="post" name="cleanup_options" action="options.php">

    <?php
        //Grab all options
        $options = get_option($this->plugin_name);

        // Cleanup
        $cleanup = $options['cleanup'];
        $comments_css_cleanup = $options['comments_css_cleanup'];
        $gallery_css_cleanup = $options['gallery_css_cleanup'];
        $body_class_slug = $options['body_class_slug'];
        $jquery_cdn = $options['jquery_cdn'];
        $cdn_provider = $options['cdn_provider'];
    ?>

    <?php
        settings_fields($this->plugin_name);
        do_settings_sections($this->plugin_name);
    ?>

    <!-- remove some meta and generators from the <head> -->
    <fieldset>
        <legend class="screen-reader-text">
            <span>Clean WordPress head section</span>
        </legend>
        <label for="<?php echo $this->plugin_name; ?>-cleanup">
            <input type="checkbox" id="<?php echo $this->plugin_name; ?>-cleanup" name="<?php echo $this->plugin_name; ?>[cleanup]" value="1" <?php checked($cleanup, 1); ?> />
            <span><?php esc_attr_e('Clean up the head section', $this->plugin_name); ?></span>
        </label>
    </fieldset>

    <!-- remove injected CSS from comments widgets -->
    <fieldset>
        <legend class="screen-reader-text"><span>Remove Injected CSS for comment widget</span></legend>
        <label for="<?php echo $this->plugin_name; ?>-comments_css_cleanup">
            <input type="checkbox" id="<?php echo $this->plugin_name; ?>-comments_css_cleanup" name="<?php echo $this->plugin_name; ?>[comments_css_cleanup]" value="1" <?php checked($comments_css_cleanup, 1); ?> />
            <span><?php esc_attr_e('Remove Injected CSS for comment widget', $this->plugin_name); ?></span>
        </label>
    </fieldset>

    <!-- remove injected CSS from gallery -->
    <fieldset>
        <legend class="screen-reader-text"><span>Remove Injected CSS for galleries</span></legend>
        <label for="<?php echo $this->plugin_name; ?>-gallery_css_cleanup">
            <input type="checkbox" id="<?php echo $this->plugin_name; ?>-gallery_css_cleanup" name="<?php echo $this->plugin_name; ?>[gallery_css_cleanup]" value="1" <?php checked( $gallery_css_cleanup, 1 ); ?>  />
            <span><?php esc_attr_e('Remove Injected CSS for galleries', $this->plugin_name); ?></span>
        </label>
    </fieldset>

    <!-- add post,page or product slug class to body class -->
    <fieldset>
        <legend class="screen-reader-text"><span><?php _e('Add Post, page or product slug to body class', $this->plugin_name); ?></span></legend>
        <label for="<?php echo $this->plugin_name; ?>-body_class_slug">
            <input type="checkbox" id="<?php echo $this->plugin_name; ?>-body_class_slug" name="<?php echo $this->plugin_name; ?>[body_class_slug]" value="1" <?php checked($body_class_slug, 1); ?>  />
            <span><?php esc_attr_e('Add Post slug to body class', $this->plugin_name); ?></span>
        </label>
    </fieldset>
    
    <!-- load jQuery from CDN -->
    <fieldset>
        <legend class="screen-reader-text"><span><?php _e('Load jQuery from CDN instead of the basic wordpress script', $this->plugin_name); ?></span></legend>
        <label for="<?php echo $this->plugin_name; ?>-jquery_cdn">
            <input type="checkbox"  id="<?php echo $this->plugin_name; ?>-jquery_cdn" name="<?php echo $this->plugin_name; ?>[jquery_cdn]" value="1" <?php checked($jquery_cdn,1); ?>/>
            <span><?php esc_attr_e('Load jQuery from CDN', $this->plugin_name); ?></span>
        </label>
        <fieldset>
            <p>You can choose your own cdn provider and jQuery version(default will be Google Cdn and version 1.11.1)-Recommended CDN are <a href="https://cdnjs.com/libraries/jquery">CDNjs</a>, <a href="https://code.jquery.com/jquery/">jQuery official CDN</a>, <a href="https://developers.google.com/speed/libraries/#jquery">Google CDN</a> and <a href="http://www.asp.net/ajax/cdn#jQuery_Releases_on_the_CDN_0">Microsoft CDN</a></p>
            <legend class="screen-reader-text"><span><?php _e('Choose your prefered cdn provider', $this->plugin_name); ?></span></legend>
            <input type="url" class="regular-text" id="<?php echo $this->plugin_name; ?>-cdn_provider" name="<?php echo $this->plugin_name; ?>[cdn_provider]" value="<?php if(!empty($cdn_provider)) echo $cdn_provider; ?>"/>
        </fieldset>
    </fieldset>

    <?php submit_button('Save all changes', 'primary','submit', TRUE); ?>

So what we’re doing is basically checking to see if the value exists already, and, if it does, populating the input field with the current value.

We do this by first grabbing all our options and assigning each one to a variable (try to keep those explicit so you know which is which).

Then we add a small condition. We will use the WordPress built-in checked function on our inputs to get the saved value and add the “checked” attribute if the option exists and is set to 1.

So save your file, try to save your plugin once last time, and, boom!, we have successfully finished our plugin.

settings-saved-success

We have seen a lots of things. From the benefits of creating your own plugin and sharing it with fellow WordPress users to why you might want to make your repetitive functions into a plugin. We have reviewed the incredible WordPress Plugin Boilerplate, its structure, and why you should definitely use it.

We put our hands in the grease and pushed ourselves in the first steps of doing a plugin, with 2 types of fields validation and sanitization, all that keeping a Oriented Object PHP process with clean and explicit code. We’re not finished yet though.

In part 2, we will make our plugin alive, creating the functions that will actually influence your WordPress website, we will also discover more complex field types and sanitization, and, finally, get our plugin ready to be reviewed by the WordPress repository team.

We’ll wrap this up with some additional links and sources:

For the first part of our tutorial series, we went through the basics of creating a WordPress plugin, using the WordPress Plugin Boilerplate, using its really handy generator, and learned how to add, sanitize, and save basic input types.

In case you missed that part, make sure you go and check it out – How to build a WordPress Plugin part 1

In part 2, we will cover:

  • Intenationalizing our plugin;
  • Adding different inputs types like color picker and file upload;
  • Dividing the whole page into multiple tabs;
  • Creating functions that will actually do something to the front/back-end;
  • Final testing on our Plugin;
  • And then sending our Plugin to the WordPress repository team for review.

We’ll be working off the codebase from part 1, so if you haven’t done it already, make sure you grab the code from part 1 on GitHub.

Before diving into the code, let me explain why it is important to make your plugin translatable.

Adding a plugin to the WordPress repository will make you part of the WordPress community – which is huge! So, you probably can understand that many people from a full range of different countries might use it. Some of those people might not be fluent in english or your primary language. It makes a lot of sense to have your plugin easily tranlatable without having to touch its core coding.

The plugin boilerplate comes with a languages folder and as you might remember from part one. This is where your plugin language files live. I won’t go deep through the translation process here, but just know that some applications like poedit are here to help with this.

Let’s make some change to our existing code so poedit will be able to find all our translatable strings:

translate_your_pluginBasically, we haven’t changed a lot here, we have just wrapped all hard-coded strings with the following:


    <?php _e('our string', $this->plugin_name);?>

All this does is just echo(_e) our string and assign it to our plugin ($this→plugin_name).

See the code below for the full code change:

<?php
/**
*
* admin/partials/wp-cbf-admin-display.php
*
**/


/**
 * Provide a admin area view for the plugin
 *
 * This file is used to markup the admin-facing aspects of the plugin.
 *
 * @link       http://lostwebdesigns.com
 * @since      1.0.0
 *
 * @package    Wp_Cbf
 * @subpackage Wp_Cbf/admin/partials
 */
?>

<!-- This file should primarily consist of HTML with a little bit of PHP. -->

<div class="wrap">

    <h2><?php echo esc_html( get_admin_page_title() ); ?></h2>

    <h2 class="nav-tab-wrapper">Clean up</h2>

    <form method="post" name="cleanup_options" action="options.php">

    <?php
    //Grab all options

        $options = get_option($this->plugin_name);

        // Cleanup
        $cleanup = $options['cleanup'];
        $comments_css_cleanup = $options['comments_css_cleanup'];
        $gallery_css_cleanup = $options['gallery_css_cleanup'];
        $body_class_slug = $options['body_class_slug'];
        $jquery_cdn = $options['jquery_cdn'];
        $cdn_provider = $options['cdn_provider'];
        
    ?>


    <?php
        settings_fields( $this->plugin_name );
        do_settings_sections( $this->plugin_name );
    ?>

    <!-- remove some meta and generators from the <head> -->
    <fieldset>
        <legend class="screen-reader-text"><span><?php _e('Clean WordPress head section', $this->plugin_name);?></span></legend>
        <label for="<?php echo $this->plugin_name;?>-cleanup">
            <input type="checkbox" id="<?php echo $this->plugin_name;?>-cleanup" name="<?php echo $this->plugin_name;?>[cleanup]" value="1" <?php checked( $cleanup, 1 ); ?> />
            <span><?php esc_attr_e( 'Clean up the head section', $this->plugin_name ); ?></span>
        </label>
    </fieldset>

    <!-- remove injected CSS from comments widgets -->
    <fieldset>
        <legend class="screen-reader-text"><span>Remove Injected CSS for comment widget</span></legend>
        <label for="<?php echo $this->plugin_name;?>-comments_css_cleanup">
            <input type="checkbox" id="<?php echo $this->plugin_name;?>-comments_css_cleanup" name="<?php echo $this->plugin_name;?>[comments_css_cleanup]" value="1" <?php checked( $comments_css_cleanup, 1 ); ?> />
            <span><?php esc_attr_e( 'Remove Injected CSS for comment widget', $this->plugin_name ); ?></span>
        </label>
    </fieldset>

    <!-- remove injected CSS from gallery -->
    <fieldset>
        <legend class="screen-reader-text"><span>Remove Injected CSS for galleries</span></legend>
        <label for="<?php echo $this->plugin_name;?>-gallery_css_cleanup">
            <input type="checkbox" id="<?php echo $this->plugin_name;?>-gallery_css_cleanup" name="<?php echo $this->plugin_name;?>[gallery_css_cleanup]" value="1" <?php checked( $gallery_css_cleanup, 1 ); ?>  />
            <span><?php esc_attr_e( 'Remove Injected CSS for galleries', $this->plugin_name ); ?></span>
        </label>
    </fieldset>

    <!-- add post,page or product slug class to body class -->
    <fieldset>
        <legend class="screen-reader-text"><span><?php _e('Add Post, page or product slug to body class', $this->plugin_name);?></span></legend>
        <label for="<?php echo $this->plugin_name;?>-body_class_slug">
            <input type="checkbox" id="<?php echo $this->plugin_name;?>-body_class_slug" name="<?php echo $this->plugin_name;?>[body_class_slug]" value="1" <?php checked( $body_class_slug, 1 ); ?>  />
            <span><?php esc_attr_e('Add Post slug to body class', $this->plugin_name);?></span>
        </label>
    </fieldset>

    <!-- load jQuery from CDN -->
    <fieldset>
        <legend class="screen-reader-text"><span><?php _e('Load jQuery from CDN instead of the basic wordpress script', $this->plugin_name);?></span></legend>
        <label for="<?php echo $this->plugin_name;?>-jquery_cdn">
            <input type="checkbox"  id="<?php echo $this->plugin_name;?>-jquery_cdn" name="<?php echo $this->plugin_name;?>[jquery_cdn]" value="1" <?php checked($jquery_cdn,1);?>/>
            <span><?php esc_attr_e('Load jQuery from CDN', $this->plugin_name);?></span>
        </label>
                <fieldset class="<?php if(1 != $jquery_cdn) echo 'hidden';?>">
                    <p>You can choose your own cdn provider and jQuery version(default will be Google Cdn and version 1.11.1)-Recommended CDN are <a href="https://cdnjs.com/libraries/jquery">CDNjs</a>, <a href="https://code.jquery.com/jquery/">jQuery official CDN</a>, <a href="https://developers.google.com/speed/libraries/#jquery">Google CDN</a> and <a href="http://www.asp.net/ajax/cdn#jQuery_Releases_on_the_CDN_0">Microsoft CDN</a></p>
                    <legend class="screen-reader-text"><span><?php _e('Choose your prefered cdn provider', $this->plugin_name);?></span></legend>
                    <input type="url" class="regular-text" id="<?php echo $this->plugin_name;?>-cdn_provider" name="<?php echo $this->plugin_name;?>[cdn_provider]" value="<?php if(!empty($cdn_provider)) echo $cdn_provider;?>"/>
                </fieldset>
    </fieldset>

    <?php submit_button(__('Save all changes', $this->plugin_name), 'primary','submit', TRUE); ?>

    </form>

</div>

The only change from the GIF here is for the submit_button(). You can see that the first parameter (which is the button text) has its text wrapped inside __(). This is the same as _e() except that this text will be returned instead of echoed.

So now, all our wrapped strings will be referred to our plugin and poedit will be able to grab those and insert it in its .pot file automatically.

You have now a fully translatable plugin, bravo!

We have already added many files to our plugin, but only two types of inputs:

  • Checkboxes
  • Text inputs

We will now add 2 different and more complex inputs:

  • Color pickers
  • File/Media uploads

To do so, let’s add a new section login page customization to our plugin inside the form. Insert this after the html we already are working from:


/**
*
* admin/partials/wp-cbf-admin-display.php
*
**/

...

<form method="post" name="cleanup_options" action="options.php">

<?php
    //Grab all options      
    $options = get_option($this->plugin_name);
        
    // Cleanup
    $cleanup = $options['cleanup'];
    $comments_css_cleanup = $options['comments_css_cleanup'];
    $gallery_css_cleanup = $options['gallery_css_cleanup'];
    $body_class_slug = $options['body_class_slug'];
    $jquery_cdn = $options['jquery_cdn'];
    $cdn_provider = $options['cdn_provider'];
    
    // New Login customization vars
    $login_logo_id = $options['login_logo_id'];
    $login_logo = wp_get_attachment_image_src( $login_logo_id, 'thumbnail' );
    $login_logo_url = $login_logo[0];
    $login_background_color = $options['login_background_color'];
    $login_button_primary_color = $options['login_button_primary_color'];

?>

...


    <!-- Login page customizations -->

    <h2 class="nav-tab-wrapper"><?php _e('Login customization', $this->plugin_name);?></h2>

        <p><?php _e('Add logo to login form change buttons and background color', $this->plugin_name);?></p>

        <!-- add your logo to login -->
            <fieldset>
                <legend class="screen-reader-text"><span><?php esc_attr_e('Login Logo', $this->plugin_name);?></span></legend>
                <label for="<?php echo $this->plugin_name;?>-login_logo">
                    <input type="hidden" id="login_logo_id" name="<?php echo $this->plugin_name;?>[login_logo_id]" value="<?php echo $login_logo_id; ?>" />
                    <input id="upload_login_logo_button" type="button" class="button" value="<?php _e( 'Upload Logo', $this->plugin_name); ?>" />
                    <span><?php esc_attr_e('Login Logo', $this->plugin_name);?></span>
                </label>
                <div id="upload_logo_preview" class="wp_cbf-upload-preview <?php if(empty($login_logo_id)) echo 'hidden'?>">
                    <img src="<?php echo $login_logo_url; ?>" />
                    <button id="wp_cbf-delete_logo_button" class="wp_cbf-delete-image">X</button>
                </div>
            </fieldset>

        <!-- login background color-->
            <fieldset class="wp_cbf-admin-colors">
                <legend class="screen-reader-text"><span><?php _e('Login Background Color', $this->plugin_name);?></span></legend>
                <label for="<?php echo $this->plugin_name;?>-login_background_color">
                    <input type="text" class="<?php echo $this->plugin_name;?>-color-picker" id="<?php echo $this->plugin_name;?>-login_background_color" name="<?php echo $this->plugin_name;?>[login_background_color]"  value="<?php echo $login_background_color;?>"  />
                    <span><?php esc_attr_e('Login Background Color', $this->plugin_name);?></span>
                </label>
            </fieldset>
            
        <!-- login buttons and links primary color-->
            <fieldset class="wp_cbf-admin-colors">
                <legend class="screen-reader-text"><span><?php _e('Login Button and Links Color', $this->plugin_name);?></span></legend>
                <label for="<?php echo $this->plugin_name;?>-login_button_primary_color">
                    <input type="text" class="<?php echo $this->plugin_name;?>-color-picker" id="<?php echo $this->plugin_name;?>-login_button_primary_color" name="<?php echo $this->plugin_name;?>[login_button_primary_color]" value="<?php echo $login_button_primary_color;?>" />
                    <span><?php esc_attr_e('Login Button and Links Color', $this->plugin_name);?></span>
                </label>
            </fieldset>

        <?php submit_button(__('Save all changes', $this->plugin_name), 'primary','submit', TRUE); ?>

 </form>

We have now added our new variables and all our needed inputs. We now also have to add a variable to “cache” their value from the $options variable. They are here just to grab the logo image url as we will save the image id in our options:


$login_logo = wp_get_attachment_image_src( $login_logo_id, 'thumbnail' );
$login_logo_url = $login_logo[0];

Nothing fancy is going on here. We are simply just using wp_get_attachment_image_src and passing it our login_logo_id and the size that we want. Then $login_logo_url will just give us the image url so we will be able to use it inside our img src attribute.

Looking at the result, it’s not what we were expecting:

login-no-jsTo make this work, we need to add some Javascript and CSS files included in WordPress core to make it display and work properly.

Open admin/class-wp-cbf-admin.php and add the following to the public function enqueue_styles() and public function enqueue_scripts():


/**
*
* admin/class-wp-cbf-admin.php
*
**/


     public function enqueue_styles() {

          /**
           * This function is provided for demonstration purposes only.
           *
           * An instance of this class should be passed to the run() function
           * defined in Wp_Cbf_Loader as all of the hooks are defined
           * in that particular class.
           *
           * The Wp_Cbf_Loader will then create the relationship
           * between the defined hooks and the functions defined in this
           * class.
         */             
         if ( 'settings_page_wp-cbf' == get_current_screen() -> id ) {
             // CSS stylesheet for Color Picker
             wp_enqueue_style( 'wp-color-picker' );            
             wp_enqueue_style( $this->plugin_name, plugin_dir_url( __FILE__ ) . 'css/wp-cbf-admin.css', array( 'wp-color-picker' ), $this->version, 'all' );
         }


    }

    /**
     * Register the JavaScript for the admin area.
     *
     * @since    1.0.0
     */
    public function enqueue_scripts() {

        /**
         * This function is provided for demonstration purposes only.
         *
         * An instance of this class should be passed to the run() function
         * defined in Wp_Cbf_Loader as all of the hooks are defined
         * in that particular class.
         *
         * The Wp_Cbf_Loader will then create the relationship
         * between the defined hooks and the functions defined in this
         * class.
         */
        if ( 'settings_page_wp-cbf' == get_current_screen() -> id ) {
            wp_enqueue_media();   
            wp_enqueue_script( $this->plugin_name, plugin_dir_url( __FILE__ ) . 'js/wp-cbf-admin.js', array( 'jquery', 'wp-color-picker' ), $this->version, false );         
        }

    }

As you can see, we are adding 2 stylesheets: wp-color-picker and thickbox. These are enqueued only if we are on our plugin page because of the conditional if statement in place:

if ( 'settings_page_wp-cbf' == get_current_screen() -> id )

As you can see again, both our JS and CSS calls are loading the previously added dependencies. So any code that we will add in those will overwrite the default values:

wp_enqueue_style( $this->plugin_name, plugin_dir_url( __FILE__ ) . 'css/wp-cbf-admin.css', array('wp-clor-picker' ), $this->version, 'all' );

wp_enqueue_script( $this->plugin_name, plugin_dir_url( __FILE__ ) . 'js/wp-cbf-admin.js', array( 'jquery', 'wp-color-picker' ), $this->version, false );

If you look closely at the enqueue_scripts() function, you might wonder why there is not wp-color-picker, media-upload, or thickbox scripts enqueued here. Well the Iris color-picker script is loaded only as a javascript dependency of our plugin.

Since WordPress v3.5, the Media Uploader is no longer using Thickbox. To load the new media uploader dependencies, we just have to add wp_enqueue_media() and it will load all the needed scripts.

Check those documentation pages for more info wp_enqueue_media() and wp.media the Javascript Reference.

And again, we are just adding those files if we are on our plugin’s settings page.

wp_enqueue_script( $this->plugin_name, plugin_dir_url( __FILE__ ) . 'js/wp-cbf-admin.js', array( 'jquery', 'wp-color-picker', 'media-upload' ), $this->version, false );

Let’s now add some javascript to our plugin’s JS file: admin/js/wp-cbf-admin.js

/**
*
* admin/js/wp-cbf-admin.js
*
**/
(function( $ ) {
    'use strict';

    /**
     * All of the code for your admin-specific JavaScript source
     * should reside in this file.
     *
     * Note that this assume you're going to use jQuery, so it prepares
     * the $ function reference to be used within the scope of this
     * function.
     *
     * From here, you're able to define handlers for when the DOM is
     * ready:
     *
     * $(function() {
     *
     * });
     *
     * Or when the window is loaded:
     *
     * $( window ).load(function() {
     *
     * });
     *
     * ...and so on.
     *
     * Remember that ideally, we should not attach any more than a single DOM-ready or window-load handler
     * for any particular page. Though other scripts in WordPress core, other plugins, and other themes may
     * be doing this, we should try to minimize doing that in our own work.
     */


    $(function(){

         // Let's set up some variables for the image upload and removing the image     
         var frame,
             imgUploadButton = $( '#upload_login_logo_button' ),    
             imgContainer = $( '#upload_logo_preview' ),
             imgIdInput = $( '#login_logo_id' ),
             imgPreview = $('#upload_logo_preview'),        
             imgDelButton = $('#wp_cbf-delete_logo_button'),
             // Color Pickers Inputs
             colorPickerInputs = $( '.wp-cbf-color-picker' );

             


         // WordPress specific plugins - color picker and image upload
         $( '.wp-cbf-color-picker' ).wpColorPicker();

        // wp.media add Image
         imgUploadButton.on( 'click', function( event ){
            
            event.preventDefault();
            
            // If the media frame already exists, reopen it.
            if ( frame ) {
              frame.open();
              return;
            }
            
            // Create a new media frame
            frame = wp.media({
              title: 'Select or Upload Media for your Login Logo',
              button: {
                text: 'Use as my Login page Logo'
              },
              multiple: false  // Set to true to allow multiple files to be selected
            });
            // When an image is selected in the media frame...
            frame.on( 'select', function() {
              
              // Get media attachment details from the frame state
              var attachment = frame.state().get('selection').first().toJSON();                

              // Send the attachment URL to our custom image input field.
              imgPreview.find( 'img' ).attr( 'src', attachment.sizes.thumbnail.url );

              // Send the attachment id to our hidden input
              imgIdInput.val( attachment.id );

              // Unhide the remove image link
              imgPreview.removeClass( 'hidden' );
            });

            // Finally, open the modal on click
            frame.open();
        });


        // Erase image url and age preview
        imgDelButton.on('click', function(e){
            e.preventDefault();
            imgIdInput.val('');
            imgPreview.find( 'img' ).attr( 'src', '' );
            imgPreview.addClass('hidden');
        });

    }); // End of DOM Ready

})( jQuery );

As defined in the comments, we have enclosed our JS inside the DOM ready function: $(function(){ …our code… }); which is itself enclosed in a self-invoking anonymous function.

First we define some vars that we will use in our Javascript, then for the color picker we just call the wpColorPicker() method to our color-picker fields – nothing really too complicated.

login-color-pickerFor the image upload, we mostly use an adapted version of the wp.media Javascript Reference. It’s all pretty much commented, but let’s get through it a bit:

Obviously, when we click on the ‘Upload Logo’ button we will open the media upload frame, which is set by the last statement of the imgUploadButton.on( 'click', function( event ){ … frame.open() });

The frame is a representation of wp.media which can take some options as:

  • The title of the popup, here: title: 'Select or Upload Media for your Login Logo'
  • The bottom right button text, here: button: {text: 'Use as my Login page Logo'}
  • A multi-select option (if you want to be able to grab multiple images), here set to false: multiple: false

We have then a frame.on( 'select', function() {}) which will be triggered once we will choose an uploaded image and return the result as a JSON object represented here by the attachment var:

var attachment = frame.state().get('selection').first().toJSON();

//console.log(attachment)

You can console.log this object to see that it gives us a whole list of attributes from our image. We use it just after to give a src value to our img field (using attachment.sizes.thumbnail.url so we have a nice 150x150px image) and also the image id to our hidden field:

<input type="hidden" id="login_logo_id" name="<?php echo $this->plugin_name;?>[login_logo_id]" value="<?php echo esc_url($login_logo_id); ?>`

Why give it an id value instead of the direct image url? Well, this will make the sanitization way easier as it should only be a number.

login-logo-uploadFinally, we have the imgDelButton.on('click', function(e){}); which will remove the img source value. The hidden file id value and adds the hidden class back to the preview controller if clicked.

So with this little JS, you have now an almost fully functional settings page with some of coolest WordPress built in features.

You should have now the same result as the screenshots below which are the super nice Iris color picker. If you click on any of the color fields, the media uploader pop-up when clicking on the Upload and once the image is selected our new logo display nicely there.

login-options-onPretty neat, right!

But once again, if you try to save all those new values, nothing will happen. To save and be able to retrieve our new inputs values, we will have to go through sanitizing and saving/updating.

Let’s do that now.

As you might remember from part one, register_setting( $this→plugin_name, $this→plugin_name, array($this, 'validate') ); will take care of the update/saving of our variables and values once validated from our validate function:



/**
*
* admin/class-wp-cbf-admin.php
*
**/

    public function validate($input) {
        // All checkboxes inputs
        $valid = array();

        //Cleanup
        $valid['cleanup'] = (isset($input['cleanup']) && !empty($input['cleanup'])) ? 1 : 0;

        $valid['comments_css_cleanup'] = (isset($input['comments_css_cleanup']) && !empty($input['comments_css_cleanup'])) ? 1: 0;

        $valid['gallery_css_cleanup'] = (isset($input['gallery_css_cleanup']) && !empty($input['gallery_css_cleanup'])) ? 1 : 0;

        $valid['body_class_slug'] = (isset($input['body_class_slug']) && !empty($input['body_class_slug'])) ? 1 : 0;

        $valid['jquery_cdn'] = (isset($input['jquery_cdn']) && !empty($input['jquery_cdn'])) ? 1 : 0;

        $valid['cdn_provider'] = esc_url($input['cdn_provider']);

                // Login Customization
                //First Color Picker
                $valid['login_background_color'] = (isset($input['login_background_color']) && !empty($input['login_background_color'])) ? sanitize_text_field($input['login_background_color']) : '';

                if ( !empty($valid['login_background_color']) && !preg_match( '/^#[a-f0-9]{6}$/i', $valid['login_background_color']  ) ) { // if user insert a HEX color with #
                    add_settings_error(
                            'login_background_color',                     // Setting title
                            'login_background_color_texterror',            // Error ID
                            'Please enter a valid hex value color',     // Error message
                            'error'                         // Type of message
                    );
                }

                //Second Color Picker
                $valid['login_button_primary_color'] = (isset($input['login_button_primary_color']) && !empty($input['login_button_primary_color'])) ? sanitize_text_field($input['login_button_primary_color']) : '';
                
                if ( !empty($valid['login_button_primary_color']) && !preg_match( '/^#[a-f0-9]{6}$/i', $valid['login_button_primary_color']  ) ) { // if user insert a HEX color with #
                    add_settings_error(
                            'login_button_primary_color',                     // Setting title
                            'login_button_primary_color_texterror',            // Error ID
                            'Please enter a valid hex value color',     // Error message
                            'error'                         // Type of message
                    );
                }


                //Logo image id
                $valid['login_logo_id'] = (isset($input['login_logo_id']) && !empty($input['login_logo_id'])) ? absint($input['login_logo_id']) : 0;


        return $valid;
    }

This is how our validate function will look like now – let’s go through it a bit

For the login_logo_id there is not much to say. We just check if $input['login_logo_id'] is set, and, if not, we give it a value of 0(which is a false value when checked with empty()). If it is set, we then just make sure that this value is a positive integer with absint.

Now, let’s explain our Color picker validation as, as you can see is more elaborated:

First we grab our $input value and make sure it’s set and not empty:

$valid['login_background_color'] = (isset($input['login_background_color']) && !empty($input['login_background_color'])) ? sanitize_text_field($input['login_background_color']) : '';

Then if the value is not empty (which means also not an empty string), we test it against a regex to make sure it’s an hexadecimal string. This means it has to begin with a # and then include 6 characters that can be either an integer between 0-9 or a letter between a-f:

/^#[a-f0-9]{6}$/i

If the regex test fails, we add some settings error which are part of the settings_api:

add_settings_error('login_button_primary_color', // Setting title 'login_button_primary_color_texterror', // Error ID 'Please enter a valid hex value color', // Error message 'error' // Type of message);

As described in the add_settings_error documentation, the first argument is a unique identifier that must be related to our setting. Here: login_button_primary_color.

Then we have another slug kind of string that will be added to the error message class, here: login_button_primary_color_texterror.

Then the message you want to display (it is quite important to make this explicit): Please enter a valid hex value color.

And finally, then the type of error which is optional as the default value is error. It’s always good to write it down so you can directly know what this error message is about.

Below is an error screenshot when trying to save an non-hexadecimal string for the color picker:

hex-errorAnd if everything is right, we now have our options saved with the success notice instead:

options-savedOk, so now we have a whole bunch of options with values properly sanitized and saved! It’s time to give our plugin the ability to change the behavior of our website!

We haven’t yet made our plugin interact with our WordPress website. This will change right now. We will divide this in 2 separate parts:

  • The “Clean up” part will change our website on the front-end and the login
  • And then “Login Customizations” will apply to the backend

In order to keep our code nicely organized, we will then write each part in its related folder. We’ll use public for the front-end, and we will use admin for the backend – which makes perfect sense.

Open the public/class-wp-cbf-public.php file and add the following after our enqueue_scripts() function:


/**
*
* public/class-wp-cbf-public.php
*
**/

    public function __construct( $plugin_name, $version ) {

        $this->plugin_name = $plugin_name;
        $this->version = $version;
        $this->wp_cbf_options = get_option($this->plugin_name);

    }



    /**
     * Cleanup functions depending on each checkbox returned value in admin
     *
     * @since    1.0.0
     */
    // Cleanup head
    public function wp_cbf_cleanup() {

        if($this->wp_cbf_options['cleanup']){


            remove_action( 'wp_head', 'rsd_link' );                 // RSD link
            remove_action( 'wp_head', 'feed_links_extra', 3 );            // Category feed link
            remove_action( 'wp_head', 'feed_links', 2 );                // Post and comment feed links
            remove_action( 'wp_head', 'index_rel_link' );
            remove_action( 'wp_head', 'wlwmanifest_link' );
            remove_action( 'wp_head', 'parent_post_rel_link', 10, 0 );        // Parent rel link
            remove_action( 'wp_head', 'start_post_rel_link', 10, 0 );       // Start post rel link
            remove_action( 'wp_head', 'rel_canonical', 10, 0 );
            remove_action( 'wp_head', 'wp_shortlink_wp_head', 10, 0 );
            remove_action( 'wp_head', 'adjacent_posts_rel_link_wp_head', 10, 0 ); // Adjacent post rel link
            remove_action( 'wp_head', 'wp_generator' );               // WP Version
            remove_action( 'wp_head', 'print_emoji_detection_script', 7 );
            remove_action( 'wp_print_styles', 'print_emoji_styles' );


        }
    }   
    // Cleanup head
    public function wp_cbf_remove_x_pingback($headers) {
        if(!empty($this->wp_cbf_options['cleanup'])){
            unset($headers['X-Pingback']);
            return $headers;
        }
    }

    // Remove Comment inline CSS
    public function wp_cbf_remove_comments_inline_styles() {
        if(!empty($this->wp_cbf_options['comments_css_cleanup'])){
            global $wp_widget_factory;
            if ( has_filter( 'wp_head', 'wp_widget_recent_comments_style' ) ) {
                remove_filter( 'wp_head', 'wp_widget_recent_comments_style' );
            }

            if ( isset($wp_widget_factory->widgets['WP_Widget_Recent_Comments']) ) {
                remove_action( 'wp_head', array($wp_widget_factory->widgets['WP_Widget_Recent_Comments'], 'recent_comments_style') );
            }
        }
    }

    // Remove gallery inline CSS
    public function wp_cbf_remove_gallery_styles($css) {
        if(!empty($this->wp_cbf_options['gallery_css_cleanup'])){
            return preg_replace( "!<style type='text/css'>(.*?)</style>!s", '', $css );
        }

    }


    // Add post/page slug
    public function wp_cbf_body_class_slug( $classes ) {
        if(!empty($this->wp_cbf_options['body_class_slug'])){
            global $post;
            if(is_singular()){
                $classes[] = $post->post_name;
            }
        }
                return $classes;
    }
    
    // Load jQuery from CDN if available
    public function wp_cbf_cdn_jquery(){
        if(!empty($this->wp_cbf_options['jquery_cdn'])){
            if(!is_admin()){
                            if(!empty($this->wp_cbf_options['cdn_provider'])){
                                $link = $this->wp_cbf_options['cdn_provider'];
                            }else{
                                $link = 'http://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js';
                            }
                            $try_url = @fopen($link,'r');
                            if( $try_url !== false ) {
                                wp_deregister_script( 'jquery' );
                                wp_register_script('jquery', $link, array(), null, false);
                            }
            }
        }
    }

First thing here is that we are adding a reference to our saved options:

$this->wp_cbf_options = get_option($this->plugin_name);

So we will be able to use it inside our functions.

Then we are just adding some public functions that each include an if condition checking if its related option has been checked (here comes our useful $this→wp_cbf_options reference).

Let’s take take the wp_cbf_bobody_class_slug function as an example, looking at the body_class documentation on the WordPress Codex and scrolling down to “Add Classes By Filters” gives us the following example:


/* From the WordPress Codex */

// Add specific CSS class by filter
add_filter( 'body_class', 'my_class_names' );
function my_class_names( $classes ) {
    // add 'class-name' to the $classes array
    $classes[] = 'class-name';
    // return the $classes array
    return $classes;
}


/* From our Plugin */

public function wp_cbf_body_class_slug( $classes ) {
    if(!empty($this->wp_cbf_options['body_class_slug'])){
        global $post;
        if(is_singular()){
            $classes[] = $post->post_name;
        }
    }
    return $classes;
}

We can see that we are using the same format with some change for our plugin as we want to add the post_name if we are on a page, an attachment page, or a single post is_singular. We can then return the $classes array augmented with our new class.

You might notice that in our example we are not applying the add_filter as in the Codex’s example. This is because we are going to add all our front-end related filters and actions to the define_public_hooks private function and our backend related actions and filters to the define_admin_hooks. If you remember, these functions are in the includes folder in class-wp-cbf.php file.

Let’s do this and add our actions and filters to includes/class-wpcbf.php



/**
*
* includes/class-wp-cbf.php
*
**/

    /**
     * Register all of the hooks related to the public-facing functionality
     * of the plugin.
     *
     * @since    1.0.0
     * @access   private
     */
    private function define_public_hooks() {

        $plugin_public = new Wp_Cbf_Public( $this->get_plugin_name(), $this->get_version() );

        /* 
        *  The following actions are commented out as we won't need any added style or script to our theme
        $this->loader->add_action( 'wp_enqueue_scripts', $plugin_public, 'enqueue_styles' );
        $this->loader->add_action( 'wp_enqueue_scripts', $plugin_public, 'enqueue_scripts' );
        */
        
        // Below are our "public" frontend related actions and filters hooks
        
        // Cleanup - Actions and filters
          //Actions
        $this->loader->add_action( 'init', $plugin_public, 'wp_cbf_cleanup' );
        $this->loader->add_action( 'wp_loaded', $plugin_public, 'wp_cbf_remove_comments_inline_styles' );
        $this->loader->add_action( 'wp_loaded', $plugin_public, 'wp_cbf_remove_gallery_styles' );
        $this->loader->add_action('wp_enqueue_scripts', $plugin_public, 'wp_cbf_cdn_jquery', PHP_INT_MAX);

           //Filters
        $this->loader->add_filter('wp_headers', $plugin_public, 'wp_cbf_remove_x_pingback');
        $this->loader->add_filter( 'body_class', $plugin_public, 'wp_cbf_body_class_slug' );


    }

First you will notice that I have commented out the enqueue_styles, enqueue_scripts actions. This is because this plugin won’t add any CSS or Javascript to our website. If you needed to add some style or interaction to your website with your plugin to the front-end, you will have to write some code into those files (public/css/wp-cbf-public.css, public/js/wp-cbf-public.js). We also left those 2 actions un-commented.

We have added 4 actions and 2 filters corresponding to our 6 functions. Let’s explain one of the hook calls. We will keep our body_class slug example:

$this->loader->add_filter( 'body_class', $plugin_public, 'wp_cbf_body_class_slug' );

Looking at the add_filter function (which is what we are calling with $this→loader→add_filter) from the includes/class-wp-cbf-loader.php, we have the following:

public function add_filter( $hook, $component, $callback, $priority = 10, $accepted_args = 1 ) {

    $this->filters = $this->add( $this->filters, $hook, $component, $callback, $priority, $accepted_args );
}

So we easily can decrypt it as:

  • $hook is 'body_class',
  • $component is $plugin_public defined at the beginning of the define_public_hook we will use it on all our “public”(frontend) hooks calls
  • $callback here our function name wp_cbf_body_class_slug from class-wp-cbf-public.php
  • $priority with a default value of 10, we don’t need to specify it from this call but sometimes you will need your hook to have an higher priority, so you will just have to add a number here (only integer)
  • $accepted_args with a default value of 1, same here, we have just one argument passed to our function so we don’t need to specify it, but depending on the hook you want to call you will need to adjust it according to the documentation.

Of course depending on what you want to change, you will have to search the documentation to know which action/filter needs to be triggered and against which hook, all with what argument and priority.

Sweet, let’s see what the results are now.

Go back to your website admin and first uncheck ‘Add Post slug to body class`. If it’s checked, save and go to any page or single post. I will go to the default “Sample Page” from an initial WordPress install and check our page body class from the developer tools. Here’s what you should have (more or less depending on the plugins you might have installed and activated already):

no-body-classNow, let’s activate our “Add Post slug to body class” and then you should get the same result as the below screenshot. The post/page slug is now added to the body class:

body-class-addedBoom! Our plugin changes our front-end as we wanted. We now just have to check or uncheck a checkbox and we will be able to do that to every website we want now. Plugins rock!

Of course you can and you should check what happens with all of our other options. It’s now time for us to add our backend functions.

Open admin/class-wp-cbf-admin.php and after the validate() function, add as follow:


/**
*
* admin/class-wp-cbf-admin.php
*
**/

...

    public function __construct( $plugin_name, $version ) {

        $this->plugin_name = $plugin_name;
        $this->version = $version;
        $this->wp_cbf_options = get_option($this->plugin_name);

    }

...



    /**
     * Login page customizations Functions
     *
     * @since    1.0.0
     */
     private function wp_cbf_login_logo_css(){
         if(isset($this->wp_cbf_options['login_logo_id']) && !empty($this->wp_cbf_options['login_logo_id'])){
             $login_logo = wp_get_attachment_image_src($this->wp_cbf_options['login_logo_id'], 'thumbnail');
             $login_logo_url = $login_logo[0];
             $login_logo_css  = "body.login h1 a {background-image: url(".$login_logo_url."); width:253px; height:102px; background-size: contain;}";
             return $login_logo_css;
         }
     }

     
     // Get Background color is set and different from #fff return it's css
     private function wp_cbf_login_background_color(){
         if(isset($this->wp_cbf_options['login_background_color']) && !empty($this->wp_cbf_options['login_background_color']) ){
             $background_color_css  = "body.login{ background:".$this->wp_cbf_options['login_background_color']."!important;}";
             return $background_color_css;
         }
     }
     // Get Button and links color is set and different from #00A0D2 return it's css
     private function wp_cbf_login_button_color(){
         if(isset($this->wp_cbf_options['login_button_primary_color']) && !empty($this->wp_cbf_options['login_button_primary_color']) ){
             $button_color = $this->wp_cbf_options['login_button_primary_color'];
             $border_color = $this->sass_darken($button_color, 10);
             $message_color = $this->sass_lighten($button_color, 10);
             $button_color_css = "body.login #nav a, body.login #backtoblog a {
                                   color: ".$button_color." !important;
                  }
                  .login .message {
                   border-left: 4px solid ".$message_color.";
                  }
                  body.login #nav a:hover, body.login #backtoblog a:hover {
                        color: ". $border_color." !important;
                  }

                  body.login .button-primary {
                         background: ".$button_color."; /* Old browsers */
                         background: -moz-linear-gradient(top, ".$button_color." 0%, ". $border_color.", 10%) 100%); /* FF3.6+ */
                         background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,".$button_color."), color-stop(100%, ". $border_color.", 10%))); /* Chrome,Safari4+ */
                         background: -webkit-linear-gradient(top, ".$button_color." 0%, ". $border_color.", 10%) 100%); /* Chrome10+,Safari5.1+ */
                         background: -o-linear-gradient(top, ".$button_color." 0%, ". $border_color.", 10%) 100%); /* Opera 11.10+ */
                         background: -ms-linear-gradient(top, ".$button_color." 0%, ". $border_color.", 10%) 100%); /* IE10+ */
                         background: linear-gradient(to bottom, ".$button_color." 0%, ". $border_color.", 10%) 100%); /* W3C */

                         -webkit-box-shadow: none!important;
                         box-shadow: none !important;

                         border-color:". $border_color."!important;
                    }
                    body.login .button-primary:hover, body.login .button-primary:active {
                         background: ". $border_color."; /* Old browsers */
                         background: -moz-linear-gradient(top, ". $border_color." 0%, ". $border_color.", 10%) 100%); /* FF3.6+ */
                         background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,". $border_color."), color-stop(100%,". $border_color.", 10%))); /* Chrome,Safari4+ */
                         background: -webkit-linear-gradient(top, ". $border_color." 0%,". $border_color.", 10%) 100%); /* Chrome10+,Safari5.1+ */
                         background: -o-linear-gradient(top, ". $border_color." 0%,". $border_color.", 10%) 100%); /* Opera 11.10+ */
                         background: -ms-linear-gradient(top, ". $border_color." 0%,". $border_color.", 10%) 100%); /* IE10+ */
                         background: linear-gradient(to bottom, ". $border_color." 0%,". $border_color.", 10%) 100%); /* W3C */
                    }
 
                    body.login input[type=checkbox]:checked:before{
                          color:".$button_color."!important;
                    }

                    body.login input[type=checkbox]:focus,
                    body.login input[type=email]:focus,
                    body.login input[type=number]:focus,
                    body.login input[type=password]:focus,
                    body.login input[type=radio]:focus,
                    body.login input[type=search]:focus,
                    body.login input[type=tel]:focus,
                    body.login input[type=text]:focus,
                    body.login input[type=url]:focus,
                    body.login select:focus,
                    body.login textarea:focus {
                    border-color: ".$button_color."!important;
                    -webkit-box-shadow: 0 0 2px ".$button_color."!important;
                    box-shadow: 0 0 2px ".$button_color."!important;
             }";

             return $button_color_css;
         }
     }

     // Write the actually needed css for login customizations
     public function wp_cbf_login_css(){
         if( !empty($this->wp_cbf_options['login_logo_id']) || $this->wp_cbf_login_background_color() != null || $this->wp_cbf_login_button_color() != null){
             echo '<style>';
             if( !empty($this->wp_cbf_options['login_logo_id'])){
                   echo $this->wp_cbf_login_logo_css();
             }
             if($this->wp_cbf_login_background_color() != null){
                   echo $this->wp_cbf_login_background_color();
             }
             if($this->wp_cbf_login_button_color() != null){
                   echo $this->wp_cbf_login_button_color();
             }
             echo '</style>';
         }
     }



    /**
     * Utility functions
     *
     * @since    1.0.0
     */

     private function sass_darken($hex, $percent) {
         preg_match('/^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i', $hex, $primary_colors);
         str_replace('%', '', $percent);
         $color = "#";
         for($i = 1; $i <= 3; $i++) {
             $primary_colors[$i] = hexdec($primary_colors[$i]);
             $primary_colors[$i] = round($primary_colors[$i] * (100-($percent*2))/100);
             $color .= str_pad(dechex($primary_colors[$i]), 2, '0', STR_PAD_LEFT);
         }
 
         return $color;
     }
 
     private function sass_lighten($hex, $percent) {
         preg_match('/^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i', $hex, $primary_colors);
         str_replace('%', '', $percent);
         $color = "#";
         for($i = 1; $i <= 3; $i++) {
             $primary_colors[$i] = hexdec($primary_colors[$i]);
             $primary_colors[$i] = round($primary_colors[$i] * (100+($percent*2))/100);
             $color .= str_pad(dechex($primary_colors[$i]), 2, '0', STR_PAD_LEFT);
         }

         return $color;
     }

As you can it’s fairly different in here, this has nothing to do with the fact that we are adding admin related functions. It’s just because of how these particular functions are going to be used – so it’s also a good example. Thing that don’t change though is that we have to add a reference to our plugin options in the __construct function: $this→wp_cbf_options = get_option($this→plugin_name);

Then we have here three private functions that are just returning a chunk of CSS code:

  • private function wp_cbf_login_logo_css() returns $login_logo_css
  • private function wp_cbf_login_background_color() returns $background_color_css
  • and a fairly longer bit of CSS is returned by private function wp_cbf_login_button_color() as $button_color_css

Below those three private functions we have the function that will be called as a callback in our hook definition.

You might then wonder what are the last two private functions. Those are just helpers to emulate sass darken and lighten HSL functions so we can get a bunch of slightly different colors for our hover or active state on button or link.

So basically here we just have a function that will write a <style>…our CSS code…</style> tag to our login page with the CSS code returned by the first three private functions.

Let’s add our hooks in includes/class-wp-cbf.php inside define_admin_hooks() function:


/**
*
* includes/class-wp-cbf.php
*
**/

        //Admin Customizations
        $this->loader->add_action( 'login_enqueue_scripts', $plugin_admin, 'wp_cbf_login_css' );

And yes it’s a one liner! Here we use the exact same $this→loader→add_action definition, the only change is on the $component, we are here calling $plugin_admin instead of $plugin_public.

Let’s test it! Once you have added a logo image and chosen a color for your login background and primary button and link color, save and just log out and once directed to the login page you will see your logo, background and buttons and link colors changed according to your choices in the plugin setting page.

login-customizedBravo! You have now a fully functioning plugin. You will be able to re-use that on all websites by just uploading it and configuring its settings.

So we are now happy with our plugin, we should have tested all it’s functionalities but if you want to investigate further I recommend you to add the Developer Plugin to your plugins while developing, it will give you all the needed tools for testing your plugin/theme as deprecated notices, a PHP/MYSQL console, and much more.

Another interesting step would be to go with some BDD or Behavior Driven Development, this is not the subject of this tutorial, but might be an interesting future post, if you want to check it out yourself, make sure to take a look at Codeception.

Anyways, once you are sure your plugin is working perfectly, with no Errors or Notice, you are now ready to send it to be reviewed by the WordPress team

Well, you have done all this work and want to share it with the world, this is great but your plugin will need to be reviewed by WordPress before it can be hosted on the WordPress Plugin Repository.

Before you go, read on how to send your plugin for review, you can already make it an Archive(.zip) and have it ready to share(on your Dropbox or Google Drive for example).

You can also create an account on WordPress.org if it’s not already done.

Then after submission you will just have to be patient, those guys sometimes have a huge amount of plugins to review.

Once approved, you will then have to send your plugin to the [WordPress SVN repository][29] but before, make sure your `readme.txt file is valid

So this is it. We have built a fully functional WordPress plugin from scratch (or almost) thanks to the WordPress plugin boilerplate. We have covered a lot here, from why to build a plugin and where to begin, to the plugin coding itself and how to keep it organized and clean.

I hope this will be helpful for you guys and that you have enjoyed it as much as I enjoyed covering the subject.

A future interesting post following on this would really be going with some Behaviour Driven Development (BDD), but I would be happy to hear what you guys think first.

A smaller post could also go through plugin settings page styling and adding some interaction with a little bit of Javascript as adding tabs as an example.

And of course to learn more about WordPress plugins make sure to check the WordPress Plugin Guideline.

You can find the full plugin on the WordPress repository.

Cheers!

  • web_development/wordpress/plugin-boilerplate.txt
  • Last modified: 2020/10/09 04:02
  • by jimboobrien