Please click here if you are not redirected within a few seconds.
MVC WordPress Plugin › shanebow.com
MVC WordPress Plugin

This post shows how to get an extensible WordPress plugin up and running quickly.

This guide targets developers comfortable with MVC frameworks who are new to WordPress and need to quickly get up to speed writing some custom functionality.

In short, it's the tutorial I wish I had when called in on a WordPress job. The sidebar has more background and some really useful links, but the code here is self contained.

What this plugin does

This plugin adds a page (and menu link) in the administrative section of a WordPress site. The page presents a form which is submitted using ajax. The back end then uses the form data to inform a database lookup which is then returned to the front end and displayed using javascript(jQuery).

WP Example PlugIn - UI 1
Specify critia, then click to "Fetch" data via ajax
WP Example PlugIn - UI 2
Result of first ajax call to fetch and display table rows
WP Example PlugIn - UI 3
After clicking on a row in the table the user may update that row

Here are the main points in case you want to skip to some particular task.

Boilerplate

Organization and directory structure

We are writing a plugin called sbow-utils, so create a new directory in the site's plugin directory: /site/wp-content/plugins/sbow-utils, as shown in the diagram.

The name sbow-utils, short for Shanebow Utilities, is the name of our plugin you must give your plugin a unique name.

The sub-directories help us keep the code organized in a manner similar to other MVC systems.

We will go through each of the files shown in the directory tree — Note that you can click on the diagram to go to the appropriate section of this page.

Entry Point - sbow-utils.php

The first file is the entry point for the code — It must have the same name as the containing directory with a php extension.

Here's what it does:

<?php (defined('ABSPATH')) OR exit('No direct script access allowed');
/**
 * Plugin Name: Shane Bow Utilities
 * Plugin URI: http://shanebow.com/
 * Description: Developer tools for maintaining and optimizing WordPress sites.
 * Author: Shane Bow
 * Author URI: http://shanebow.com/
 * Version: 1.0
 * Requires PHP: 5.4
 *
 * Copyright (C) 2014-2020 ShaneBow Inc.
 *
 */

// Plugin Basename
define( 'SBUTILS_PLUGIN_BASENAME', plugin_basename( __FILE__ ));

// Plugin Path
define( 'SBUTILS_PATH', plugin_dir_path(__FILE__));

// Plugin URL
define( 'SBUTILS_URL', plugins_url( '', SBUTILS_PLUGIN_BASENAME ));

// =========================================================================
//  App initialization is done in the main controller
// =========================================================================
require_once SBUTILS_PATH . 'controllers/SBUtils_Controller.php';
$main_controller = new SBUtils_Controller();

Base Controller Class - SB_WP_Controller.php

All controllers should extend the base controller to provide common functionality:

Writing a Controller

In most web apps, there is controller function that corresponds to a url entered or clicked on by a user.

For instance, on this website, the url https://shanebow.com/page/show/wordpress-plugins is routed to function show($page_name) { ... } in the Show.php controller file.

But, WordPress does things differently...

In WordPress controllers, the functions are callbacks for hooks registered in the constructor — so, there is a level of indirection.

The pattern of controllers in a WordPress is to add a bunch of hooks in the constructor, then write the callbacks as the class functions.

Let's take a look at the main controller to see this in action.

Main Controller Class - SBUtils_Controller.php

Here is an abridged look at the main controller showing the basic structure a WordPress MVC controller:

class SBUtils_Controller extends SB_WP_Controller {

    public function __construct() {
        register_activation_hook( SBUTILS_PLUGIN_BASENAME, [$this, 'activation_hook'] );

        add_action('admin_menu', [&$this, 'admin_menu']);
        add_action('wp_ajax_form_one', [&$this, 'form_one']);
        add_action('wp_ajax_fetch_table', [&$this, 'fetch_table']);
        add_action("wp_ajax_nopriv_fetch_table", [&$this, 'login_reqd']);
        }

    public function activation_hook() { ... }

    public function admin_menu() { ... }

    public function my_form_one() { ... }

    public function fetch_table() { ... }
    }

Notice that all hooks (which include actions and filters) expect two parameters: the name of the hook followed by the callback function.

