This tutorial is out of date and no longer maintained.
In this tutorial, we will go through the process of creating a plugin for WordPress. A WordPress plugin extends the WordPress core and is intended to be reusable code or functionality across multiple projects. This is one of the most interesting things about it - you can share your code or functionality on the web.
I am sure many, if not all of you, have already searched for a plugin in the WordPress repository or any of the available market places. This is one of the reasons why WordPress is so widely used. It’s extremely extensible and has a huge community of developers around it. As of today, there are more than 39,000 publicly available free plugins on the WordPress repository.
The plugin we are going to make in this tutorial will help automate some of the most usual functions developers do when creating a new WordPress project. By the end of this tutorial you will know how to:
You might be thinking that it would be easier and faster to just copy and paste code from your last project and not to even bother with writing a custom plugin to do this. Well, this is where you are wrong!
This example will demonstrate the benefits of a WordPress plugin’s purpose by eliminating repetition. All you’ll need to do is add your plugin, change the options, and be on your way. You won’t need to worry that you forgot a function or anything because it’s all self-contained to a single plugin.
The best part about building a WordPress plugin is joining the WordPress open-source community. You can share and get feedback on your work, sell it as a premium plugin, and add it to a browsable marketplace.
Above is a screenshot of the final plugin we’re building. As mentioned earlier, it groups a handful of functions and settings you would usually add to each new project.
Some of cool things that we’re going to be able to do are:
The best way to begin with a new plugin is by working on the incredibly useful WordPress Plugin Boilerplate. You might ask yourself why you would use a boilerplate instead of building from scratch. This boilerplate will get you started quick with a standardized, organized and object-oriented foundation - basically everything you want if you started from scratch.
To get started, just go to the WordPress Plugin Boilerplate Generator and fill-out the form and click on the Build button.
You now have just downloaded the generated plugin boilerplate as a .zip
file. Now, simply unzip it and add it to your WordPress development installation in the plugins
folder.
You might want to have a dedicated local environment for testing your plugins. We recommend using either MAMP/XAMP or using a LAMP vagrant box like the awesome Scotch Box. You should also make sure to turn on the debug functionalities of WordPress by adding the following to your wp-config.php
file:
define('WP_DEBUG', true)
This will help us check for any errors while coding our plugin.
Now that the boilerplate of our plugin is ready and installed, let’s review a bit about the plugin folder structure before we begin with coding it.
First thing you might notice, is that we have 4 main folders:
The folder admin
is where all our admin facing code will live; including CSS, JS and partials folders and our PHP admin class file class-wp-cbf-admin.php
.
Here you will find:
class-wp-cbf.php
where we will add all our actions and filters.class-wp-cbf-activator.php
.class-wp-cbf-desactivator.php
, the internationalization file class-wp-cbf-i18n.php
class-wp-cbf-loader.php
which will basically call all our actions in the main class file.languages
folder which is a ready to use .pot
file to make your plugin in muliple languages.public
folder is the same as our admin
folder except for public facing functionalities.This leaves us with 4 files:
LICENCE.txt
: GPL-2 license.README.txt
: This will include your plugin name, compatibility version, and description as seen on the plugin page in the WordPress repository. This is the first file we will edit.uninstall.php
: This script is called when the user clicks on the Delete
link in the WordPress plugin backend.wp-cbf.php
: This is the main plugin bootstrap file. You will likely edit this file with the version number and the short description of your plugin.Now that all this is cleared, it’s time to get our hands dirty. Let’s add some code to our brand new plugin!
If you go to the plugins page in your WordPress back-end, you will see our plugin with its title, a description, and Activate
, Edit
and Delete
links.
If you click on Activate
, it will work thanks to the activator
and deactivator
classes in the includes
folder. This is great, but once activated, nothing really will happen yet.
We need to add a settings page where we will add our plugin options. You might also notice here that we still have a very generic description - let’s fix that first.
This short description is written in the comments of the main plugin class: wp-cbf/wp-cbf.php
Since we are at the root of our plugin, let’s update the README.txt
file. You will want this to be pretty detailed explanation, especially since this is what people will see when they reach your plugin webpage. You’ll also notice installation and FAQ sections. The more you cover here, the less you might need to explain during possible support later.
If you reload your Plugins admin page now, you will see your new description.
Next, let’s add a setting page so we will be able to edit our plugin’s options.
Open the admin/class-wp-cbf-admin.php
where we have 3 functions already here:
__construct
which is instantiated whenever this class is calledenqueue_styles
and enqueue_scripts
which are used where we will add our admin related CSS and JSAfter these functions, add these following 3 functions. You don’t need to add the huge comment blocks since they’re just there to help you.
/**
*
* admin/class-wp-cbf-admin.php - Don't add this
*
**/
/**
* Register the administration menu for this plugin into the WordPress Dashboard menu.
*
* @since 1.0.0
*/
public function add_plugin_admin_menu() {
/*
* Add a settings page for this plugin to the Settings menu.
*
* NOTE: Alternative menu locations are available via WordPress administration menu functions.
*
* Administration Menus: http://codex.wordpress.org/Administration_Menus
*
*/
add_options_page( 'WP Cleanup and Base Options Functions Setup', 'WP Cleanup', 'manage_options', $this->plugin_name, array($this, 'display_plugin_setup_page')
);
}
/**
* Add settings action link to the plugins page.
*
* @since 1.0.0
*/
public function add_action_links( $links ) {
/*
* Documentation : https://codex.wordpress.org/Plugin_API/Filter_Reference/plugin_action_links_(plugin_file_name)
*/
$settings_link = array(
'<a href="' . admin_url( 'options-general.php?page=' . $this->plugin_name ) . '">' . __('Settings', $this->plugin_name) . '</a>',
);
return array_merge( $settings_link, $links );
}
/**
* Render the settings page for this plugin.
*
* @since 1.0.0
*/
public function display_plugin_setup_page() {
include_once( 'partials/wp-cbf-admin-display.php' );
}
Let’s review and explain those 3 functions:
add_plugin_admin_menu()
add_plugin_admin_menu()
, as its name says, will add a menu item in the Settings
sub-menu items. This is called by the add_options_page()
. This function takes five arguments:
$this->plugin_name
).display_plugin_setup_page()
. This is where our options will be displayed.add_action_links()
This function adds a “Settings” link to the “Deactivate | Edit” list when our plugin is activated. It takes one argument, the $links
array to which we will merge our new link array.
display_plugin_setup_page()
This one is called inside our first add_plugin_admin_menu()
function. It just includes the partial file where we will add our Options. It will be mostly HTML and some little PHP logic.
All this is great, but if you just save that file and go back to your plugin page, nothing new will appear yet. We first need to register these functions into your define_admin_hook
.
Go to the includes
folder and open includes/class-wp-cbf.php
. We need to add the following define_admin_hooks()
private function to get this started:
/**
*
* include/class-wp-cbf.php - Don't add this
*
**/
// Add menu item
$this->loader->add_action( 'admin_menu', $plugin_admin, 'add_plugin_admin_menu' );
// Add Settings link to the plugin
$plugin_basename = plugin_basename( plugin_dir_path( __DIR__ ) . $this->plugin_name . '.php' );
$this->loader->add_filter( 'plugin_action_links_' . $plugin_basename, $plugin_admin, 'add_action_links' );
Each one of these lines are calling the loader file, actions, or filter hooks. From the includes/wp-cbf-loader.php
file, we can get the way we have to add our arguments for example for the first action:
$hook
('admin_menu'
), this is the action/filter hook we will add our modifications to$component
($plugin_admin
), this is a reference to the instance of the object on which the action is defined, more simply, if we had a hook to the admin_hooks
it will be $plugin_admin
on the public hooks it will be $plugin_public
$callback
(add_plugin_admin_menu
), the name of our function$priority
(not set here - default is 10
), priority at which the function is fired with the default being 10$accepted_args
(not set here - default is 1
), number of arguments passed to our callback functionYou can also see that we are setting up a $plugin_basename
variable. It will give us the plugin main class file and is needed to add the action_links
.
Now, if you refresh your plugins admin page and activate the plugin you will now see the “Settings” link and also the menu link in there.
Now we have a page to display our settings and that’s pretty good, but it’s empty. You can verify that by jumping on this page by either clicking on the “Settings” link on the “WP Cleanup” menu item.
Before you go and add all your options fields, you might want to write all your plugin options on paper with the type of field you will add. For this particular plugin, most of these will be checkboxes to enable/disable functionalities, a couple of text inputs, selects that we will cover below, and some other very specific fields (color-pickers and image uploads that we will talk about in part 2.
I would also recommend using another utility plugin to grab all the admin-specific markup that we will use. It’s not available on the WordPress repository, so you will need to get it from GitHub: WordPress Admin Style
Now, with our list of fields and some admin related markup, we can go on and add our first inputs. For our plugin’s purpose, we will be adding 4 checkboxes to start.
Open admin/partials/wp-cbf-admin-display.php
since it’s the file that will display our settings page (as stated in our add_options_page()
). Now add the following:
<?php
/**
*
* admin/partials/wp-cbf-admin-display.php - Don't add this comment
*
**/
?>
<!-- This file should primarily consist of HTML with a little bit of PHP. -->
<div class="wrap">
<h2><?php echo esc_html(get_admin_page_title()); ?></h2>
<form method="post" name="cleanup_options" action="options.php">
<!-- remove some meta and generators from the <head> -->
<fieldset>
<legend class="screen-reader-text"><span>Clean WordPress head section</span></legend>
<label for="<?php echo $this->plugin_name; ?>-cleanup">
<input type="checkbox" id="<?php echo $this->plugin_name; ?>-cleanup" name="<?php echo $this->plugin_name; ?> [cleanup]" value="1"/>
<span><?php esc_attr_e('Clean up the head section', $this->plugin_name); ?></span>
</label>
</fieldset>
<!-- remove injected CSS from comments widgets -->
<fieldset>
<legend class="screen-reader-text"><span>Remove Injected CSS for comment widget</span></legend>
<label for="<?php echo $this->plugin_name; ?>-comments_css_cleanup">
<input type="checkbox" id="<?php echo $this->plugin_name; ?>-comments_css_cleanup" name="<?php echo $this->plugin_name; ?>[comments_css_cleanup]" value="1"/>
<span><?php esc_attr_e('Remove Injected CSS for comment widget', $this->plugin_name); ?></span>
</label>
</fieldset>
<!-- remove injected CSS from gallery -->
<fieldset>
<legend class="screen-reader-text"><span>Remove Injected CSS for galleries</span></legend>
<label for="<?php echo $this->plugin_name; ?>-gallery_css_cleanup">
<input type="checkbox" id="<?php echo $this->plugin_name; ?>-gallery_css_cleanup" name="<?php echo $this->plugin_name; ?>[gallery_css_cleanup]" value="1" />
<span><?php esc_attr_e('Remove Injected CSS for galleries', $this->plugin_name); ?></span>
</label>
</fieldset>
<!-- add post,page or product slug class to body class -->
<fieldset>
<legend class="screen-reader-text"><span><?php _e('Add Post, page or product slug to body class', $this->plugin_name); ?></span></legend>
<label for="<?php echo $this->plugin_name; ?>-body_class_slug">
<input type="checkbox" id="<?php echo $this->plugin_name;?>-body_class_slug" name="<?php echo $this->plugin_name; ?>[body_class_slug]" value="1" />
<span><?php esc_attr_e('Add Post slug to body class', $this->plugin_name); ?></span>
</label>
</fieldset>
<!-- load jQuery from CDN -->
<fieldset>
<legend class="screen-reader-text"><span><?php _e('Load jQuery from CDN instead of the basic wordpress script', $this->plugin_name); ?></span></legend>
<label for="<?php echo $this->plugin_name; ?>-jquery_cdn">
<input type="checkbox" id="<?php echo $this->plugin_name; ?>-jquery_cdn" name="<?php echo $this->plugin_name; ?>[jquery_cdn]" value="1" />
<span><?php esc_attr_e('Load jQuery from CDN', $this->plugin_name); ?></span>
</label>
<fieldset>
<p>You can choose your own cdn provider and jQuery version(default will be Google Cdn and version 1.11.1)-Recommended CDN are <a href="https://cdnjs.com/libraries/jquery">CDNjs</a>, <a href="https://code.jquery.com/jquery/">jQuery official CDN</a>, <a href="https://developers.google.com/speed/libraries/#jquery">Google CDN</a> and <a href="http://www.asp.net/ajax/cdn#jQuery_Releases_on_the_CDN_0">Microsoft CDN</a></p>
<legend class="screen-reader-text"><span><?php _e('Choose your prefered cdn provider', $this->plugin_name); ?></span></legend>
<input type="url" class="regular-text" id="<?php echo $this->plugin_name; ?>-cdn_provider" name="<?php echo $this->plugin_name; ?>[cdn_provider]" value=""/>
</fieldset>
</fieldset>
<?php submit_button('Save all changes', 'primary','submit', TRUE); ?>
</form>
</div>
This code will generate a form and a couple of checkboxes.
If you try to check one of these checkboxes now and hit save, you will get redirected to the options.php
page. This is because if you look at our form, the action
attribute is linked to options.php
. So let’s go on and save those options.
At this point, you might be thinking that before saving any of these options, that we should probably be first validating and sanitizing them. Well that’s exaclty what we’re going to do.
So let’s validate and sanitize those options:
Let’s open admin/class-wp-cbf.php
in our editor and add a new validation function. So after our display_plugin_setup_page()
function jump a couple of lines and add the following:
/**
*
* admin/class-wp-cbf-admin.php
*
**/
public function validate($input) {
// All checkboxes inputs
$valid = array();
//Cleanup
$valid['cleanup'] = (isset($input['cleanup']) && !empty($input['cleanup'])) ? 1 : 0;
$valid['comments_css_cleanup'] = (isset($input['comments_css_cleanup']) && !empty($input['comments_css_cleanup'])) ? 1: 0;
$valid['gallery_css_cleanup'] = (isset($input['gallery_css_cleanup']) && !empty($input['gallery_css_cleanup'])) ? 1 : 0;
$valid['body_class_slug'] = (isset($input['body_class_slug']) && !empty($input['body_class_slug'])) ? 1 : 0;
$valid['jquery_cdn'] = (isset($input['jquery_cdn']) && !empty($input['jquery_cdn'])) ? 1 : 0;
$valid['cdn_provider'] = esc_url($input['cdn_provider']);
return $valid;
}
As you can see here, we just created a function called validate
, and we are passing it an $input
argument. We then add some logic for the checkboxes to see if the input is valid.
We’re doing this with isset
and !empty
which checks for us if the checkbox as been checked or not. It will assign the valid[]
array the value we get from that verification. We also checked our url
input field with the esc_url
for a simple text field. We used a sanitize_text_field
instead, but the process is the same.
We are now going to add the saving/update function for our options.
In the same file, right before the previous code, add:
/**
*
* admin/class-wp-cbf-admin.php
*
**/
public function options_update() {
register_setting($this->plugin_name, $this->plugin_name, array($this, 'validate'));
}
Here we use the register_setting()
function which is part of the WordPress API. We are passing it three arguments:
$plugin_name
as it’s unique and safe.$plugin_name
again.Now that we have registered our settings, we need to add a small line of php to our form in order to get it working properly. This line will add a nonce
, option_page
, action
, and a http_referer
field as hidden inputs.
So open up the form and update it so it look like the below code:
<?php
/**
*
* admin/partials/wp-cbf-admin-display.php - Don't add this comment
*
**/
?>
<div class="wrap">
<h2><?php echo esc_html( get_admin_page_title() ); ?></h2>
<form method="post" name="cleanup_options" action="options.php">
<?php settings_fields($this->plugin_name); ?>
<!-- This file should primarily consist of HTML with a little bit of PHP. -->
...
Great - we are almost there! We’re just missing one last step. We need to register the options_update()
to the admin_init
hook.
Open includes/class-wp-cbf.php
and register our new action:
/**
*
* include/class-wp-cbf.php
*
**/
// Save/Update our plugin options
$this->loader->add_action('admin_init', $plugin_admin, 'options_update');
Let’s try our option page now. On save, the page should refresh, and you should see a notice saying “Settings saved”.
Victory is ours!
But wait… If you had a checkbox checked, it’s no longer showing as checked now…
It because we now just need to grab our “options” values and add a small condition to our inputs to reflect this.
Open again the admin/partials/wp-cbf-admin.php
file and update it as follow
<h2 class="nav-tab-wrapper">Clean up</h2>
<form method="post" name="cleanup_options" action="options.php">
<?php
//Grab all options
$options = get_option($this->plugin_name);
// Cleanup
$cleanup = $options['cleanup'];
$comments_css_cleanup = $options['comments_css_cleanup'];
$gallery_css_cleanup = $options['gallery_css_cleanup'];
$body_class_slug = $options['body_class_slug'];
$jquery_cdn = $options['jquery_cdn'];
$cdn_provider = $options['cdn_provider'];
?>
<?php
settings_fields($this->plugin_name);
do_settings_sections($this->plugin_name);
?>
<!-- remove some meta and generators from the <head> -->
<fieldset>
<legend class="screen-reader-text">
<span>Clean WordPress head section</span>
</legend>
<label for="<?php echo $this->plugin_name; ?>-cleanup">
<input type="checkbox" id="<?php echo $this->plugin_name; ?>-cleanup" name="<?php echo $this->plugin_name; ?>[cleanup]" value="1" <?php checked($cleanup, 1); ?> />
<span><?php esc_attr_e('Clean up the head section', $this->plugin_name); ?></span>
</label>
</fieldset>
<!-- remove injected CSS from comments widgets -->
<fieldset>
<legend class="screen-reader-text"><span>Remove Injected CSS for comment widget</span></legend>
<label for="<?php echo $this->plugin_name; ?>-comments_css_cleanup">
<input type="checkbox" id="<?php echo $this->plugin_name; ?>-comments_css_cleanup" name="<?php echo $this->plugin_name; ?>[comments_css_cleanup]" value="1" <?php checked($comments_css_cleanup, 1); ?> />
<span><?php esc_attr_e('Remove Injected CSS for comment widget', $this->plugin_name); ?></span>
</label>
</fieldset>
<!-- remove injected CSS from gallery -->
<fieldset>
<legend class="screen-reader-text"><span>Remove Injected CSS for galleries</span></legend>
<label for="<?php echo $this->plugin_name; ?>-gallery_css_cleanup">
<input type="checkbox" id="<?php echo $this->plugin_name; ?>-gallery_css_cleanup" name="<?php echo $this->plugin_name; ?>[gallery_css_cleanup]" value="1" <?php checked( $gallery_css_cleanup, 1 ); ?> />
<span><?php esc_attr_e('Remove Injected CSS for galleries', $this->plugin_name); ?></span>
</label>
</fieldset>
<!-- add post,page or product slug class to body class -->
<fieldset>
<legend class="screen-reader-text"><span><?php _e('Add Post, page or product slug to body class', $this->plugin_name); ?></span></legend>
<label for="<?php echo $this->plugin_name; ?>-body_class_slug">
<input type="checkbox" id="<?php echo $this->plugin_name; ?>-body_class_slug" name="<?php echo $this->plugin_name; ?>[body_class_slug]" value="1" <?php checked($body_class_slug, 1); ?> />
<span><?php esc_attr_e('Add Post slug to body class', $this->plugin_name); ?></span>
</label>
</fieldset>
<!-- load jQuery from CDN -->
<fieldset>
<legend class="screen-reader-text"><span><?php _e('Load jQuery from CDN instead of the basic wordpress script', $this->plugin_name); ?></span></legend>
<label for="<?php echo $this->plugin_name; ?>-jquery_cdn">
<input type="checkbox" id="<?php echo $this->plugin_name; ?>-jquery_cdn" name="<?php echo $this->plugin_name; ?>[jquery_cdn]" value="1" <?php checked($jquery_cdn,1); ?>/>
<span><?php esc_attr_e('Load jQuery from CDN', $this->plugin_name); ?></span>
</label>
<fieldset>
<p>You can choose your own cdn provider and jQuery version(default will be Google Cdn and version 1.11.1)-Recommended CDN are <a href="https://cdnjs.com/libraries/jquery">CDNjs</a>, <a href="https://code.jquery.com/jquery/">jQuery official CDN</a>, <a href="https://developers.google.com/speed/libraries/#jquery">Google CDN</a> and <a href="http://www.asp.net/ajax/cdn#jQuery_Releases_on_the_CDN_0">Microsoft CDN</a></p>
<legend class="screen-reader-text"><span><?php _e('Choose your prefered cdn provider', $this->plugin_name); ?></span></legend>
<input type="url" class="regular-text" id="<?php echo $this->plugin_name; ?>-cdn_provider" name="<?php echo $this->plugin_name; ?>[cdn_provider]" value="<?php if(!empty($cdn_provider)) echo $cdn_provider; ?>"/>
</fieldset>
</fieldset>
<?php submit_button('Save all changes', 'primary','submit', TRUE); ?>
So what we’re doing is basically checking to see if the value exists already, and, if it does, populating the input field with the current value.
We do this by first grabbing all our options and assigning each one to a variable (try to keep those explicit so you know which is which).
Then we add a small condition. We will use the WordPress built-in checked
function on our inputs to get the saved value and add the “checked” attribute if the option exists and is set to 1.
So save your file, try to save your plugin once last time, and, boom!, we have successfully finished our plugin.
We have seen a lots of things. From the benefits of creating your own plugin and sharing it with fellow WordPress users to why you might want to make your repetitive functions into a plugin. We have reviewed the incredible WordPress Plugin Boilerplate, its structure, and why you should definitely use it.
We put our hands in the grease and pushed ourselves in the first steps of doing a plugin, with 2 types of fields validation and sanitization, all that keeping a Oriented Object PHP process with clean and explicit code. We’re not finished yet though.
In part 2, we will make our plugin alive, creating the functions that will actually influence your WordPress website, we will also discover more complex field types and sanitization, and, finally, get our plugin ready to be reviewed by the WordPress repository team.
We’ll wrap this up with some additional links and sources:
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!