Blurbette Plugin: Metabox Abstract


One of the biggest benefits of OOP is the ability to extend classes. Depending on how the classes are organized and structured, one class can take care of a lot of tedious or repetitive work, enabling you to benefit by extending it and simply defining a few additional elements.

An abstract class is one that can only be extended — it cannot be instantiated until you extend it and add a few necessary elements.

The purpose of this chapter is to create an abstract class that lumps together a lot of repetitive tasks required to build a metabox. The result will be a reusable tool you can use to quickly and easily create metaboxes in the future, requiring only a handful of arguments upon instantiation, one required method to be defined, and two optional hook callbacks. In addition, you can add any number of additional properties and methods to your extending class.

	abstract class WPCX_Abs_Metabox {
		protected $noncename = 'wpcx_nonce';
		protected $nonceval;
		protected $capability;
		protected $post_types;
		protected $priority;
		protected $title;
		protected $domid;

The keyword abstract signals PHP this is an abstract class. As you’ll see later, methods can also be abstract — they must be named but not defined (no curly braces). Be aware that if a single method is abstract, the whole class must also be abstract (and include this keyword).

Abstract classes can define constants; however, in this class, there are none. Even though other classes define nonce name, nonce value and capability as constants, it’s not appropriate here.

WordPress metaboxes are thoroughly documented here.

		function __construct( array $opts ) {
			if ( empty( $opts['title'] ) ) :
				throw new Exception('Metabox title not specified.');
			endif;
			$opts = wp_parse_args( $opts, array(
				'title'			=> null,
				'post_types'	=> array('post'),
				'context'		=> 'side',
				'priority'		=> 'default',
				'capability'	=> 'edit_post',
				'domid'			=> uniqid('wpcxid'),
				'noncename'		=> null
			));
			foreach( $opts as $k => $v ) :
				$this->$k = $v;
			endforeach;
			if ( empty( $this->nonceval ) ) :
				$this->nonceval = plugin_basename( __FILE__ ) . __CLASS__ . get_current_user_id();
			endif;
			if ( empty( $opts['noncename'] ) ) :
				$this->create_noncename();
			endif;
			$this->do_all_hooks();
		}

This class expects an $opts array upon construction, with several optional keys.

  • title is the only one required; it’s displayed as the title of the metabox.
  • post_types, an array of post types on whose admin panels this will appear. Default is only post.
  • context: ‘normal’, ‘advanced’, or ‘side’.
  • priority: ‘high’, ‘core’, ‘default’ or ‘low’.
  • capability: the capability required to display the metabox, default ‘edit_post’.
  • domid: the HTML tag id assigned to the metabox, useful to define if you plan to access via Javascript; generated by default.
  • noncename can also be defined if you desire, but will be generated by default.

Other properties may be passed through, and will be assigned as defined; your extending class can make use of them as needed.

		protected function do_all_hooks() {
			add_action( 'add_meta_boxes', array( $this, 'add_meta_box' ) );
			add_action( 'admin_enqueue_scripts', array( $this, 'q_admin_scripts' ) );
			add_action( 'save_post', array( $this, 'save_meta_box' ) );
		}

The familiar do_all_hooks() method, setting up the three most common hooks required to define a metabox. As you’ve seen, you could expand on this method — say by adding ajax hooks — by defining it in your child class and calling parent::do_all_hooks().

The add_meta_box() callback is defined next. The other two, q_admin_scripts() and save_meta_box() are defined later, but are void functions. This means, if your metabox does not need to perform one of those actions — e.g. there are no script files to enqueue or data doesn’t need to be saved — you can omit those methods entirely from your extending class.

		public function add_meta_box() {
			if ( ! is_array( $this->post_types ) ) :
				$this->post_types = array( $this->post_types );
			endif;
			foreach( $this->post_types as $post_type ):
				add_meta_box( $this->domid, $this->title, array( $this, 'output_meta_box' ), $post_type, $this->context, $this->priority );
			endforeach;
		}

Making use of the $opts passed upon construction, one new metabox is defined for each specified post type.

The output_meta_box method will be covered below; next are a few helper methods that will streamline certain processes:

		protected function create_noncename() {
			$this->noncename = 'wpcx_' . str_rot13( strtolower( get_class( $this ) ) );
		}

Called by default in __construct(), defines a nonce name unique for the extending class. Notice the use of get_class( $this ), because __CLASS__ would always output ‘WPCX_Abs_Metabox’.

		protected function okay_to_save( $postid ) {
			// bail with common conditions
			if ( ! is_numeric( $postid ) || ! $postid ) return false;
			if ( ! current_user_can( $this->capability, $postid ) ) return false;
			if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) return false;
			if ( ! isset( $_POST[$this->noncename] ) ) return false;
			if ( ! wp_verify_nonce( $_POST[$this->noncename], $this->nonceval ) ) return false;
		    return true;
		}

