WordPress Plugin Tutorial: Create a Widget that uses Post Meta Data

There are a lot of beginner tutorials for developing WordPress plugins, but most focus on creating one simple feature, for example a shortcode, and this may leave programmers new to WordPress feeling lost about how to do something a little more complicated.

What you Will Learn

  • How to create a Meta Box, which is a custom user interface that displays on the post edit page
  • How to save the values entered in the Meta Box into the wp_postmeta database table
  • How to retrieve these values and display them in a dynamic widget
  • How to create a WordPress widget by extending the WP_Widget class
  • How to use the Singleton design pattern (we will use Object Oriented Programming (OOP) principles)
  • Best practices for sanitizing inputs to prevent security concerns

Prerequisites

  • PHP 5.3+
  • WordPress 4.4+ (This may work with earlier versions but has not been tested)
  • A general understanding of PHP and HTML

What we Will Make

In this tutorial, we are going to build a WordPress plugin that displays an Amazon link in a widget.  This Amazon link will be different for every post, as it will use the post’s custom fields to determine which link to display.

Here is what our finished product will look like:

Amazon Widget - Link with Affiliate Tag
We will create this widget, which has an input field for entering the affiliate tag that you want to use for your product link. This widget can go anywhere your theme allows widgets – typically this means the sidebar and (maybe) the footer.

 

WordPress Meta Box Example
We will create this meta box, which has input fields for specifying the Amazon title and the asin. The widget will use these fields to determine which product link to display.
This Plugin is simple – it displays an Amazon link. HOW it looks will depend on your theme and any custom CSS you write. This might not seem too exciting, but it allows you to specify a different product for every single post/page!  If you click on “Amazon Fire Tablet” it will send you to Amazon, using your affiliate link!

A Tutorial for Beginner WordPress Plugin Developers

This tutorial is aimed at people just beginning WordPress plugin development, to show you how to create a widget that displays dynamic content.  It’s easy to display the same content on every page — just add a text widget to your sidebar, footer, or any other widgetized area.  However, displaying custom content that varies based on which page/post you are viewing, requires a custom solution.

Note that the plugin we are going to build doesn’t interact with the Amazon Product Advertising API, which means that it doesn’t grab any database information from Amazon.  This means that it can’t display the current price or the product image.  However, you could extend this plugin to do that if you so desire.  You’ll want to use the WordPress HTTP API to retrieve the data and then you will need to parse the XML.  I have written several plugins that work with the Amazon database, and the whole process is a bit challenging, but definitely doable!

Everyone Learns Differently

This tutorial is written in a step-by-step way, where I only show small parts of the code at a time.  This is so that I can explain what they’re doing and provide relevant tips.  You can start with a blank text editor and follow along.

However, if you learn best by looking at the whole code first, you can get the completed code on Github.

Setting up our Plugin Folder and File

This is going to be a simple plugin with only one file.  We could put this file directly in the \wp-content\plugins directory, but a better idea is to create a directory for it. That allows us to easily extend the plugin in the future, by adding additional files, like CSS stylesheets or Javascript scripts or additional PHP classes.

  1. Create a directory titled lsw-amazon-in-widget
  2. In that directory, create the file lsw-amazon-widget.php

Note: Throughout this tutorial, we will preface many things with “lsw”.  When writing WordPress plugins, it’s a good idea to preface class names, function names, and setting names to decrease the chance of your plugin conflicting with another plugin.

Creating the Plugin File Header

All plugins have to have a file header at the top of their main PHP file. The file header is used by the WordPress File Header API to extract information about your plugin, like the plugin’s name.  Without this file header, your plugin won’t show up in the list of plugins in WordPress admin.   The file header is written inside of PHP comments.

Place this at the top of lsw-amazon-widget.php:

<?php
/**
 *
 * Plugin Name: Amazon Widget
 * Plugin URI:  https://www.linsoftware.com/wordpress-plugin-tutorial-widget/
 * Description: Display Amazon Product in Widget
 * Author: Linnea Wilhelm
 * Author URI: https://www.linsoftware.com
 * Version: 1.0.0
 * License: GPL2
 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain: lsw-amazon-widget
 * Tags: amazon, widget, amazon affiliate
 **/

 Creating the Outline of the Plugin

