DesignINk Digital created specific code for the DesignInk Wordpress Framework
DesignINk Digital created specific code for the DesignInk Wordpress Framework

How to Use the DesignInk WordPress Framework

The DesignInk WordPress framework is intended to facilitate the PHP development of WordPress plugins by standardizing action patterns and providing an object-oriented approach to the structure of a plugin. The goal is to encourage use of the correct coding standards for WordPress and expedite the process of building plugins.

Part of the initial idea was to embrace the singleton pattern since they are so deeply ingrained with WordPress and its community, and to provide a strong base of crucial singleton classes so that I also never had to think about it again.

Another goal was to modularize the structure of a plugin to help categorize large amounts of files by functionality. I have all the respect for the WooCommerce developers, but that is a lot of files in their `includes` folder! By using our own autoloader implementation, better organizational capacity can be achieved by separating the includes folder into an array of sub-folder hierarchies.

This is our internal tool developed here at DesignInk Digital. I have personally found great utility in this framework, and I hope others can as well. This framework is an incomplete implementation of what I imagine could be a much fuller product, but I am the sole developer here and I have limited time to theorize further planning. I will be updating the framework with small features here and there, but always welcome suggestions.

How to Write a Plugin

In deciding how I was going to do the initial documentation for this DesignInk WordPress project I figured, “you know, people are just going to want to know how to write a plugin with it”, so that’s how this initial documentation is structured.

Naming Conventions

When you create a plugin with this DesignInk WordPress framework, the name of the main PHP file should be the same as the parent folder, so that the path would look like `/wp-content/plugins/my-plugin/my-plugin.php`.

Except for the main plugin file, the admin module file, and template files, every file should contain only one class and the name of the file will have the prefix `class-`. This is in accordance with the WordPress PHP naming conventions. If you place multiple classes in a file or do not prefix your file names, your classes may not be found by the autoloader. All file names should be lowercase and dashed. Class names should be the capitalized and underscored version of the file name (without the .php extension). E.g. `class-my-class.php` => `class My_Class { }`.

Directory Structure

There are several folders which can exist in any Module, the Plugin being the primary Module. Many of these will exist in your project’s root folder.

  • admin
    The `admin` folder is for the Plugin Admin Module.
  • assets
    The `assets` folder is for JavaScript and CSS resources that can be loaded by a Plugin Module. These resources reside in the `js` and `css` sub-folders of the `assets` folder.
  • includes
    The `includes` folder is the top level directory for the sub-folders a module uses to autoload files. The folders that can exist inside of the `includes` folder depending on the Module, but default to:
    • abstractions
    • classes
    • modules
    • post-types
    • statics
  • templates
    The `templates` folder is so a Plugin can easily call and echo a template with localized data.
  • vendor
    The `vendor` folder is created by Composer when installing the framework. It is where the framework resides.

Creating a Plugin (main plugin file)

First things first, we need to import the DesignInk WordPress Framework. You can install the package in the root directory of your project using the command `composer require designink/designink-wp-framework`. Composer installs this in the `vendor/designink/designink-wp-framework` folder.

Secondly, create the primary plugin file. This plugin will be called the “DesignInk WP Framework Test Plugin”, so the plugin folder name should be `designink-wp-framework-test-plugin`, and the entry PHP file should be `designink-wp-framework-test-plugin.php` accordingly. The class inside of the entry file will extend the `\Designink\WordPress\Framework\{Version}\Plugin` class. All we need to do inside this file is call the framework and create our Plugin class. The Plugin class resides in the `\Designink\WordPress\Framework\{Version}` namespace, as do all other classes provided by the framework.

<?php
/**
 * Plugin Name: DesignInk WP Framework Test Plugin
 */

defined( 'ABSPATH' ) or exit;

use Designink\WordPress\Framework\v1_0_3\Plugin;

// Include DesignInk's framework
require_once __DIR__ . '/vendor/designink/designink-wp-framework/index.php';