If you define a save_meta_box() method, you can call this method to run several checks on common conditions, returning true if they all pass. It requires a noncefield, defined with current properties (next method).

Important: Your save_meta_box() method must still filter incoming data appropriately.

		protected function noncefield() {
			wp_nonce_field( $this->nonceval, $this->noncename );
		}

The next two methods are void, so your class needn’t define them if the functionality isn’t required.

		public function q_admin_scripts() {}
		
		public function save_meta_box( $postid ) {}

If you define the save_meta_box( $postid ) method, remember to include the $postid arg.

Finally, the single method your class must define:

		abstract public function output_meta_box( $post );
	} // end class WPCX_Abs_Metabox

Notice the abstract keyword, and the lack of definition or curly braces. This is an action hook, so the $post arg is always passed to it; your extending class’ method will need to accept a $post arg too.

Extending the class

Most of the tedious work of defining a metabox has been taken care of, freeing you to extend this class and focus on the meaningful parts. For example, you could define an information-only metabox that simply displays the current post’s ID number, with just a few lines of code:

class My_ID_Metabox extends WPCX_Abs_Metabox {
	function output_meta_box( $post ) {
		echo "This post's ID is <b>$post->ID</b>.";
	}
}
$my_id_metabox = new My_ID_Metabox( array( 'title' => 'Post ID' ) );

The only requirements are:

  • You define the output_meta_box( $post ) method.
  • You provide a title to the constructor.

Recapping the code

Here is the entire WPCX_Abs_Metabox code:

<?php
	abstract class WPCX_Abs_Metabox {
		
		protected $noncename = 'wpcx_nonce';
		protected $nonceval;
		protected $capability;
		protected $post_types;
		protected $priority;
		protected $title;
		protected $domid;
		
		function __construct( array $opts ) { //$title, array $post_types = array('post'), $priority='side', $domid=null) {
			if ( empty( $opts['title'] ) ) :
				throw new Exception('Metabox title not specified.');
			endif;
			$opts = wp_parse_args( $opts, array(
				'title'			=> null,
				'post_types'	=> array('post'),
				'context'		=> 'side',
				'priority'		=> 'default',
				'capability'	=> 'edit_post',
				'domid'			=> uniqid('wpcxid'),
				'noncename'		=> null
			));
			foreach( $opts as $k => $v ) :
				$this->$k = $v;
			endforeach;
			if ( empty( $this->nonceval ) ) :
				$this->nonceval = plugin_basename( __FILE__ ) . __CLASS__ . get_current_user_id();
			endif;
			if ( empty( $opts['noncename'] ) ) :
				$this->create_noncename();
			endif;
			$this->do_all_hooks();
		}

		protected function do_all_hooks() {
			add_action( 'admin_enqueue_scripts', array( $this, 'q_admin_scripts' ) );
			add_action( 'add_meta_boxes', array( $this, 'add_meta_box' ) );
			add_action( 'save_post', array( $this, 'save_meta_box' ) );
		}
		
		public function add_meta_box() {
			if ( ! is_array( $this->post_types ) ) :
				$this->post_types = array( $this->post_types );
			endif;
			foreach( $this->post_types as $post_type ):
				add_meta_box( $this->domid, $this->title, array( $this, 'output_meta_box' ), $post_type, $this->context, $this->priority );
			endforeach;
		}
		
		protected function create_noncename() {
			$this->noncename = 'wpcx_' . str_rot13( strtolower( get_class( $this ) ) );
		}
		
		protected function okay_to_save( $postid ) {
			// bail with common conditions
			if ( ! is_numeric( $postid ) || ! $postid ) return false;
			if ( ! current_user_can( $this->capability, $postid ) ) return false;
			if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) return false;
			if ( ! isset( $_POST[$this->noncename] ) ) return false;
			if ( ! wp_verify_nonce( $_POST[$this->noncename], $this->nonceval ) ) return false;
		    return true;
		}
		
		protected function noncefield() {
			wp_nonce_field( $this->nonceval, $this->noncename );
		}
		
		public function q_admin_scripts() {}
		
		public function save_meta_box( $postid ) {}
		
		abstract public function output_meta_box( $post );
	} // end class WPCX_Abs_Metabox