Building a WordPress Plugin: Multiple Featured Images

I worked on a WordPress site recently where the client needed to associate two different images with each piece of content. I couldn’t use a single image with a custom size because the images’ aspect ratios were so different.

I knew that I could use WordPress’s Featured Image functionality for the first image, but I wasn’t quite sure what to do for the second. I needed it to be as easy as possible to use because my client was non-technical. I looked for an existing solution by searching the WordPress plugin repository. I found a plugin that seemed promising, but Multiple Featured Images hasn’t been updated since May 2012 and didn’t support the media picker interface introduced in WordPress 3.5.

Because I knew this was something I would use on future projects, I decided to build my own plugin.

Building the Plugin

The plugin provides an interface that a developer can use to add multiple named image pickers to any type of content and access the images within the site’s theme. It uses the new media picker interface introduced in 3.5.

Laying the Foundation

Every WordPress plugin I build starts from the same basic plugin skeleton. If you’re interested in using my skeleton, feel free to download it from GitHub.

My plugin skeleton is ridiculously simple – it is a class with exclusively static methods. The class acts as a pseudo namespace that encapsulates the functionality of my plugin and ensures there are no function naming collisions. There are a million different ways to structure a WordPress plugin, but my skeleton has worked well for me over the past few years.

After forking my skeleton for this project, I renamed the plugin class to something appropriate (MFI_Reloaded) and changed all other strings as necessary.

Defining the Public Interface

Since this plugin is going to be used by other developers, the first thing I wanted to do is define a well thought out interface. The following is what I came up with:

function mfi_reloaded_add_image_picker($name, $args = array());

function mfi_reloaded_has_image($name, $post_id = null);

function mfi_reloaded_get_image_id($name, $post_id = null);

function mfi_reloaded_get_image($name, $size = 'thumbnail', $post_id = null, $attributes = array());
function mfi_reloaded_the_image($name, $size = 'thumbnail', $post_id = null, $attributes = array());

This interface accomplishes both goals I had for the plugin. Developers can register new named image pickers using mfi_reloaded_add_image_picker and access saved information with the rest.

With this interface, I consciously tried to stay close to the analogous functions provided by the WordPress post thumbnail template tags.

Implementing the Public Interface

Now that there is an interface for telling the plugin what image pickers to display, I need a way to store that data in the plugin. For the sake of simplicity, I decided to use an associative array which I added as a private static field on our plugin class.

At the top of my plugin class I added the following:

private static $image_pickers = array();

Now, I needed to actually make the template tags defined earlier do something. When I build template tags, I tend to make all “getters” and “setters” simply delegate to public static methods on the class. That’s what I did here.

At the bottom of my plugin class I added the following:

public static function add_image_picker($name, $args) {
	return false;
}

public static function get_image_id($name, $post_id) {
	return false;
}

public static function get_image($name, $size, $post_id, $attributes) {
	return false;
}

I started here by implementing add_image_picker. This could have been as easy as adding $args as a new item on the previously defined $image_pickers field with $name as the array key. In the interest of making this plugin more robust, however, I went a little bit further:

  1. Return false immediately if the image picker name is not a string or if an image picker with that name has been previously registered
  2. Normalize the data passed in the $args parameter given the defaults specified in the template tag documentation
  3. Set $image_pickers[$name] = $normalized_args

Here is what this method ended up looking like:

public static function add_image_picker($name, $args) {
	if(!is_string($name) || isset(self::$image_pickers[$name])) {
		return false;
	}

	self::$image_pickers[$name] = self::_normalize_args($args);
}

I needed to normalize the arguments so I created the helper method named _normalize_args to do so. I usually prefix helper methods like this with an underscore (as you can see). That method reads as follows:

private static function _normalize_args($args) {
	$normalized_args = array();

	if(!isset($args['post_types'])) {
		$normalized_args['post_types'] = array('post', 'page');
	} else if(!is_array($args['post_types'])) {
		$normalized_args['post_types'] = array($args['post_types']);
	} else {
		$normalized_args['post_types'] = $args['post_types'];
	}

	$default_labels = array(
		'name' => __('Featured Image'),
		'set' => __('Set featured image'),
		'remove' => __('Remove featured image'),
		'popup_title' => __('Set Featured Image'),
		'popup_select' => __('Set featured image'),
	);

	if(!isset($args['labels']) || !is_array($args['labels'])) {
		$normalized_args['labels'] = $default_labels;
	} else {
		$normalized_args['labels'] = shortcode_atts($default_labels, $args['labels']);
	}

	return $normalized_args;
}

For now, that’s all I could really implement. I wasn’t storing any data yet so there was no way to return anything meaningful.

Adding the Meta Boxes

Once the public API was defined, the next step was to show the user an interface they could use. It won’t be fully functional at first, but it will look like correct.

