Main Content
Extending the WP_Post Class – Post Thumbnail

Extending the WP_Post Class

by

In my previous article, "WordPress Blog with Custom iOS Icons", I had mentioned that I'm quasi-extending WordPress' built-in WP_Post class:

You may have noticed that I'm using an Article class which doesn't exist in WordPress. That's because I'm extending the global $post object and using this everywhere in my custom plugin and theme. (Actually, the WP_Post class is defined as final and therefore can't be extended so it's not really extending the object, but you get what I mean.)

So here's how I'm doing it:

Base Value Objects Class

First, I have an abstract class called Base_ValueObjects.

  • All value objects in my plugin extend this class.
  • The constructor (__construct()) is protected so that new objects can't be created externally.
  • The factory() method takes a php object (or array) as input and this object is also protected.
  • The __call() method is a getter/setter for properties. If the property doesn't exist in the class, the value is retrieved from the object that was passed to the factory() method when creating the object.
  • A couple helper functions are included to help with dates and date intervals. Naturally, these helper methods can be overridden by the extending class if necessary.

Here's what that Base_ValueObjects class looks like:

<?php if( ! defined('ABSPATH') && ! defined('WPINC')) { header('Location: /'); }

abstract class Base_ValueObjects {
	// store a reference to the object we're extending
	protected $_value_object;

	// make our constructor protected so that new objects can't be created externally – use the factory() method to create new objects instead
	protected function __construct($value_object)
	{
		/*
			if our object is null, create a new stdClass object
			otherwise type cast our value object into a PHP object
			this will work for arrays and objects, but not anything else
		*/
		$value_object = ($value_object === NULL) ? new StdClass : (object) $value_object;

		// now set our $_value_object variable
		$this->_value_object = $value_object;
	}

	/**
	 * Create an instance of our object.  This way, we can call our static method and chain properties all in one line.
	 *
	 *		$object = Object::factory();
	 *
	 * @return	$this
	 **/
	public static function factory($value_object = NULL)
	{
		$class = get_called_class();
		return new $class($value_object);
	}

	/**
	 * A magic method to SET or GET the values of existing properties
	 *
	 *		$this->id();						// get the id of the object
	 *		$this->id(55);						// set the id of the object
	 *
	 * @param	string	The name of the method/property we called
	 * @param	mixed	The value to set the property to. (Optional.)
	 * @return	mixed	Returns $this when setting values to allow chaining, or returns the value when getting.
	 **/
	public function __call($name, $arguments)
	{
		// if there are no arguments/parameters then just GET the value
	 	if (empty($arguments))
	 	{
	 		// if the property exists in this object, then use that value, otherwise get the value from our $_value_object
	 		$value_object = (property_exists($this,$name)) ? $this : $this->_value_object;

	 		return $value_object->$name;
	 	// otherwise if we have arguments, SET the value (locally)
	 	} else {
	 		$this->$name	= $arguments[0];

			// return $this to allow chaining
	 		return $this;
	 	}
	}

	/**
	 * Either SET or GET a date value.
	 *
	 *		$this->date('date');							// get the 'date' value as a PHP DateTime object
	 *		$this->date('date',NULL,'America/Toronto')		// get the 'date' value as a PHP DateTime object in the 'America/Toronto' timezone
	 *		$this->date('date','1980-10-11');				// set the 'date' value
	 *		$this->date('date',new DateTime);				// set the 'date' value
	 *
	 * @param	string	The name of the property that holds our date value
	 * @param	mixed	Either a DateTime object or a date/time string
	 * @param	string	The name of the timezone to use when create our DateTime object
	 * @return	mixed	Returns $this when setting values to allow chaining, or returns the date value as a PHP DateTime object when getting.
	 **/
	public function date($name, $date = NULL, $timezone = NULL)
	{
		// check if the property exists
		if (property_exists($this, $name) OR property_exists($this->_value_object, $name))
		{
			// if there is no value for $date then just GET the value
		 	if (empty($date))
		 	{
		 		// if our date is already a DateTime object then just return that
		 		// we use '@' here to supress the warning incase it isn't an object (returns false)
		 		if (@get_class($this->$name()) == 'DateTime')
		 		{
		 			return $this->$name(); // let the _call method decide which object to use for the property (don't call the property directly)
				// otherwise, let's convert this to a DateTime object
		 		} else {
		 			// first, let's create our timezone object
		 			$timezone	= ($timezone == NULL)
		 						? new DateTimeZone(date_default_timezone_get())
		 						: new DateTimeZone($timezone);

		 			return new DateTime($this->$name(),$timezone);
		 		}
			// otherwise if we have arguments, SET the value
		 	} else {
				// if we have a timezone, create a DateTime object, otherwise just use the $date specified
		 		$this->$name	= ($timezone == NULL)
		 						? $date
		 						: new DateTime($date,new DateTimeZone($timezone));

				// return $this to allow chaining
		 		return $this;
		 	}
		// if the property doesn't exist, we should throw an exception
		} else {
			$object_class = get_class($this);
			throw new Exception("The property '$name' does not exist in $object_class");
		}
	}

	/**
	 * GET a length value
	 *
	 *		$this->length('length');			// get the 'length' value as a PHP DateTime object
	 *
	 * @param	string	The name of the property that holds our length value
	 * @return	DateInterval	A PHP DateInterval object representing the length
	 **/
	public function length($name)
	{
		// check if the property exists
		if (property_exists($this, $name) OR property_exists($this->_value_object, $name))
		{
			return new DateInterval('P0000-00-00T'.$this->$name());
		// if the property doesn't exist, we should throw an exception
		} else {
			$object_class = get_class($this);
			throw new Exception("The property '$name' does not exist in $object_class");
		}
	}
}