if ( ! class_exists( 'Designink_WP_Framework_Test_Plugin', false ) ) {

	/**
	 * The wrapper class for this plugin.
	 */
	final class Designink_WP_Framework_Test_Plugin extends Plugin {

		/**
		 * The inherited function called on plugin activation.
		 */
		final public static function activate() { }

		/**
		 * The inherited function called on plugin deactivation.
		 */
		final public static function deactivate() { }
		
	}

	// Fire it up
	Designink_WP_Framework_Test_Plugin::instance();

}

A Plugin class extends a Module, which will be gone over in more detail next. Note that to instantiate your plugin, you must call the `instance` method of the Plugin. This is because all Modules are Singletons, and a protected constructor ensures only one instance exists at a time. You will instantiate your Plugin like this to gain access to script enqueuing and template rendering in your Plugin.

If you run this code so far, you have a very simple plugin!

Creating a Module (includes/modules)

What is a module? It’s one of those mean words that means nothing that I decided to use. No, but it is a pretty abstract word in meaning, so we appropriated it for our personal use here when saying, “we have this thing that does something and it belongs to something else”. A Plugin is a special type of Module that belongs to WordPress. Modules can contain other modules, so a Plugin can contain its own Modules, which are loaded after the plugin is. The modules are loaded from `/includes/modules`. Sub Modules are constructed at the end of the parent Module constructors. The Plugin constructor runs when WordPress calls the project’s primary PHP file, which happens right before the `plugins_loaded` WordPress action hook is fired.

Create the `/includes/modules` folder in your project root. To create a Module class called `Designink_Test_Module`, you need a file called `class-designink-test-module.php` with a class that extends the `\Designink\WordPress\Framework\{Version}\Module` class. This file can go in a folder `/includes/modules/designink-test-module/`, where it can have its own directory structure with it’s own sub-Modules, or, since I have no sub-Modules to load with it, I can just create the file `/includes/modules/designink-test-module.php`.

<?php

defined( 'ABSPATH' ) or exit;

use Designink\WordPress\Framework\v1_0_3\Module;

if ( ! class_exists( 'Designink_Test_Module', false ) ) {

	/**
	 * A test module to demonstrate functionality.
	 */
	final class Designink_Test_Module extends Module {

		/**
		 * The module entry point.
		 */
		final public static function construct() {
			add_action( 'init', array( __CLASS__, '_init' ) );
		}

		/**
		 * The WordPress 'init' action hook.
		 */
		final public static function _init() { }

	}

}

The static function `construct` (without the double underscore) is the entry point for all Modules. Even the Plugin class inherits this function which is called when the Plugin is loaded before the `plugins_loaded` WordPress action hook. This is a great place to add action and filter hooks required by each Module. The convention for this framework is that a public, static function is created on the Module class with the same name as the hook, but prefaced by an underscore, and called using the `__CLASS__` magic constant as is typical within WordPress.

Creating a Post Type (includes/post-types)

At some point, you are going to want to register a custom Post Type to use within your Plugin. The Post Type class is another core singleton class utilized by the Plugin class. To create a custom post type called `designink-test-post-type`, you would create a file called `/includes/post-types/class-designink-test-post-type.php` with a class which extends the `\Designink\WordPress\Framework\{Version}\Post_Type` class.

<?php

defined( 'ABSPATH' ) or exit;

use Designink\WordPress\Framework\v1_0_3\Plugin\Post_Type;

if ( ! class_exists( 'Designink_Test_Post_Type', false ) ) {

	/**
	 * A class to represent and help deal with common plugin functionality.
	 */
	final class Designink_Test_Post_Type extends Post_Type {

		/**
		 * The required name from the abstract.
		 * 
		 * @return string The Post Type name.
		 */
		final public static function post_type() { return 'designink_test_posts'; }

		/**
		 * The required options from the abstract.
		 * 
		 * @return array The Post Type options.
		 */
		final protected function post_type_options() {
			return array(
				'labels' => array(
					'menu_name' => __( 'Test Posts' ),
				),
				'singular_name'	=> __( 'Test Post' ),
				'plural_name'		=> __( 'Test Posts' ),
				'public'		=> true,
				'show_in_menu'		=> true,
				'show_ui'		=> true,
				'has_archive'		=> true,
				'supports'		=> array( 'title', 'thumbnail' ),
			);
		}

	}

}

Eventually, we will create some Meta Boxes to go on the Post Screens, but for now, this just registers the Post Type in WordPress for us.

