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 rowHere are the main points in case you want to skip to some particular task.
- Boilerplate MVC This is code that you can just drop into any plugin to get MVC functionality with ajax support
- Writing a controller
- Understanding Views
- Ajax in WordPress
- The Model: Database Access
Boilerplate
-
wp-content
-
plugins
-
sbow-utils
- sbow-utils.php
- assets
- controllers
- models
- views
-
sbow-utils
-
plugins
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:
- Plugin info The top comment identifies the plugin details which WordPress displays on the plugin activation page: We show basic fields more details here
- Path Constants Several constants used throughout the code are defined
- Instantiation The main controller class is instantiated
<?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:
load_view($name, $data)
Processes and echos a view fileload_model($name)
loads the specified model for userespond($err, $msg, $dat)
sends a response to an ajax request
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:
$title
→ the string 'Very Classy'$doll
→ an array with keysname
,hair
,eyes
, etc$post
→ WordPressWP_Post
object for post withID == 5
read from the database
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:
- The model file must reside in the model folder
- The file name must exactly match the class name — including case
— the extension, of course, is
.php
- After calling
$this->load_model(Model_Class_Name)
the singleton model instance is accessible as$this->model_class_name
. Note that the instance is always the lower-cased version of the class name.
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(...)
.