How to Build a WordPress Plugin (part 2)

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.

Making your plugin translatable (internationalization or il18n)

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_plugin

Basically, 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!

Add more complex input types

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

To 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-picker

For 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-upload

Finally, 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-on

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

Sanitizing and saving/updating those complex fields

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

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

options-saved

Ok, 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!

Create the functions that will change our theme/backend behaviour

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.

Our frontend functions

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

Now, 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-added

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

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

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

Testing Our Plugin

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

Sending our plugin to be reviewed by the WordPress repository 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

Conclusion

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!

Guillaume Kanoufi

Freelance Front end and WordPress developer