Create a Post (includes/classes)

You may wonder, “why have a Post class when there is already a WP_Post class provided?” Well, the answer to that really boils down to the Post Meta. I wanted to have a way to document what Meta data was added to a Post and have some wrapper functionality around it as well, so the Post class was the intermediary step between Post Type and Post Meta. It allows me to consistently treat the WP_Post object as a property of a framework class instead of incorporating the WP_Post type as native to the framework, which it is certainly not.

You may have noticed the `classes` folder inside of the `includes` above. The Post class is an instantiable class, so they would belong in the `classes` folder. This is as oppose to static classes, which would belong in the `statics` folder. I sometimes create static classes in my plugins to provide access to CRUD operations to the rest of a plugin. But as far as this Post class goes, to create the ‘Designink_Test_Post’ class, create the ‘/includes/classes/class-designink-test-post.php’ file and extend the class with `\Designink\WordPress\Framework\{Version}\Post`.

<?php

defined( 'ABSPATH' ) or exit;

use Designink\WordPress\Framework\v1_0_4\Plugin\Post;

if ( ! class_exists( 'Designink_Test_Post', false ) ) {

	/**
	 * A class wrapper for dealing with Test Post functionality.
	 */
	final class Designink_Test_Post extends Post {

		/**
		 * The required abstract function for the expected post type.
		 * 
		 * @return string The post type.
		 */
		final public static function post_type() { return Designink_Test_Post_Type::post_type(); }

		/**
		 * A more readable function to access the Meta with.
		 * 
		 * @return \Test_Post_Meta The Test Post Meta.
		 */
		final public function get_test_meta() {
			return $this->get_meta( Designink_Test_Post_Meta::get_key() );
		}

		/**
		 * Test Post constructor.
		 *
		 * @param int|string|\WP_Post $id The ID of the parent \WP_Post, or the parent class itself.
		 */
		public function __construct( $id ) {
			parent::__construct( $id );
			$this->add_meta( new Designink_Test_Post_Meta( $this ) );
		}

	}

}

You will notice I created this example with the Test Post Meta already attached to save time and demonstrate how this end of the next step is incorporated.

Create a Post Meta (includes/classes)

Just like the Post class, the Post Meta class is instantiable. To create the aforementioned Designink Test Post Meta class, create the `/includes/classes/class-designink-test-post-meta.php` file and extend the class with `\Designink\WordPress\Framework\{Version}\Post\Post_Meta`. The following example is my logic flow for setting up a Post Meta class. The parent constructor takes the WP_Post object the Meta is connected to.

<?php

defined( 'ABSPATH' ) or exit;

use Designink\WordPress\Framework\v1_0_4\Plugin\Post\Post_Meta;

if ( ! class_exists( 'Designink_Test_Post_Meta', false ) ) {

	/**
	 * A class for managing the Designink Test Post post type Meta data.
	 */
	final class Designink_Test_Post_Meta extends Post_Meta {

		/** @var \Designink_Test_Post Parent Designink Test Post object */
		protected $Test_Post = null;

		/** @var array The first test value of the Post Meta. */
		public $meta_value_1;

		/** @var string The second test value of the Post Meta. */
		public $meta_value_2;

		/** @var boolean The third test value of the Post Meta. */
		public $meta_value_3;

		/** @var array Default Meta values. */
		private static $default_values = array(
			'meta_value_1' => array(),
			'meta_value_2' => '',
			'meta_value_3' => false,
		);

		/**
		 * The required abstraction function meta_key()
		 * 
		 * @return string The meta key.
		 */
		final public static function meta_key() { return '_designink_test_post_data'; }

		/**
		 * Constructs the Post Series Meta.
		 * 
		 * @param \Designink_Test_Post $Test_Post The parent Designink Test Post the Meta values belong to.
		 */
		public function __construct( Designink_Test_Post $Test_Post ) {

			if ( ! $Test_Post ) {
				$message = sprintf( "No %s passed to %s constructor.", Designink_Test_Post::class, self::class );
				throw new \Exception( $message );
			}

			foreach ( self::$default_values as $property => $value ) {
				if ( property_exists( $this, $property ) ) {
					$this->{ $property } = $value;
				}
			}

			$this->Test_Post = $Test_Post;
			parent::__construct( $Test_Post->get_post() );
		}

		/**
		 * The required abstract called when saving the Meta. This function returns what is saved.
		 * 
		 * @return array The array representation of the Meta.
		 */
		final public function export_meta() {
			$export = array();

			foreach ( self::$default_values as $property => $default_value ) {
				if ( isset( $this->{ $property } ) ) {
					$export[ $property ] = $this->{ $property };
				}

				else {
					$export[ $property ] = $default_value;
				}
			}

			return $export;
		}

	}

}