To register our meta boxes, we first add a callback to the add_meta_boxes action. In my add_actions method (inside the is_admin conditional), I added the following code:

add_action('add_meta_boxes', array(__CLASS__, 'add_image_picker_meta_boxes'));

Then, I added the following public static method inside of my class:

public static function add_image_picker_meta_boxes($post_type) {
	foreach(self::$image_pickers as $image_picker_name => $image_picker_args) {
		if(in_array($post_type, $image_picker_args['post_types'])) {
			add_meta_box(
				'mfi-reloaded-' . sanitize_title_with_dashes($image_picker_name),
				$image_picker_args['labels']['name'],
				array(__CLASS__, 'display_image_picker_meta_box'),
				$post_type,
				'side',
				'default',
				compact('image_picker_name', 'image_picker_args')
			);
		}
	}
}

This method iterates over each registered image picker and checks to see if it is registered for the post type currently being edited. If it is, it adds a meta box by calling add_meta_box with the appropriate arguments.

In order to actually display the meta box, we need to implement the display_image_picker_meta_box method. In my plugin class, I added another public static method with that name that displays some output:

public static function display_image_picker_meta_box($post, $meta_box) {
	$image_picker_args = $meta_box['args']['image_picker_args'];
	$image_picker_name = $meta_box['args']['image_picker_name'];

	$image_id = mfi_reloaded_get_image_id($image_picker_name, $post->ID);
	$image = mfi_reloaded_get_image($image_picker_name, 'full', $post->ID);

	include('views/meta-boxes/image-picker.php');
}

We’re using the unimplemented public interface that we stubbed out earlier to grab the full size image for the picker in question (if it exists). We’ll be using this later. When printing HTML, I prefer to include a separate file (as you can see in the method above). The entirety of that file is as follows:

<div class="mfi-reloaded-image-picker"
		data-mfi-reloaded-image-id="<?php printf('%d', $image_id); ?>"
		data-mfi-reloaded-name="<?php esc_attr_e($image_picker_name); ?>"
		data-mfi-reloaded-select="<?php esc_attr_e($image_picker_args['labels']['popup_select']); ?>"
		data-mfi-reloaded-title="<?php esc_attr_e($image_picker_args['labels']['popup_title']); ?>">

	<div class="mfi-reloaded-image-picker-preview"></div>

	<a class="mfi-reloaded-image-picker-remove" href="#"><?php esc_html_e($image_picker_args['labels']['remove']); ?></a>
	<a class="mfi-reloaded-image-picker-set" href="#"><?php esc_html_e($image_picker_args['labels']['set']); ?></a>
</div>

This code outputs a container for the thumbnail preview and two links that allow the user to take action. Of course, nothing happens yet.

Adding the Image Picking Functionality

The media picker is controlled via JavaScript and styled with CSS so I needed to enqueue the appropriate scripts and styles in the administrative panel on the editing pages. To do so, I hooked into admin_enqueue_scripts by adding the following line inside of my add_actions method (in the is_admin conditional):

add_action('admin_enqueue_scripts', array(__CLASS__, 'enqueue_administrative_resources'));

The enqueue_administrative_resources callback is responsible for checking if the post editing screen is being viewed and, if so, enqueueing the appropriate scripts.

public static function enqueue_administrative_resources() {
	$screen = get_current_screen();

	if('post' === $screen->base) {
		wp_enqueue_media();

		wp_enqueue_script('mfi-reloaded', plugins_url('resources/backend/mfi-reloaded.js', __FILE__), array('jquery'), self::VERSION, true);
		wp_enqueue_style('mfi-reloaded', plugins_url('resources/backend/mfi-reloaded.css', __FILE__), array(), self::VERSION);
	}
}

The stylesheet is pretty simple as it just prevents the preview image from flowing outside the bounds of its container:

.mfi-reloaded-image-picker-preview img {
	height: auto;
	max-width: 100%;
}

The JavaScript file is a little more complex, but I’ve added comments liberally where I think there could be confusion. I encourage you to read the source if you’re interested in what is going on:

