Blurbette Plugin: Organization and First Classes


Almost ready to start building. I’ll first decide how the files within the plugin directory are going to be organized.

WordPress requires at least one ‘main’ PHP file within a plugin directory, which begins with a standardized header. This header is essentially a copy-and-pastable ‘form’ you can replace with descriptive information; mine looks like this:

<?php
/**
**************************************************************************
Plugin Name:  Blurbettes
Plugin URI:   http://www.wpcraftsman.com
Description:  Repurposable text clips which can be included within posts, pages &amp; widgets.
Version:      1.0.0
Author:       Dave Bushnell
Author URI:   http://www.wpcraftsman.com
**************************************************************************/

View the WordPress documentation on plugins; a few more lines may be included here, e.g. localization information.

There may be any number of files within this directory; they’re required or included by this ‘main’ file.

I’ve chosen to place only one PHP file at the top of this directory, and to enclose class PHP files within a ‘classes’ sub-directory. In later chapters, I’ll enclose Javascript and CSS files in a separate sub-folder.

These class files will stick to a strict naming convention, in this case, the lowercased class name (with a .php extension). By sticking to this convention, I can register an autoload function so that I never have to use require statements to load class files — PHP will call my function automatically.

In this ‘main’ PHP file (mine is named ‘blurbette.php’), it’s important to block direct access. Someone could simply visit a script file’s URI; even though an error would likely be thrown if it uses any WordPress-defined functions, a hacker could learn something about the filesystem or data by reading that message. We block access by testing for the presence of some function or constant defined by WordPress:

if ( ! defined( 'ABSPATH' ) ) die( 'Nothing to see here' );

Building the Classes

Now I can start building. Remember our ‘blueprint’ from the last chapter?

  1. A registry class (manages all the other classes)
  2. A definition class
  3. Shortcode class
  4. Tiny MCE button class
  5. Widget class
  6. Settings control panel class
  7. Abstract metabox class
  8. Post “copy” metabox class
  9. Blurbette options metabox class

I think it’ll be clearer if I start with the Definition class, then back up and create the Registry class. In later chapters, I’ll return to these classes and add to them as needed.

The Definition Class

My aim is to keep the Definition class completely static; all its members can be accessed without instantiating. Its purpose is to define all the global particulars of a Blurbette.

class WPCX_Blurbette_Def {
	const POST_TYPE = 'wpcx_blurbette';
	const TEXT_DOMAIN = 'wpcx_blurbette';

} // end of class WPCX_Blurbette_Def

I’ve chosen to define a couple of constants, which I’ll invoke throughout all plugin files: 1) POST_TYPE: the name of the custom post type, 2) TEXT_DOMAIN: the localization text domain used in WordPress’ __() and _e() functions.

By defining these as constants, it’s easy to change all instances later if needed. Plus, they’re easier to remember! If I had ten different classes which define post types, wouldn’t it be easier to simply refer to each as Whatever_Class::POST_TYPE, instead of having to recall the specific name for each?

Next, I’ll add the standard post type registration method. Below, I’ll hook this method into the ‘init’ action.

	public static function register_cpt() {
		register_post_type( self::POST_TYPE,
			array(
				'labels' 		=> array(
					'name' 			=> __( 'Blurbettes', self::TEXT_DOMAIN ),
					'singular_name'	=> __( 'Blurbette', self::TEXT_DOMAIN ),
					'edit_item' 	=> __( 'Edit Blurbette', self::TEXT_DOMAIN ),
					'view_item' 	=> __( 'View as Standalone Page', self::TEXT_DOMAIN ),
					'add_new_item'	=> __( 'Add New Blurbette', self::TEXT_DOMAIN ),
					'new_item'		=> __( 'New Blurbette', self::TEXT_DOMAIN )
				),
				'public' 		=> false,
				'show_ui' 		=> true,
				'has_archive' 	=> false,
				'hierarchical'	=> true,
				'menu_icon'		=> 'dashicons-text',
				'supports' 		=> array( 'title', 'editor' )
			)
		);
	}

} // end of class WPCX_Blurbette_Def

In many of my classes, I like to define a do_all_hooks() method, which adds action and filter hooks for the class’ methods.

	public static function do_all_hooks() {
		add_action( 'init', array( __CLASS__, 'register_cpt' ) );
	}

} // end of class WPCX_Blurbette_Def

I must use __CLASS__ (or simply the name of this class) when assigning static method callbacks; self won’t work here.

That’s enough Definition for now. As I go along, the WPCX_Blurbette_Def class will grow, gaining more constants and static methods.

The Registry Class

The Registry class is the only one of our classes that will be instantiated as a named (object) variable — we’ll call it $WPCX_Blurbette. The rest will become instantiated as properties of this object. That’s one advantage of adopting a Registry pattern — only one variable is added to the global scope.