Our plugin will have two classes, which will both be in the same PHP file.  The first class, Lsw_Amazon, will deal with initialization of our plugin and it will contain the functions having to do with the meta box.  The second class, Lsw_Amazon_Widget, will extend WP_Widget and it will handle the front and back end display of the widget.

class Lsw_Amazon  {

	protected function __construct() {
		// constructor
	}

	public static function add_custom_meta_box( $post_type, $post ) {
		//this function will be called by the add_meta_boxes hook
	}

	public function init() {
		//this function will have all of the hooks for setting up our plugin
	}

	public static function render_meta_box() {
		// this function will contain the html for displaying the meta box
	}

	public static function save_meta_details( $post_id ) {
		// this function will be called by the save_post hook
		// it will save the values entered in the meta box
	}
}


class Lsw_Amazon_Widget extends WP_Widget {


	function __construct() {
                // constructor
	}
	
	public function widget( $args, $instance ) {
		//Front-end display of widget.
	}


	public function form( $instance ) {
		//Back-end display of widget form.
	}


	public function update( $new_instance, $old_instance ) {
		//Process widget options on save
	}

	private function getAmazonUrl($asin, $affiliate_tag) {
		//a helper function
	}

}

Looking over this outline, one thing you may notice is a lot of static methods (Also called “functions”).  When using WordPress hooks, the method that is called by the hook must be both public and static.

 

Implementing the Singleton Design Pattern

We are going to use the singleton design pattern for our Lsw_Amazon class because we only want it to be instantiated once.  If we allowed our class to be instantiated multiple times, not only could this use up more server memory, but it could cause unexpected behavior, like duplicate meta boxes.

Alter the beginning of your Lsw_Amazon class so that it looks like this:

class Lsw_Amazon  {

	public $version = '1.0.0';
	public static $text_domain = 'lsw-amazon-widget';

	protected static $_instance = null;

	protected function __construct() {
		$this->init();
	}

	public static function instance() {
		if ( is_null( self::$_instance ) ) {
			self::$_instance = new self();
		}
		return self::$_instance;
	}

 

 

Since we are using the Singleton pattern, when we want an instance of the Lsw_Amazon class, we will call Lsw_Amazon::instance() instead of calling the constructor directly with the word new. The instance method checks to see if we already have an instance, and if we do, it returns that instance.  If not, then it creates the instance with the new keyword and the constructor runs.   This means that the constructor runs only once each time WordPress runs. The constructor has the keyword protected in front of it to prevent it from being called with the new keyword.

We also added a couple of class properties, the $version and $text_domain, for our later use.  It is a good idea to save the version as a variable so that you can check it programmatically.  Sometimes, you may use this version number to run specific code when upgrading your plugin.

Let’s add the Action Hooks

For organization, all hooks and filters should go in the same place, if possible.  We’re going to put them in our init() function, which is called by the constructor.

	public function init() {
		add_action( 'add_meta_boxes', array('Lsw_Amazon', 'add_custom_meta_box'), 10, 2 );
		add_action( 'save_post',  array('Lsw_Amazon', 'save_meta_details') );
		add_action( 'widgets_init', function(){ register_widget( 'Lsw_Amazon_Widget' );});
	}

As you can see with the above code, it’s possible to call functions that are inside of classes.  Instead of passing the function name as the second parameter to add_action, you pass an array that contains the class name and function name.

For the widgets_init hook, we are using an anonymous function.  Anonymous functions first became available in PHP with version 5.3.  If you are using an earlier version, you will have to rewrite that line to use a named function, as we are using in the first two lines.

Let’s Add the Meta Box and write the HTML to Display It

	public static function add_custom_meta_box( $post_type, $post ) {
		$post_types = apply_filters('lsw_amazon_post_types', array('post', 'page'));
		add_meta_box( 'lsw_amazon_settings_box',
                    __( 'Amazon Product Settings', self::$text_domain ), 
                    array('Lsw_Amazon','render_meta_box'),
		    $post_types, 'normal', 'default' );
	}