jQuery(document).ready(function($) {
	// Register a click event handler on the remove links
	$('.mfi-reloaded-image-picker-remove').click(function(event) {
		event.preventDefault();

		var $remove = $(this),
			$container = $remove.parents('.mfi-reloaded-image-picker'),
			$preview = $remove.siblings('.mfi-reloaded-image-picker-preview'),
			$set = $remove.siblings('.mfi-reloaded-image-picker-set'),
			_name = $container.data('mfi-reloaded-name');

		// Initiate an AJAX request to remove the image id for this image picker
		$.post(
			ajaxurl,
			{
				action: 'mfi_reloaded_set_image_id',
				image_id: 0,
				name: _name,
				post_id: $('#post_ID').val()
			},
			function(data, status) { }
		);

		// Hide the link that allows a user to remove the image
		$remove.hide();

		// Remove the preview thumbnail because it is no longer valid
		$preview.empty();

		// Show the link that allows a user to set the image
		$set.show();
	});

	// Register a click event handler in order to show the media picker
	$('.mfi-reloaded-image-picker-set').click(function(event) {
		event.preventDefault();

		var $set = $(this),
			$container = $set.parents('.mfi-reloaded-image-picker'),
			$preview = $set.siblings('.mfi-reloaded-image-picker-preview'),
			$remove = $set.siblings('.mfi-reloaded-image-picker-remove'),
			_name = $container.data('mfi-reloaded-name'),
			_select = $container.data('mfi-reloaded-select'),
			_title = $container.data('mfi-reloaded-title');

		// Set up the media picker frame
		var mfi_reloaded_frame = wp.media({
			// Open the media picker in select mode only
			frame: 'select',

			// Only allow a single image to be chosen
			multiple: false,

			// Set the popup title from the HTML markup we output for the active picker
			title: _title,

			// Only allow the user to choose form images
			library: { type: 'image' },

			button: {
				// Set the button text from the HTML markup we output for the active picker
				text:  _select
			}
		});

		mfi_reloaded_frame.on('select', function(){
			var media_attachment = mfi_reloaded_frame.state().get('selection').first().toJSON();

			// Initiate an AJAX request to set the image id for this image picker
			$.post(
				ajaxurl,
				{
					action: 'mfi_reloaded_set_image_id',
					image_id: media_attachment.id,
					name: _name,
					post_id: $('#post_ID').val()
				},
				function(data, status) { }
			);

			// Add the image to the preview container
			$preview.append($('<img />').attr('src', media_attachment.sizes.full.url).attr('alt', media_attachment.title));
		});

		// Show the remove link
		$remove.show();

		// Hide the set link
		$set.hide();

		mfi_reloaded_frame.open();
	});

	$('.mfi-reloaded-image-picker').each(function(index, element) {
		var $container = $(element),
			$preview = $container.children('.mfi-reloaded-image-picker-preview'),
			$remove = $container.children('.mfi-reloaded-image-picker-remove'),
			$set = $container.children('.mfi-reloaded-image-picker-set');

		if(0 === $preview.children().size()) {
			$remove.hide();
		} else {
			$set.hide();
		}
	});
});

Now that the JavaScript was in place and working, I need to make sure that the user’s choices were persisted.

Saving and Loading Data

When I’m saving data for a post, I generally like to create two small wrapper methods that get and set the data. That’s exactly what I did for this plugin:

private static function _get_meta($post_id, $meta_key = null) {
	$post_id = empty($post_id) && in_the_loop() ? get_the_ID() : $post_id;

	$meta = get_post_meta($post_id, 'rfi-reloaded-images', true);
	
	if(!is_array($meta)) {
		$meta = array();
	}

	return is_null($meta_key) ? $meta : (isset($meta[$meta_key]) ? $meta[$meta_key] : false);
}

private static function _set_meta($post_id, $meta) {
	$post_id = empty($post_id) && in_the_loop() ? get_the_ID() : $post_id;

	update_post_meta($post_id, 'rfi-reloaded-images', $meta);

	return $meta;
}

Now that these were in place, I could address the AJAX call that was currently being unhandled. To start, I added an action in my add_actions method for the ajax call:

add_action('wp_ajax_mfi_reloaded_set_image_id', array(__CLASS__, 'ajax_mfi_reloaded_set_image_id'));

Then, I created the callback and made sure it checked permissions for the post being modified and then persisted data appropriately.

public static function ajax_mfi_reloaded_set_image_id() {
	$data = stripslashes_deep($_REQUEST);

	$image_id = $data['image_id'];
	$name = $data['name'];
	$post_id = $data['post_id'];

	if($post_id && current_user_can('edit_post', $post_id)) {
		$images = self::_get_meta($post_id);

		if(empty($image_id)) {
			unset($images[$name]);
		} else {
			$images[$name] = $image_id;
		}

		self::_set_meta($post_id, $images);
	}

	exit;
}

Finishing the Public Interface

Now that we’ve started saving data, we can finally implement the rest of the public interface. I started with the get_image_id method in the plugin class.

public static function get_image_id($name, $post_id) {
	return self::_get_meta($post_id, $name);
}

I just used the wrapper function I created earlier to pull out the appropriate information from the serialized data and returned it.

Next, I implemented the get_image method, which was straightforward enough now that I had the ID.

public static function get_image($name, $size, $post_id, $attributes) {
	$image_id = self::get_image_id($name, $post_id);

	if($image_id) {
		return wp_get_attachment_image($image_id, $size, false, $attributes);
	}

	return false;
}