Because we are using classes rather than raw PHP, we need to specify the callback as an array where the first element, &$this, is a pointer to the class instance.

For this controller you see that we handle the activation hook, the admin menu hook and some ajax actions. We'll look closely at ajax below, for now lets examine the other hooks.

As you gain experience you will learn more and more hooks provided by WordPress, but only a few are necessary for most plugins.

Activation Hook

The register_activation_hook is the place for "one time setup" of the plugin — Things like creating database tables and validating the license.

Our simple plugin does nothing at activation but if it did, we would also need to handle the register_deactivation_hook() and register_uninstall_hook().

Admin Menu Action

In the constructor we specified a hook whose callback WordPress will call whenever the admin menu is being built: add_action('admin_menu', [&$this, 'admin_menu']).

Let's take a look at that callback.

function admin_menu(){
    add_menu_page(
        $icon = file_get_contents(SBUTILS_PATH.'/assets/sbow-icon.b64');
        'ShaneBow Utilities',   // page title
        'ShaneBow',             // label for admin sidebar menu
        'manage_options',           // required privilege level
        'sbow-menu',            // menu slug
        [&$this, 'index'],      // callback function
        $icon );
        );
    }

All we are doing is registering a sidebar menu entry and a callback which will render the our 'index' page when it is clicked. We will show the home() function in the next section on views.

The icon is a nice touch, you can read more about the requirements in the sidebar.

