The WordPress Plugin Boilerplate Part 2: Developing a Plugin
In the first part of my series, an introduction to the WordPress Plugin Boilerplate, we looked at how the code is organised within the Boilerplate. To continue with this series, we’ll apply what we’ve learnt previously to build a real working plugin. We are going to take a look at how quickly we can get our plugin up and running using the Boilerplate code, with as little work as possible.
This article will focus on creating and activating the plugin, as well as developing the admin facing functionality of the plugin. In order to follow this tutorial, you’ll need a basic understanding of PHP and WordPress, as well as having a working knowledge of the WordPress Plugin API.
About the Plugin
We’re going to develop a simple plugin that will display the number of days since a specific post was last updated. We’re also going to offer a couple of simple customizations to the plugin, allowing the user to choose a specific number of days after which a post will be considered outdated, as well as the position of the notice in the post content.
Preparing the Boilerplate
As mentioned in the first article, we can either download a fresh copy of the Boilerplate and do the search and replace ourselves, or we can use the unofficial WordPress Plugin Boilerplate Generator to speed up the process. Let’s use the generator for our plugin.
Head over to the WordPress Plugin Boilerplate Generator website and fill in the form with the appropriate values. Let’s just call our plugin “Outdated Notice”. Here’s a sample form with the fields filled in.
I’m using an imaginary URL for the plugin URL that links to an official repository. Don’t worry too much about this stuff as we can always modify it later on in the plugin header.
Click the “Build” button and you should get a nice, customized copy of the WordPress Plugin Boilerplate.
Installing and Activating the Plugin
The generated zip archive will contain the two expected directories, assets
and trunk
. We are not going to use the symlink route in installing our plugin, so extract the trunk
folder in the archive and copy it into the wp-content/plugins
directory.
We still need to rename it appropriately to avoid a naming conflict with other plugins, so we will rename the trunk
directory to outdated-notice
.
If you now go to the “Installed Plugins” section in wp-admin
, sure enough, you’ll see your plugin is on the list of plugins installed but not yet activated. The Plugin Boilerplate generator does not change anything with regards to plugin description so if we want to change it, we can simply edit the description in the main plugin file, in our case, outdated-notice.php
.
Click on “Activate” to activate your shiny new plugin. Nothing will change on your WordPress site, so don’t worry that there’s nothing to see yet after activating the plugin.
Adding an Options Page
Plugin developers usually provide a means for the user to customize the settings of the plugin. This can be achieved by utilizing the Settings API provided by WordPress. Let’s see how can we integrate our own settings into the plugin.
In short, we are going to allow the user to choose where the notice will appear, either before the post content or after the post content. As far as the number of days threshold goes, the user can set the number of days before a post is to be considered outdated. Using that piece of information, we are going to dynamically change the class of the notice so that we can style it differently from a post that is still considered fresh.
Let’s get started by adding an options page for our plugin.
Open up class-oudated-notice-admin.php
inside your admin
folder. We need to modify this class to allow us to register a settings page for our plugin. Add this public method towards the end of the class.
/**
* Add an options page under the Settings submenu
*
* @since 1.0.0
*/
public function add_options_page() {
$this->plugin_screen_hook_suffix = add_options_page(
__( 'Outdated Notice Settings', 'outdated-notice' ),
__( 'Outdated Notice', 'outdated-notice' ),
'manage_options',
$this->plugin_name,
array( $this, 'display_options_page' )
);
}
One thing to note is since we are using classes to define our hooks, we need to pass an array in the form of array( <class instance>, <class method )
instead of directly specifying which function is to be called. The only rule that needs to be observed is that the method needs to be public.
We are not quite done yet! As we can see, the add_options_page
requires a valid callback function, which we have not yet defined in our Outdated_Notice_Admin
class. Let’s add it in. This should be simple enough since we are going to use the provided outdated-notice-admin-display.php
included in our admin/partials
folder. So, all we have to do for our callback function is to include that file.
/**
* Render the options page for plugin
*
* @since 1.0.0
*/
public function display_options_page() {
include_once 'partials/outdated-notice-admin-display.php';
}
That should do it. The last thing we need to do now is to load it correctly using the provided loader class in the Boilerplate. Open up your class-outdated-notice.php
in the includes
folder and add the additional hook we defined earlier inside the define_admin_hooks
method. The proper action hook to include on our options page is admin_menu
so let’s add it in.
$this->loader->add_action( 'admin_menu', $plugin_admin, 'add_options_page' );
You should now see an additional “Outdated Notice” submenu under Settings. You can access the empty options page by going to the URL http://<our-site-url>/wp-admin/options-general.php?page=outdated-notice
.
It’s a blank page for now, so let’s start populating the partial file with proper markup.
Registering, Saving and Retrieving the Settings Value
The Settings API page on the WordPress Codex provides a good explanation on how to register our own settings, including displaying them on the options page.
Here’s a breakdown of what we are going to do in this section:
- Register the hook with the Boilerplate loader
- Register a settings section
- Register two settings fields (threshold days and text position)
- Register the two settings
- Populate the options page
- Saving and repopulating the fields for display.
Register the Hook to the Boilerplate Loader
Let’s go through all the steps one by one.
To register a settings section, we will need to use the register_setting
function. The proper hook to initialize that function is admin_init
. So, first we are going to add another hook into the Boilerplate loader to register our settings inside the define_admin_hooks
method of our main Boilerplate class, Outdated_Notice
class.
$this->loader->add_action( 'admin_init', $plugin_admin, 'register_setting' );
To make things simpler and provide a basic sort of namespacing to our options names, we are going to add another private variable on top of this class. Put this snippet on top of the Outdated_Notice_Admin
class.
/**
* The options name to be used in this plugin
*
* @since 1.0.0
* @access private
* @var string $option_name Option name of this plugin
*/
private $option_name = 'outdated_notice';
From now on, we are going to prepend this value to anything that is related to our options.
Next thing is to actually register the settings section, settings field and the individual settings. Open up the Outdated_Notice_Admin
class again and add the public method register_setting
into that.
Register a Settings Section
Inside the public register_setting
method, we are going to register a settings section. I won’t delve too much into the various functions and APIs to do this since the Codex has already provided enough information to get started. As our plugin settings are relatively simple, we are going to register only one section.
This snippet will allow us to register a “General” section for our options page using the add_settings_section
function.
// Add a General section
add_settings_section(
$this->option_name . '_general',
__( 'General', 'outdated-notice' ),
array( $this, $this->option_name . '_general_cb' ),
$this->plugin_name
);
Notice that we are pre-pending our section name with the variable $option_name to prevent conflicts with other plugins. The callback can be used to provide additional information about our section, which is exactly what we want.
We are going to add another public method, outdated_notice_general_cb
that will echo basic information about this section.
/**
* Render the text for the general section
*
* @since 1.0.0
*/
public function outdated_notice_general_cb() {
echo '<p>' . __( 'Please change the settings accordingly.', 'outdated-notice' ) . '</p>';
}
Register Two Settings Fields (Threshold Days and Text Position)
The next part of the Settings API that we need to use is to register the actual field to be rendered on the options page. This can be achieved using the add_settings_field
function.
We will use radio buttons for the text position configuration. This is done by adding this code to the register_setting
function that we have.
add_settings_field(
$this->option_name . '_position',
__( 'Text position', 'outdated-notice' ),
array( $this, $this->option_name . '_position_cb' ),
$this->plugin_name,
$this->option_name . '_general',
array( 'label_for' => $this->option_name . '_position' )
);
We need to make sure that the fifth argument of add_settings_field
will point to the correct settings section that we registered before or we might not see the field on our options page.
This is not done just yet. We need to provide the callback function that will render the actual markup for our radio buttons. In our outdated_notice_position_cb
function, we need to include this block of code:
**
* Render the radio input field for position option
*
* @since 1.0.0
*/
public function outdated_notice_position_cb() {
?>
<fieldset>
<label>
<input type="radio" name="<?php echo $this->option_name . '_position' ?>" id="<?php echo $this->option_name . '_position' ?>" value="before">
<?php _e( 'Before the content', 'outdated-notice' ); ?>
</label>
<br>
<label>
<input type="radio" name="<?php echo $this->option_name . '_position' ?>" value="after">
<?php _e( 'After the content', 'outdated-notice' ); ?>
</label>
</fieldset>
<?php
}
The second option for day threshold can be configured using a normal text input. So we are going to register another settings field:
add_settings_field(
$this->option_name . '_day',
__( 'Post is outdated after', 'outdated-notice' ),
array( $this, $this->option_name . '_day_cb' ),
$this->plugin_name,
$this->option_name . '_general',
array( 'label_for' => $this->option_name . '_day' )
);
Again, we also need to provide a callback function that will render our text field.
/**
* Render the treshold day input for this plugin
*
* @since 1.0.0
*/
public function outdated_notice_day_cb() {
echo '<input type="text" name="' . $this->option_name . '_day' . '" id="' . $this->option_name . '_day' . '"> '. __( 'days', 'outdated-notice' );
}
Register the Settings
Lastly, we need to register the options name that we are going to use so that it can be recognized within WordPress. Since we are using two different option names, outdated_notice_position
and outdated_notice_day
, we are going to register them both using the register_setting
function.
register_setting( $this->plugin_name, $this->option_name . '_position', array( $this, $this->option_name . '_sanitize_position' ) );
register_setting( $this->plugin_name, $this->option_name . '_day', 'intval' );
Notice that third parameter for the register_setting
function is a sanitization callback. Although it is optional, it’s always useful to make sure that input values are sanitized before being saved to the database.
For day sanitization, we will use the built-in PHP function, intval
as it is enough in our case. As for the text notice position, we are going to define our own sanitization callback function, which will allow only certain values to be saved into the database. This is particularly useful when dealing with options that are limited to specific values, such as in this case where we only accept two values, which are before
and after
, so our sanitization callback will need to make sure that if the value is not one of these, it won’t get saved into the database.
Here’s a simple sanitization callback function to achieve that:
/**
* Sanitize the text position value before being saved to database
*
* @param string $position $_POST value
* @since 1.0.0
* @return string Sanitized value
*/
public function outdated_notice_sanitize_position( $position ) {
if ( in_array( $position, array( 'before', 'after' ), true ) ) {
return $position;
}
}
Populate the Options Page
After we are done with registering all the related settings, now we need to make sure our options page renders correctly. Since we are using the WordPress way to register our fields and settings, this task is particularly straightforward.
Open up the outdated-notice-admin-display.php
inside the admin/partials
folder. Here’s how we can render the options page based on the settings that we have registered before.
<div class="wrap">
<h2><?php echo esc_html( get_admin_page_title() ); ?></h2>
<form action="options.php" method="post">
<?php
settings_fields( $this->plugin_name );
do_settings_sections( $this->plugin_name );
submit_button();
?>
</form>
</div>
With a simple combination of do_settings_sections
and settings_fields
functions, your options page is done in no time at all.
Let’s take a break and refresh the options page.
Saving and Repopulate the Fields
Try filling in some values and save the form. You should get the notice “Settings saved.” but nothings happened. Let try doing a var_dump
to both our options. Place this somewhere in the related function.
var_dump(get_option( $this->option_name . '_position' ));
var_dump(get_option( $this->option_name . '_day' ));
We should get some values back from the database, as per the example below:
string(5) "after"
string(2) "90"
That means our form is working just fine, so the only thing left that needs to be done is to display the current value in the text field, and make sure the correct radio button is checked.
Let’s tackle the radio buttons first. As a shortcut, we are just going to use the checked
function provided by WordPress to mark the value selected previously. Our outdated_notice_position_cb
will need some modification.
Here’s an updated snippet for the callback.
/**
* Render the radio input field for position option
*
* @since 1.0.0
*/
public function outdated_notice_position_cb() {
$position = get_option( $this->option_name . '_position' );
?>
<fieldset>
<label>
<input type="radio" name="<?php echo $this->option_name . '_position' ?>" id="<?php echo $this->option_name . '_position' ?>" value="before" <?php checked( $position, 'before' ); ?>>
<?php _e( 'Before the content', 'outdated-notice' ); ?>
</label>
<br>
<label>
<input type="radio" name="<?php echo $this->option_name . '_position' ?>" value="after" <?php checked( $position, 'after' ); ?>>
<?php _e( 'After the content', 'outdated-notice' ); ?>
</label>
</fieldset>
<?php
}
[php]
<p>Notice that we are retrieving the value back from the database and assigning it to the <code>$position</code> variable before using it in the <code>checked</code> function for the radio buttons.</p>
<p>Next we are going to modify the <code>outdated_notice_day_cb</code> function.</p>
[php]
/**
* Render the treshold day input for this plugin
*
* @since 1.0.0
*/
public function outdated_notice_day_cb() {
$day = get_option( $this->option_name . '_day' );
echo '<input type="text" name="' . $this->option_name . '_day' . '" id="' . $this->option_name . '_day' . '" value="' . $day . '"> ' . __( 'days', 'outdated-notice' );
}
Now, whenever we change the value of either fields, it will be reflected correctly in the options page.
Further Improvements
This is by no means complete. We can always improve the admin facing functionality of this plugin. Some of the things that I can think of are:
- Code cleanup – WordPress Plugin Boilerplate comes with a lot of useful functionality, but in our case, the admin side, CSS and JS loading is completely unnecessary. We can always remove that from our codebase to make it smaller.
- i18n (Internationalization) ready – Although we are using the
__()
and_e()
extensively in our plugin, we don’t really go through the actual i18n process. I won’t cover the process here as this topic has been discussed fairly extensively on SitePoint, for example in this article. - Finer selection – As our implementation is going to be applied to all posts, we can further optimize it to be applied to a post within a certain category, or posts that have specific tags in them.
The complete code can be viewed from this GitHub repository, in the part-2
branch.
Conclusion
We have created a plugin with basic admin facing functionality by registering related settings, and creating an options page for the user to customize our plugin. In a relatively short time using the WordPress Plugin Boilerplate, we achieved this without compromising the code quality and yet still adhering to the best practices as recommended by WordPress.
Stay tuned for the next part of the series where we will do the public facing side of the plugin to display the appropriate notice inside the post content.