Article Class

The Article class extends the Base_ValueObjects class and includes a bunch of helper functions. Anywhere I need to access the $post object, I use an $article object instead:

$article = Article::factory($post);

Here's what that Article class looks like:

<?php if( ! defined('ABSPATH') && ! defined('WPINC')) { header('Location: /'); }

class Article extends Base_ValueObjects {
	// get the url for our author
	public function author_url()
	{
		// set the user id
		$id		= $this->post_author();

		$url	= get_author_posts_url($id);
		return $url;
	}

	// get the name of our author
	public function author_name($display = NULL)
	{
		// set the user id
		$id		= $this->post_author();

		if ($display == 'display')
		{
			// use the display name
			$name	= get_the_author_meta('display_name',$this->post_author());
		} else {
			// use the full name regardless of what is set as the display name - DEFAULT
			$name	= get_the_author_meta('first_name',$id).' '.get_the_author_meta('last_name',$id);
		}
		return $name;
	}

	/**
	 * Either SET or GET a post meta value
	 *
	 *		$this->post_meta('class');			// get the 'dm_class' post meta
	 *		$this->post_meta('class',$value)	// set the 'dm_class' post meta
	 *
	 * @param	string	The name of the post meta value
	 * @param	string	The value to set our post meta to
	 * @return	mixed	Returns $this when setting values to allow chaining, or returns the post meta value when getting.
	 **/
	public function post_meta($name, $value = NULL)
	{
		// if there is no value then just GET the value
		if (empty($value))
		{
			$post_meta = get_post_meta($this->ID(),$name,true);

			return $post_meta;
		// otherwise if we have a value, let's set it
		} else {
			// update our post meta (or set it if it doesn't yet exist)
			update_post_meta($this->ID(),$name,$value);

			// return $this to allow chaining
			return $this;
		}
	}

	// get or set our metadata
	public function metadata(Metadata $data = NULL)
	{
		// append our prefix to the name.  Also, by appending '_', it makes sure that this field isn't visible as a dropdown in the 'custom meta' box on the post edit screen
		$name		= '_'.Enchufe::get_instance()->prefix.'metadata';

		// if we have no values to set
		if($data == NULL)
		{
			// first get our metadata
			$metadata	= $this->post_meta($name);
			// unserialize our metadata, but if none exists, create a new Metadata object
			$metadata	= (empty($metadata)) ? Metadata::factory() : unserialize($metadata);

			return $metadata;
		} else {
			// serialize then set our metadata
			$data		= serialize($data);
			$this->post_meta($name,$data);
			return $this;	// allow method chaining
		}
	}

	// get our post date in the post's specified timezone
	public function post_date()
	{
		// first get our date in GMT
		$date		= $this->date('post_date_gmt',NULL,'GMT');

		// get the timezone of the post
		$timezone	= $this->metadata()->timezone();

		// now let's get our date in the post's timezone
		$date		= $date->setTimezone($timezone);

		return $date;
	}

	// get our modified date (always in GMT)
	public function modified_date()
	{
		// get our modified date in GMT
		return $this->date('post_modified_gmt',NULL,'GMT');
	}

	// get our post's thumbnail image
	public function thumbnail()
	{
		$thumb_id	= get_post_thumbnail_id($this->ID());

		// if there isn't an article thumbnail then use the default one
		$thumb		= (! empty($thumb_id))
					? wp_get_attachment_url($thumb_id)
					: Enchufe::get_instance()->upload_url_path.'/article_thumb.png';
		return $thumb;
	}

	// get the permalink to our post
	public function permalink()
	{
		return get_permalink($this->ID());
	}

	// get the shortlink to our post
	public function shortlink()
	{
		return Enchufe::get_instance()->home_url.'a/'.$this->metadata()->article_id();
	}

	// determine whether this post is geo-tagged or not
	public function is_geotagged()
	{
		$is_geotagged = ($this->post_meta('geo_latitude') !='' AND $this->post_meta('geo_longitude') !='');
		return $is_geotagged;
	}

	// get the category for this post
	public function category()
	{
		return get_the_category($this->ID())[0]->slug;
	}
}

And here's a quick example of how this can make things easier:

<?php
while (have_posts()) :
	the_post(); $article = Article::factory($post);?>

	<article id="post-<?php echo $article->ID();?>">
		<header class="entry-header">
			<a href="<?php echo $article->permalink();?>">
				<div class="post-thumbnail">
					<img src="<?php echo $article->thumbnail();?>" alt="Article Thumbnail" />
				</div>
				<h1 class="entry-title"><?php echo $article->post_title();?></h1>
			</a><?php

			// only display the geodata if we have it
			if ($article->is_geotagged()) :?>
				<span class="geo">
					<a href="https://maps.google.com/maps?z=15&q=<?php
						echo $article->post_meta('geo_latitude').'+'.$article->post_meta('geo_longitude');?>" title="<?php
						echo $article->post_meta('geo_address');?>" class="place" rel="external"><?php echo $article->post_meta('geo_address');?> 
					</a>
				</span><?php
			endif;?>
		</header>
		…
		…
	</article><?php
endwhile;?>

This is just a very quick example, obviously. These two classes (extend-post.zip) are just a starting point… I hope this helps!