Understanding Views {#views} In MVC, it is the responsibility of the controller to assemble any data necessary for the view — usually, or at least very often — by delegating data retrieval to the model.

Once all the data is ready, it loads the view file passing along the data.

Preparing the view

In order to prepare for loading the view, the controller creates an array of data that the view can display however it sees fit. The idea is that you can change the look and feel without having to rewrite the business logic.

Here is a contrived example of a controller function that is preparing and finally loading a view with the data:

Controller function

   public function doll() {
       $data['title'] = 'Very Classy';
       $data['doll'] = [
           'name' => 'Barbi',
           'hair' => 'blond',
           'eyes' => 'blue',
           'bust' => 36,
           'waist' => 18,
           'hips' => 33,
           ];
       $data['post'] = get_post(5);
       $this->load_view('index.php', $data);
       }

View file

<h1><?= $title ?></h1>
<h2><?= $doll['name'] ?></h1>
<ul>
 <li>Eyes: <?= $doll['eyes'] ?></li>
 <li>Bust: <?= $doll['bust'] ?></li>
 <li>Waist: <?= $doll['waist'] ?></li>
 <li>Hips: <?= $doll['hips'] ?></li>
</ul>
<?php if ($post): ?>
 <h2><?= $post['title'] ?></h2>
 <div>
  <?= apply_filters( 'the_content', $post->post_content ); ?>
 </div>
<?php endif; ?>

In load_view() the $data array is extracted making the following variables available to the view:

Ajax in WordPress

In this section we'll go over the entire process of making an ajax call in WordPress.You have probably guessed this is another thing that has to be done the WordPress Way.

So what is the WordPress Way for Ajax?

Essentially, instead of directly targeting your controller, all ajax calls go thru admin-ajax.php, then arrive at your controller indirectly via a hook.

By the way, despite the name, admin-ajax.php is used for all ajax calls in WordPress, not only those for the 'admin' back end.

The View - index.php {#main-view" .clear}

We looked at a simple example view above, in order to illustrate how to pass data into the view.

Now, let's look at the actual view we use in the plugin which is slightly more complicated since it uses to ajax to load content dynamically.

Controller function

    public function index() {
        $data['title'] = 'ShaneBow Utilities';
        wp_register_script( 'sbow_utils_js', SBUTILS_URL.'/assets/sbutils.js', ['jquery'] );
        wp_localize_script( 'sbow_utils_js', 'myAjax', ['ajaxurl' => admin_url( 'admin-ajax.php')] );        
        wp_enqueue_script( 'jquery' );
        wp_enqueue_script( 'sbow_utils_js' );
        $this->load_view('index.php', $data);
        }

The new thing we see in function index() is the code to load the scripts that facilitate the ajax calls.

When we need asset files such as javascript or css in our view, we have to load them the WordPress way — basically, we let WordPress resolve the full URL as well as the load order based on the supplied dependencies.

Bottom line is you call wp_enqueue_script() passing a registered script. Since jquery is preregistered, you don't need to register it explicitly as you must do with custom javascript files.

The wp_localize_script() call is necessary when you need to fill in some variables in the script. In our case, we need replace our place holder for the ajax endpoint to point to the admin-ajax.php file. We need WordPress do this because the path to this file will be different for every site that uses your plugin.

index.php

<div id="show-table">
 <h2>Fetch Table</h2>
 <form method="post">
  <input type="hidden" name="nonce" value="<?= wp_create_nonce("sbow-fetch-table-nonce") ?>">
  <label for="table-name">
   Table Name<br>
   <input type="text" class="widefat edit-menu-item-title" name="table-name" value="">
  </label>
  <div>
   <input type="submit" class="button button-primary button-large menu-save" value="Submit">
  </div>
 </form>
 <div class="response"></div>
</div>

Now let's look at the javascript itself so we can make sense of the whole process.

Javascript - sbutils.js

jQuery(document).ready( function() {

    // fetch_table action
    ////////////////
    jQuery(".fetch_table").click( function(e) {
        e.preventDefault();
        nonce = jQuery(this).attr("data-nonce");

        jQuery.ajax({
            type : "post",
            dataType : "json",
            url : myAjax.ajaxurl,
            data : {action: "fetch_table", nonce: nonce},
            success: function(response) {
                const output = jQuery("#response");
                if(response.err == "0") {
                    response.dat.forEach (row => output.append(row + <br>');
                    }
                else {
                    output.html(Error: ' + response.msg)
                    }
                }
            });
        });

    });

First, notice the url : myAjax.ajaxurl

We don't know where admin-ajax.php is (nor should we care), instead we ask WordPress to fill it in for us. This is the reason for our earlier call to

wp_localize_script( 'sbow_utils_js', 'myAjax', ['ajaxurl' => admin_url( 'admin-ajax.php')] )

Next, notice the data : {action: "fetch_table", nonce: nonce}.

WordPress uses the action we post to create two hooks, that we are listening for via these calls previously made in our controller's constructor:

    add_action('wp_ajax_fetch_table', [&$this, 'fetch_table']);
    add_action("wp_ajax_nopriv_fetch_table", [&$this, 'login_reqd']);

The first hook is triggered when the user is logged in and the second if not. If we don't care whether the user is logged in or not, we can supply the same callback to both.

The other piece of data we are sending is the nonce.

The Model: Database Access

The function load_model(model_class_name) is provided in the base controller class to instantiate the specified model class, if necessary, then return the model's singleton object.

Because this code instantiates a class, we follow certain conventions to make this work:

Our model - SB_Model.php

Following the above rules we access this model from our controllers by first calling $this->load_model('SB_Model'). Now, we can reference the model's methods via $this->sb_model->some_method(...).

Personalize Your Plugin

Specifying an icon for the admin sidebar is a nice touch.

To do this you must paste a base64 encoding of an svg image.

To generate the svg from a png or jpg you can use the trace paths command in Inkscape.

To get the svg into base 64, there are several online tools available, such as BASE64Decode and Encode.

Once you are have base 64, you turn it into a URL by prefixing it with: data:image/svg+xml;base64,.

You can speed up your code, and give yourself the flexibility to more easily try different icons, by placing the URL in separate file that is only loaded when needed.

Background

WordPress has been around for a long time (since 2003) and is very pervasive — According to Wikipedia:

As of June 2019, WordPress is used by 60.8% of all the websites whose content management system is known. This is 27.5% of the top 10 million websites.

WordPress' plugin architecture is supposed to allow regular folks to easily launch a site and augment the functionality with plugins as needed.

A WordPress plugin is code and assets that alter the functionality of the WordPress site once it has been activated. With about 60000 plugins in existence, there seems to be a plugin for everything.

Problem is, these plugins don't always play nice and many of these people who launch sites find themselves in need of professional assistance.

If you are a developer used to modern frameworks, the problem is that WordPress' uses a system of over 300 hooks. And most of the documentation and tutorials out there explain them in isolation rather than as a cohesive whole.


Self-hosted Plugin

See these resources