	public static function render_meta_box() {
		global $post;
		$lsw_amazon = get_post_meta($post->ID, 'lsw_amazon', true);
		$title = isset($lsw_amazon['title']) ? $lsw_amazon['title'] : '';
		$asin = isset($lsw_amazon['asin']) ? $lsw_amazon['asin'] : '';
		?>
		<label for="lsw_title">Title   </label><input class="widefat" type="text" name="lsw_title"
		     id="lsw_title" value="<?php echo esc_attr($title); ?>"> <br>
		<label for="lsw_asin">ASIN  </label><input class="widefat" type="text" name="lsw_asin"
		     id="lsw_asin" value="<?php echo esc_attr($asin); ?>">
		<?php
	}

Tip: Get your case right! $post->id will NOT work.  The correct field name is all capitalized, so it’s $post->ID.

Let’s take a moment to look at this line:

$post_types = apply_filters('lsw_amazon_post_types', array('post', 'page'));

That line is saying take the array and perform all of the filters that are registered with the add_filter hook.

In our case, we are filtering an array of post types.  The default action of our plugin is to only add the meta box to posts and pages.  However, if someone wanted to modify this and have the meta box appear on a custom post type, they could add this code snippet to their theme’s functions.php file.  (Or, they could add it to another plugin, or to a php file in the mu-plugin directory. See here: Must Use Plugins)

add_filter('lsw_amazon_post_types', 'associate_post_types_with_amazon_box');
function associate_post_types_with_amazon_box($post_types) {
   $post_types[] = 'YOUR_CUSTOM_POST_TYPE';
 return $post_types;
}

A few other notes about the meta box code:

  • We are using functions for internationalization. The function _() translates the text before displaying.
  • We escaped attribute values with esc_attr.  Wordpress has a variety of functions for sanitizing and validating text. (The WordPress Functions Reference is invaluable!)  It’s important to treat all user entered data and all data from the database as potentially dangerous.  User input data includes $_POST and $_SERVER and $_GET data, since those can all be changed by the user.  If you don’t sanitize or validate the data before displaying it on a webpage, your plugin is vulnerable to cross-site scripting attacks.
  • We’re using the PHP ternary operator along with the isset() function to check if our values are set, and to set them to an empty string if they are not set.  If you try to use a variable that has not been set, you will get a Notice: Undefined Variable error. This won’t stop execution of your plugin, but it may cause a very ugly error to appear.  The practice of using a ternary operator with the isset function is so common throughout PHP programming, that the latest version of PHP, PHP 7, has a new null coalescing operator which is much simpler!

Let’s Save the Values Entered in the Meta Box

	public static function save_meta_details( $post_id ) {
		if( isset($_POST['lsw_asin']) && isset($_POST['lsw_title'] )) {
			$lsw_amazon = array('asin'=>sanitize_text_field($_POST['lsw_asin']),
				'title'=>sanitize_text_field($_POST['lsw_title']));
			update_post_meta( $post_id, 'lsw_amazon', $lsw_amazon);
		}
	}

A note about this code:

  • Once again, we sanitize or escape user input before using it. Remember, values from the database can contain dangerous characters and should be treated just like user input. If you’re familiar with PHP but not WordPress, you probably have used the filter_var() function. Well, sanitize_text_field is a WordPress helper function that you can use in its place when you are sanitizing a string.

 

Now, Let’s Create the Widget

In WordPress, widgets are created by extending the class WP_Widget. This means that there is a set of methods that they must have. Specifically, the WordPress core says that “WP_Widget::widget(), WP_Widget::update() and WP_Widget::form() need to be overridden.” This means that you implement these methods in your child class so that the child methods run instead of the parent methods. If you have followed other tutorials for creating WordPress widgets, or have had a peak at the documentation, then you won’t find any surprises here.

The Widget Constructor

	/**
	 * Register widget with WordPress.
	 */
	function __construct() {
		parent::__construct(
			'lsw_amazon_widget', // Base ID
			'Amazon in Widget', // Name
			array( 'description' => 'Display an Amazon Product' )
		);

	}

Things to note about this code:

  • This calls the parent’s constructor method (in this case, WP_Widget::__construct())
  • The ID should be unique, and the other parameters are displayed in the Appearance-->Widgets admin area.

The Front-End Display of the Widget

 