And that was it, everything was working like I wanted it to.

Using the Multiple Image Picker

Now that the plugin is done, using it is easy. For testing, I put the following code into a file in /wp-content/mu-plugins/

<?php

add_action('init', function() {
	if(function_exists('mfi_reloaded_add_image_picker')) {
		mfi_reloaded_add_image_picker('hero-image', array(
			'post_types' => array('post'),
			'labels' => array(
				'name' => __('Hero Image'),
				'set' => __('Set hero image'),
				'remove' => __('Remove hero image'),
				'popup_title' => __('Set Hero Image'),
				'popup_select' => __('Set hero image'),
			),
		));

		mfi_reloaded_add_image_picker('sidekick-image', array(
			'post_types' => array('post'),
			'labels' => array(
				'name' => __('Sidekick Image'),
				'set' => __('Set sidekick image'),
				'remove' => __('Remove sidekick image'),
				'popup_title' => __('Set Sidekick Image'),
				'popup_select' => __('Set sidekick image'),
			),
		));
	}
});

This registers two image pickers for use on posts only. I then used the mfi_reloaded_the_image to display the images for posts in the loop.

If you’re interested in the plugin I built, download it from GitHub. If you’ve got suggestions for improvements, I’d love to see a pull request for patch. Let me know how you plan on using it in the comments!

12 thoughts on “Building a WordPress Plugin: Multiple Featured Images

  1. Anna

    Great Job :) Thanks a lot !
    I’ve been searching a solution like that for a month, because I don’t like to use out-of-box plugins and I’m really pensioned to know HOW THE STUFF WORKS :)

    THANKS AGAIN !

    p.s. Can you pleas send me weekly/monthly updates about your publications? I’ll be very happy to read mote from you :)

    Reply
    1. Nick Ohrn Post author

      Anna – thanks for letting me know this helped you. I’m glad it was valuable for someone! I’ll see what I can do about updating you on my writing. Thanks for asking!

      Reply
  2. Kye

    Hey Nick,

    Just want to say thank you for your work in creating this. Its been really helpful in helping me develop a client site that I am working on. On top of that when I needed a little bit of help with something relating to this, you were more than happy to answer my questions.

    Im a designer by trade, Im moving more into freelance and as the coding requirements become too much for me to manage myself it could be interesting to see if we could partner on some projects as and when the requirements crop up. Once I have done this client site I will send you some links so you can see what stuff Ive done.

    Do you have the time to fit some occasional freelance coding jobs in?

    All the best and thanks again.

    Kye.

    Reply
    1. Nick Ohrn Post author

      Kye – I’m glad my plugin helped and I was happy to answer your questions! I’ll email you regarding freelance opportunities / possibilities of working together.

      Reply
  3. Bryan

    Hey Nick,

    I’ve successfully added the new image-picker to my WordPress post pages, but can’t get the new image to display on the front-end of the site. Can you clarify how to display them for me?

    Reply
    1. Nick Ohrn Post author

      Bryan, did you take a look at the readme.txt file? In it, there are some details about how to use the template tags to retrieve and display the additional featured images that you specified within your theme. Also look in /lib/template-tags.php inside of the plugin’s directory. There are comments in that file that explain exactly how to use the various template tags. The one you’re probably looking for is mfi_reloaded_get_image.

      Reply
  4. Sharon Murphy

    The readme.txt file describes what to do to add blocks within the functions.php file. I did take a look at the template-tags.php file as well, but for me the instructions aren’t quite clear. I don’t see details on what to add to, say, the page.php theme file. I’ve tried:

    get_post_meta(get_the_ID(), 'hero-image', true);

    and

    wp_get_attachment_image_src(get_the_ID(),'hero-image');

    with no luck. If you could offer a little more info, it would be greatly appreciated. Thank you.

    Reply
    1. Nick Ohrn Post author

      Sharon – sorry for the late response. I’ve been away from the computer for a while.

      If you want to get the information about a particular image assigned to a post (in the loop) you can do something like this:

      $image_information = wp_get_attachment_image_src(mfi_reloaded_get_image_id('image-slug', get_the_ID()), 'image-size-key');

      Let me know if that works!

      Reply
  5. Oscar

    Hi,

    I was just searching for DIY tutorials for my project and I must say. this was very helpful. Great Job, honestly. Thank you. A question thou? Is there a way to achieve this with theme functions?

    Cheers
    Oscar

    Reply
    1. Nick Ohrn Post author

      Oscar, I’m sorry for the delayed response. You could definitely adapt the plugin to live within a WordPress theme, but that is somewhat out of the scope of what I can cover comfortably in a comment. Feel free to email me if you need more information.

      Reply

Leave a Reply

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