Calling a Template (templates)

Inside the `templates` folder of the Plugin is designed to store HTML templates. Templates are not supposed to contain classes, and therefore the WordPress coding standards calls not to prefix the file name with “class-”, but suffix the file name with “-template”.

Templates are a feature of the Plugin and Plugin Admin classes, and are called by accessing the singleton class instances. For example, to access a template `designink-test-template.php`, you would call the template from the Plugin without the suffix using the instanced Plugin’s `get_template` method.

Designink_WP_Framework_Test_Plugin::instance()->get_template( ‘designink-test’ );

Creating an Admin Module (admin)

The Plugin Admin is another kind of core singleton, and is more specifically a further extension of the Plugin class. All of the code that is executed when the WordPress admin panel is open belongs in the `admin` folder. Because it extends a Plugin, it has access to methods for enqueuing JS/CSS and rendering templates from the `admin` folder. Like template files and the entry PHP Plugin file, the Plugin Admin file does not have a “class-” prefix, instead it will have the same name as the primary Plugin file, except with a “-admin” suffix, and the class name will be the same as the primary Plugin class, except with the “_Admin” suffix. The admin Module for the Designink_WP_Framework_Test_Plugin would exist in `/admin/designink-wp-framework-test-plugin-admin.php` as the class `Designink_WP_Framework_Test_Plugin_Admin` which extends `\Designink\WordPress\Framework\{Version}\Plugin\Admin_Module`.

<?php

defined( 'ABSPATH' ) or exit;

use Designink\WordPress\Framework\v1_0_4\Plugin\Admin_Module;

if ( ! class_exists( 'Designink_WP_Framework_Test_Plugin_Admin', false ) ) {

	/**
	 * The admin wrapper class for this plugin.
	 */
	final class Designink_WP_Framework_Test_Plugin_Admin extends Admin_Module {

		/**
		 * Module entry point
		 */
		final public static function construct() {}

	}

}

Creating a Meta Box (admin/includes/meta-boxes)

A Meta Box is another type of instantiable class. In WordPress, a Meta Box can traditionally be added to an admin post edit page in two ways. The first way, you pass a callback function to the `register_meta_box_cb` when registering the Post Type which calls `add_meta_box` at some point. The second way is really the same as the first, but instead you call `add_meta_box` at some point specified by you instead of the Post Type registration. For example, if you wanted a Meta Box to show only on a post edit page and not a new post page, you would want to call `add_meta_box` only on the edit page, and would not pass the registration to the Post Type.

This section deals with creating a Meta Box using the first of those methods, by registering it with the Post Type. To see an example of a page-specific Meta Box, look ahead to the Screens section. To create a Meta Box, we need to do 3 things:

  • Create a class file for the Meta Box under `/admin/includes/meta-boxes`.
  • Create a template file with the HTML for the Meta Box under `/admin/templates`.
  • Register the Meta Box with the Post Type

To do the first of these things, we create a Meta Box class called `Designink_Test_Post_Meta_Box` under `/admin/includes/meta-boxes/class-designink-test-post-meta-box.php` which extends the class `\DesignInk\WordPress\Framework\{Version}\Plugin\Admin\Meta_Box`.

<?php

defined( 'ABSPATH' ) or exit;

use Designink\WordPress\Framework\v1_0_3\Plugin\Admin\Meta_Box;