	public function widget( $args, $instance ) {
		if(!is_single()) {
			return;
		}
		global $post;
		$lsw_amazon = get_post_meta($post->ID, 'lsw_amazon', true);
		if(isset($lsw_amazon['title']) && isset($lsw_amazon['asin'])) {
			$tag = isset( $instance['tag'] ) ? $instance['tag'] : '';
			echo $args['before_widget'];
			?>
                        <h2>You Might Be Interested In...</h2>
			<a href="<?php echo $this->getAmazonUrl( $lsw_amazon['asin'], $tag ); ?>">
				<?php echo sanitize_text_field( $lsw_amazon['title'] ); ?>
			</a>
			<?php
			echo $args['after_widget'];
		}
	}

What this code is doing:

This widget method is called when WordPress tries to display your Widget in the dynamic sidebar or whatever widget area you have in your theme, on the front-end. It uses is_single() to check if this is a single page or single post. It doesn’t make sense to display this widget if there is no single post or page being displayed because this widget wouldn’t know what Amazon product to display. You could alter this to have it display a default, but that’s beyond the scope of this tutorial.

So, if is_single() is false, the function completes without doing anything.

The global $post variable holds the current WP_Post object, which has an ID field. We use this ID field as the parameter for get_post_meta, which does exactly what it sounds: it gets the post meta data from the database. In this case, we provide the name of our data as the second parameter and the third parameter, true, indicates that we only want this single value. This value is actually an array that contains the title and the ASIN.

The widget echoes the before_widget html (this is specified by the theme usually), and then echoes a link to the product, based on the ASIN and the affiliate tag. And, finally, the after_widget html is displayed.

The Back-End Display of the Widget

	public function form( $instance ) {
		// outputs the options form on admin
		$tag  = isset( $instance['tag'] ) ? $instance['tag'] : '';
		?>
		<label for="tag">Affiliate Tag:</label><input type="text"
		         name=" <?php echo $this->get_field_name( 'tag' ); ?>"
		id="<?php echo $this->get_field_id( 'tag' ); ?>" value="<?php echo esc_attr($tag); ?>"> <br>
	<?php
	}

The back-end display of the widget is a very simple form with one input field for an Affiliate Tag.

Saving the Widget Values

	public function update( $new_instance, $old_instance ) {
		// processes widget options to be saved
		$instance             = $old_instance;
		$instance['tag']     = sanitize_text_field( $new_instance['tag'] );
		return $instance;
	}

If you don’t explicitly save the form value(s) in the update() method, then the form data won’t be saved. So, make sure you “wire it up” in this way. Once again, you see that we sanitized the input data for security reasons.

The Helper Function

	private function getAmazonUrl($asin, $affiliate_tag) {
		return "http://www.amazon.com/dp/" . $asin . "?tag=" . $affiliate_tag;
	}

Whenever possible, you should use helper functions to do small jobs. This follows the DRY programming principle: Don’t Repeat Yourself. This allows for reusable code and makes test driven development (TDD) possible. Perhaps you later decide to extend this plugin to display links in other ways. You can change how the url is generated by simply changing one function. Or maybe you want to use this plugin on your Amazon UK site and need to change it to amazon.co.uk. Better yet would be to provide an option to specify the country (Amazon has numerous international sites).

The Finishing Touches

Lsw_Amazon::instance();

At the bottom of your plugin file, instantiate your class. If you don’t instantiate it, your plugin won’t run! Remember that we used that Singleton pattern so we don’t use the new keyword.

Ways this could be improved

This plugin is fully functioning now, but we could make it prettier and give it more options!

  • This plugin needs some CSS. Learn how to enqueue a CSS stylesheet.
  • Document your code! I omitted the PHPDoc to make the snippets smaller.

Get the complete code on Github

Here’s the finished product that we developed in this tutorial.

Troubleshooting Tips

If you run into an issue developing your WordPress plugin, there are lots of tools for troubleshooting.  One place to start is the WordPress Developer Plugin.  It offers an easy interface for installing a bunch of helpful plugins, including the debug bar and the debug bar console.   Here’s a screenshot I took while I was debugging this plugin with the Debug Bar Console:

screenshot of debug bar console, wordpress plugin for developers

 

More WordPress Plugin Development Guides

Leave a Reply

Your email address will not be published. Required fields are marked *