class WPCX_Blurbette_Registry {
	public $objects = array();

	function __construct() {
		WPCX_Blurbette_Def::do_all_hooks();
	}
} // end of class WPCX_Blurbette_Registry

The $objects property is where all the rest of the instantiated objects will live. When I instantiate the Registry later, the magic method __construct() will execute, causing the Definition class to register its hooks.

What good is a registry that doesn’t register? I’ll go back and add the autoloader method, and a register method so I can easily instantiate new classes as necessary.

	public function class_autoloader( $classname ) {
		if ( strpos( $classname, 'WPCX_' ) !== 0 ) return null;
		require_once plugin_dir_path( __FILE__ ) . 'classes/' . strtolower( $classname ) . '.php';
	}

	public function register( $classname, $opts = null ) {
		$this->objects[$classname][] = new $classname( $opts );
	}
	
} // end of class WPCX_Blurbette_Registry

The autoloader method must be publicly visible, because it’ll be assigned to a callback for use outside the class. It’s important to ‘bail’ if the $classname isn’t one of ours, because when you register an autoloader in PHP, all class calls (throughout WordPress) visit this function.

I’ve also chosen to make the register method public, so something like $WPCX_Blurbette->register('WPCX_Some_Class') will work from anywhere. But that’s simply an arbitrary choice because this isn’t a callback method and will always be called directly. If I was sure register would always be called only from within the $WPCX_Blurbette object I could confidently make it protected or private.

Notice each instance of $this->objects[$classname] is an array, so I can add any number of $classname objects. For instance, I’ll define a TinyMCE button class (WPCX_MCE_Control) later; I could use the same class to define multiple buttons, and instantiate them here — they would exist in the array $WPCX_Blurbette->objects['WPCX_MCE_Control'].

The only thing missing is to register the autoloader, within the __construct() method.

	function __construct() {
		spl_autoload_register( array( $this, 'class_autoloader' ) );
		WPCX_Blurbette_Def::do_all_hooks();
	}

Lastly, I’ll instantiate the Registry class. I add this at the very end of the file, outside the classes:

$WPCX_Blurbette = new WPCX_Blurbette_Registry();

As far as WordPress is concerned, this plugin is complete. Of course it doesn’t do anything, other than register a new post type. As I build new classes, I’ll return to these Definition and Registry classes to expand as needed.

Recapping the code

Here’s the whole ‘blurbette.php’ file (so far), including everything I’ve built in this chapter:

<?php
/**
**************************************************************************
Plugin Name:  Blurbettes
Plugin URI:   http://www.wpcraftsman.com
Description:  Repurposable text clips which can be included within posts, pages &amp; widgets.
Version:      1.0.0
Author:       Dave Bushnell
Author URI:   http://www.wpcraftsman.com
**************************************************************************/



if ( ! defined( 'ABSPATH' ) ) die( 'Nothing to see here' );

class WPCX_Blurbette_Def {
	const POST_TYPE = 'wpcx_blurbette';
	const TEXT_DOMAIN = 'wpcx_blurbette';

	public static function register_cpt() {
		register_post_type( self::POST_TYPE,
			array(
				'labels' 		=> array(
					'name' 			=> __( 'Blurbettes', self::TEXT_DOMAIN ),
					'singular_name'	=> __( 'Blurbette', self::TEXT_DOMAIN ),
					'edit_item' 	=> __( 'Edit Blurbette', self::TEXT_DOMAIN ),
					'view_item' 	=> __( 'View as Standalone Page', self::TEXT_DOMAIN ),
					'add_new_item'	=> __( 'Add New Blurbette', self::TEXT_DOMAIN ),
					'new_item'		=> __( 'New Blurbette', self::TEXT_DOMAIN )
				),
				'public' 		=> false,
				'show_ui' 		=> true,
				'has_archive' 	=> false,
				'hierarchical'	=> true,
				'menu_icon'		=> 'dashicons-text',
				'supports' 		=> array( 'title', 'editor' )
			)
		);
	}

	public static function do_all_hooks() {
		add_action( 'init', array( __CLASS__, 'register_cpt' ) );
	}

} // end of class WPCX_Blurbette_Def



class WPCX_Blurbette_Registry {
	public $objects = array();

	function __construct() {
		spl_autoload_register( array( $this, 'class_autoloader' ) );
		WPCX_Blurbette_Def::do_all_hooks();
	}

	public function class_autoloader( $classname ) {
		if ( strpos( $classname, 'WPCX_' ) !== 0 ) return null;
		require_once plugin_dir_path( __FILE__ ) . 'classes/' . strtolower( $classname ) . '.php';
	}

	public function register( $classname, $opts = null ) {
		$this->objects[$classname][] = new $classname( $opts );
	}
	
} // end of class WPCX_Blurbette_Registry

$WPCX_Blurbette = new WPCX_Blurbette_Registry();