if ( ! class_exists( 'Designink_Test_Post_Meta_Box', false ) ) {

	/**
	 * The primary Meta Box for the Post Series Post Type page.
	 */
	final class Designink_Test_Post_Meta_Box extends Meta_Box {

		/**
		 * The required meta_key() abstract function.
		 * 
		 * @return string The meta key to use for nonces and inputs in the Meta Box.
		 */
		final public static function meta_key() { return 'test_meta'; }

		/**
		 * The required get_title() abstract function.
		 * 
		 * @return string The title of the Meta Box.
		 */
		final public static function get_title() { return 'Test Meta Box'; }

		/**
		 * The required get_id() abstract function.
		 * 
		 * @return string The ID for the Meta Box.
		 */
		final public static function get_id() { return Designink_Test_Post_Type::post_type(); }

		/**
		 * The required render() abstract function.
		 */
		final protected static function render() {
			Designink_WP_Framework_Test_Plugin::instance()->get_admin_module()->get_template( 'test-post-meta-box' );
		}

		/**
		 * The inherited abstract function to attach to the 'save_post' hook.
		 * 
		 * @param int $post_id The Post ID.
		 * @param \WP_Post $Post The Post object.
		 * 
		 * @return int The post ID.
		 */
		final protected static function save_post( int $post_id, \WP_Post $Post = null ) {
			$user_can_edit_post = current_user_can( 'edit_post', $post_id );
			$is_custom_post = $Post->post_type === Designink_Test_Post_Type::post_type();

			if ( ! $user_can_edit_post || ! $is_custom_post ) {
				return $post_id;
			}

			// Save the meta
			self::save_test_meta( $post_id );
			return $post_id;
		}

		/**
		 * A non-inherited, private function to separate Meta data management.
		 * 
		 * @param int $post_id The ID of the Test Post.
		 */
		final private static function save_test_meta( int $post_id ) {
			$Test_Post = new Designink_Test_Post( $post_id );
			$Test_Meta = $Test_Post->get_test_meta();

			if ( 'yes' === $_POST[ Designink_Test_Post_Type::post_type() ][ self::meta_key() ][ 'clear_saves' ] ) {
				$Test_Meta->times_saved = array();
			}

			array_push( $Test_Meta->times_saved, time() );

			if ( 'yes' === $_POST[ Designink_Test_Post_Type::post_type() ][ self::meta_key() ][ 'is_toggle_active' ] ) {
				$Test_Meta->is_toggle_active = true;
			} else {
				$Test_Meta->is_toggle_active = false;
			}

			$Test_Meta->current_string = $_POST[ Designink_Test_Post_Type::post_type() ][ self::meta_key() ]['current_string'];

			$Test_Meta->save_meta();
		}

	}

}

You will notice the line of code for the `render` function which calls to render a template from the admin module. To create this template, create the file `/admin/templates/test-post-meta-box-template.php` (remember that templates have a ‘-template’ suffix).

<?php

defined( 'ABSPATH' ) or exit;

global $post;

$Test_Post = new Designink_Test_Post( $post );
$Test_Meta = $Test_Post->get_test_meta();

?>

<div class="test-post-meta-box">
	<div class="times-saved">
		<h3>Times this Test Post was saved:</h3>

		<ul>

			<?php foreach ( $Test_Meta->times_saved as $time ) : $Time = new \DateTime( sprintf( '@%s', $time ) ); ?>

				<li><?php echo $Time->format( 'd/m/Y H:i:s' ); ?></li>

			<?php endforeach; ?>
		
		</ul>

		<div>
			Clear previous saves?
			<input
				name="<?php echo Designink_Test_Post_Meta_Box::create_input_name( 'clear_saves' ); ?>"
				type="checkbox"
				value="yes"
			/>
		</div>
	</div>

	<hr />

	<div class="current-string">
		<h3>Current string value:</h3>

		<div>
			<input
				name="<?php echo Designink_Test_Post_Meta_Box::create_input_name( 'current_string' ); ?>"
				type="text"
				value="<?php echo $Test_Meta->current_string; ?>"
			/>
		</div>
	</div>

	<hr />

	<div class="toggle">
		<h3>A boolean toggle:</h3>

		<div>
			<input
				name="<?php echo Designink_Test_Post_Meta_Box::create_input_name( 'is_toggle_active' ); ?>"
				type="checkbox"
				value="yes"
				<?php echo checked( $Test_Meta->is_toggle_active, true ); ?>
			/>
		</div>
	</div>
</div></code>

For the last thing we need to do, we just need to add the Meta Box to the Post Type. If we go back to the Post Type class we created, we can add in the constructor (don’t forget to construct the parent class).

// In ‘class-designink-test-post-type.php’

/**
 * Add Meta Box and call parent.
 */
final public function __construct() {
	parent::__construct();
	$this->add_meta_box( Designink_Test_Post_Meta_Box::instance() );
}

Creating a Screen or Page (admin/includes/screens)

Sometimes you want to create a specific page in the admin panel, or target a specific screen. But what is the difference between a page and a screen? The first comes from the `add_submenu_page` WordPress function, which is responsible for creating single pages in the ‘tools’, ‘settings’, and other sub-menus.

The second thing is really just the broader definition of the first. The entire admin panel is broken up into different ‘screens’, some of which are pages. Each screen has a screen base, screen ID, and a screen action. For example, the screen where you view all the Posts is the `edit.php` screen, where the base would be ‘edit’, the ID would be `edit-post`, and there would be no action.

DesignInk WordPress Post Screens

One of the core classes is the Post Screens class. The post screens deal with the screens involving a custom post type and there are three of them. The new post page, the edit post page, and the view posts page. For this example, if you wanted a Meta Box to appear only on an edit post screen, and not on a new post screen, you want to use the Post Screens class to register the Meta Box instead of the Post Type.

<?php

defined( 'ABSPATH' ) or exit;

use Designink\WordPress\Framework\v1_0_3\Plugin\Admin\Screens\Post_Screens;

if ( ! class_exists( 'Designink_Test_Post_Screens', false ) ) {

	/**
	 * The Test Post screens in the admin.
	 */
	final class Designink_Test_Post_Screens extends Post_Screens {

		/**
		 * The post type the screens apply to.
		 * 
		 * @return string The post type.
		 */
		final public static function post_type() { return Designink_Test_Post_Type::post_type(); }

		/**
		 * The primary entry function for running code globally in the admin area. This code will run with regular Module constructions.
		 */
		final public static function construct_screen() { }

		/**
		 * The primary entry function for running code locally in the screen. This code will run in the 'current_screen' WordPress hook.
		 * This function will be called when any page from the valid screens is matched.
		 * 
		 * @param \WP_Screen $current_screen The current Screen being viewed.
		 */
		final public static function current_screen( \WP_Screen $current_screen ) { }

		/**
		 * Viewing the single post.
		 * 
		 * @param \WP_Screen The current screen.
		 */
		final public static function view_post( \WP_Screen $current_screen ) {
			Designink_Test_Post_Edit_Screen_Meta_Box::instance()->add_meta_box();
		}

		/**
		 * Viewing all posts.
		 * 
		 * @param \WP_Screen The current screen.
		 */
		final public static function view_posts( \WP_Screen $current_screen ) { }

		/**
		 * Adding a post.
		 * 
		 * @param \WP_Screen The current screen.
		 */
		final public static function add_post( \WP_Screen $current_screen ) { }

	}

}

Pages

Screens are great, but they can be messy. Sometimes you just want to add one page, and you know where you want to add it, dang-it! Currently, the framework is incomplete, and there are only a couple classes defined for creating pages. Maybe some day I will create the other abstracts (or someone will help me implement them!), but until then I will provide examples for what is there.

DesignInk WordPress Options Page

The Options Page is a Page that appears under the ‘settings’ menu in the WordPress admin. To create such a page, use the `\DesignInk\WordPress\Framework\{Version}\Plugin\Admin\Pages\Options_Page` class as such.

// TODO: Fill in example

Management Page

The Management Page is a Page that appears under the ‘tools’ menu in the WordPress admin. The create a management page, use the `\DesignInk\WordPress\Framework\{Version}\Plugin\Admin\Pages\Management_Page` class.

// TODO: Fill in example

Docs to write:

Other things:

  • Debugging info

  • Enqueueing CSS/JS

  • Utility

Kyle Niemiec is a full-stack developer at DesignInk Digital and would love to get your feedback and questions about this article and really, anything and all things code.

Facebook
Twitter
LinkedIn