Writing Your First WordPress Plugin

With WordPress powering over 25% of websites, it is undoubtedly a platform of interest to many developers and entrepreneurs as it brings in many opportunities to participate, contribute and even make money. Having published first set of plugins (C9 Variables Pro and C9 Variables), I wanted to share my learning on how to write your first WordPress plugin along with a few tips.

Why write a WordPress plugin?

My talk at the Boston WordPress Meetup @Harvard University

A WordPress plugin lets you extend the WordPress platform to provide value to users. It could be as simple as showing custom text to a full blown E-commerce solution. The possibilities are endless! If you are a developer or have a great plugin idea, here are some good reasons to write a WordPress plugin.

  • WordPress is a popular platform. So, you have a huge potential market.
  • The WordPress platform makes it quite easy to extend and write your own plugin.
  • The overall WordPress ecosystem has made it easy to publish and manage plugins. Be it for free or for commercial purpose.
  • If you are looking at selling your plugin, there are well-established sites out there which can do this. In addition, you can also sell on your own.
  • Having developed in many languages and frameworks in the past, I can say from experience that it is fun writing code on top of WordPress platform.

Keep in mind though that there are several thousands of plugins already out there. Do not let the number overwhelm you. Just make sure whichever plugin you write has originality and delivers real value to the users.

Plugin Development Cycle

A picture is worth a thousand words. The following flowchart captures the essence of a typical Plugin Development Cycle.

WordPress Plugin Development Cycle

Here are the key points.

  • Come up with a plugin idea. The key being something that delivers value to the users. Here are some inputs.
    • What kind of challenges you have seen when you built/manage your site? Are there any good plugins out there that solve these? If not, there’s your idea.
      • A case in point is when I was authoring my blogs and organizing site content, I felt a need to modularize my content so that I can reuse the same content across multiple pages and posts. Also, make it easy to manage changes to such reusable content in one place. An example of this was product features. That’s how  C9 Variables was born.
    • You may already be a domain expert and have an idea of typical challenges people face. So, you could write a plugin that can help people overcome these. Bingo!
    • In software world, many times we ask “what problem are you trying to solve?” I like to ask “what value are you trying to bring?” So, you see, not everything has to be thought in terms of a problem and a solution. You may have ideas around how to do things better or may be altogether a new concept.
    • Lastly, no matter which idea(s) you choose, try to start simple and not invest a lot of time and energy unless you are absolutely sure it’s worth the time. As Entrepreneurs and developers we often juggle through various tasks and priorities. So, it’s worth getting a reasonable plugin out, get some feedback and then build more as opposed to taking a long time and delivering something that does not get used as much.
  • Develop the plugin. I would specifically like to highlight to focus on quality and ensuring you have good unit testing in place. This can save you far more hassles and support requests. Besides, writing good unit tests is fun!
  • How do you plan to release the plugin?
    • Is it free? In that case WordPress.org is perhaps the best plugin directory to submit to.
    • If paid
      • Are you planning to use an existing marketplace like Code Canyon? They take a decent cut out of the sales. But, they do provide a rich platform that makes it easy to sell plugin.
      • Lastly, you can always sell the plugin on your own site. More work for sure. But, there are several WordPress plugins (such as, WooCommerce) that can help.
  • In general, submission to any site/directory goes through an initial review process. This typically involves things like adhering to the WordPress coding standards and any other site specific guidelines.
  • Finally, make the release along with a good documentation, tutorial, demo and perhaps a video as well to make it easy for users to consume it.
  • And, the journey has begun! Make sure to stay on top of support requests, customer queries. Be helpful.
  • Many a times the support requests and user inputs may give you enough for the next release. On top of that you could come up with even more ideas to make the plugin more useful.

Developing Your Plugin

Let’s build a plugin together

Yes! What else would be a better way to show? Let’s make a simple plugin – WordPress Talks, which provides the following capabilities.

  • WordPress Admin
    • Capture the talk Title and Description
    • Capture the Presenter Name, Website and Recording URL
    • Settings
      • Enable/disable debug mode
  • WordPress Public
    • Embed list of talks in a page or post
    • Embed a specific talk in a page or post

We’ll use a plugin slugc9-wp-talks, for the sake of keeping it unique. A slug is a unique string that is part of the URL (in this case for our plugin). It is important to have a unique slug esp. when you submit it to a plugin directory like WordPress.org to avoid any conflicts.

Note: See the Resources section for access to the complete code.

Before you begin

Rest of the post assumes you have a working WordPress installation for development, preferably. If you are looking for a jumpstart on setting up WordPress, please read Getting Started with WordPress for Developers and Webmasters.

Edit your WordPress wp-config.php to enable debug by configuring the following settings.

define( 'WP_DEBUG', true);
define( 'WP_DEBUG_LOG', true);

This will enable logging to the wp-content/debug.log file.

Get a skeleton version of plugin running in 2mins!

Yes, no kidding! If you are fast you could do it in under a minute. No pressure! 🙂

WordPress recommends certain coding standards. You can obviously start from scratch or get a jumpstart by generating a skeleton code for your plugin. I recommend using a generator like the one below.
https://wppb.me/

There are a few advantages of using this tool.

  • It generates almost fully-functional code that adheres to the WordPress standards.
  • It organizes code in a more logical structure, such as, admin vs public.
  • The generated code has well-defined methods where you can add your code.
  • The generated code is also fairly well documented.

In the beginning, you may feel that it generates a lot of code. However, you can always get rid of any unnecessary code. At a minimum, it’s worth a try.

Here’s a screenshot of generating skeleton code for c9-wp-talks.

WordPress Plugin Skeleton Code Generator

Follow these steps to use the skeleton code.

  • Download and extract the c9-wp-talks.zip under your wp-content/plugins folder and change directory to wp-content/plugins/c9-wp-talks. We’ll refer to this as the PLUGIN_DIR in rest of the post.
  • Edit c9-wp-talks.php
    • Update Description in comments: A simple plugin to capture WordPress talks.
    • Replace the PLUGIN_NAME_VERSION constant by C9_WP_TALKS_VERSION.
  • Edit includes/class-c9-wp-talks.php and replace the PLUGIN_NAME_VERSION constant by C9_WP_TALKS_VERSION.
  • Go to WordPress Admin Plugins page and you should now see an entry for the WordPress Talks plugin waiting to be activated. I told you it was easy.
    C9 WordPress Talks - Activate Plugin
  • Click on Activate and your plugin is now part of the WordPress ecosystem.

Figuring out the Data Model

As you can imagine, we have talks data that needs to be persisted to the database. We have a couple options here.

  1. Use WordPress built-in data model.
  2. Create our own database schema.

In general, I highly recommend using the WordPress built-in data model for the following reasons.

  • The built-in model is reasonably generic. You can model various types of real-world or other entities including any custom attributes these may have.
    • The post object is one such generic object in WordPress out-of-the-box data model that can store custom objects. Each such object is of a custom post type, which makes it easy to identify and manage objects of that type. You will often see WordPress developers interchangeably using post object for such custom objects. In fact, several of WordPress APIs as well as parameter names would refer to such objects as posts. And, that’s primarily because these objects are stored as posts.
    • Obviously, post object has well-defined columns in it’s database table. So, how do we store custom attributes that are not part of this table? Simple! The custom attributes are captured as meta data. Think of meta data as (key, value) pairs that are associated with a given post object.
  • Apart from using the model, WordPress has a rich framework that can save you from writing additional code. Things like admin screens to perform CRUD (create/retrieve/update/delete), showing search results, etc.
  • WordPress has a fairly matured callback/hooks capability to call custom code at various points in the flow. When you use the built-in model, you get to leverage the pre-defined hooks.
  • WordPress offers an interesting capability called as multisite. In this, the same WordPress installation can support multiple sites, such as, a site per user of your site. When you use the built-in model, you automatically get the benefit of multisite compatibility by following WordPress recommended coding practices.

Net-net, using the existing model is highly recommended and will give you more time to focus on the core functionality of your product. For c9-wp-talks, we will use the following.

Custom Post Type Table Meta Data
c9_wp_talks_talk posts
  • c9_wp_talks_presenter_name
  • c9_wp_talks_presenter_website
  • c9_wp_talks_recording_url
The Custom Post Type should be unique. A simple way of doing this is to use the plugin slug followed by the noun (entity) being represented as shown above.

Understanding WordPress Hooks

In order to write a WordPress plugin, you must understand WordPress hooks. In this section we will go over these briefly. When you think of writing code for a WordPress plugin, you will often have a need to execute code at various points in the flow. That’s precisely what hooks are for. This could be things like loading your stylesheets or javascripts to something more involved, such as, performing an action when data is saved. In fact, you should always refer to WordPress Hooks documentation to see if there is one available to integrate your code more easily. And, a hook is essentially a callback function that is invoked when it is triggered. In essence, a WordPress plugin is developed on top of such hooks as opposed to a standalone application. It is crucial to understand this not only to be a good WordPress ecosystem citizen, but also to leverage it’s potential.

There are 2 types of hooks supported by WordPress.

  1. Action: An action is meant to respond to an event, such as, call a function when data is saved to the database.
  2. Filter: A filter lets you massage data, such as, for data transformation or sanitization purpose.

You can have multiple callbacks per hook. Lastly, hooks are often passed in additional parameters or can access global variables to access the data in context.

Let’s write some code

Let’s start writing our plugin code along with a few tips.

  • Write your code in custom classes with well-defined names. For example, for our plugin we would use the C9_WP_Talks_ prefix. This avoids potential issues like conflicting function names with other plugins. Also, avoid over-burdening a class with too many responsibility. Create more classes, as appropriate. For our simple plugin, we will write the following classes:
    1. C9_Wp_Talks_Constants: This provides common constants used throughout the code.
    2. C9_Wp_Talks_Admin_Delegate: This provides the core logic for admin functionality.
    3. C9_Wp_Talks_Admin_UI: This provides the user interface (UI) logic.
    4. C9_Wp_Talks_Public_Delegate: This provides the core logic for public functionality.
    5. C9_Log_Writer: A class to simplify logging.
  • We will leverage the structure of the skeleton code to organize our code.
    Plugin Directory Structure
    Note: Notice how we are organizing our custom classes under includes/code directory structure.
  • Use the generated skeleton code to call relevant methods in the above classes. That way the skeleton code is de-coupled from the core logic.

Understanding the Code Flow

We talked about a few points so far from WordPress hooks to writing custom classes for our plugin. Let’s put these in perspective by reviewing some code flows. Following sequence diagram illustrates these.

WordPress Plugin Sample Admin Interactions

Interaction#1: Plugin Initialization

  • We would like to register our custom post type and settings with WordPress. This is done via the WordPress init action callbacks implemented by C9_Wp_Talks_Admin_Delegate.
    • register_cpt(): It registers a custom post type to store talks.
    • register_settings(): It registers settings for our plugin.

Interaction#2: Menu Setup

  • The user (in this case Admin) would access our plugin via a WordPress Admin menu. To facilitate this, we would leverage the admin_menu action callback implemented by C9_Wp_Talks_Admin_UI.

Interaction#3: Add/Edit Talk

  • When Admin clicks on Add New or Edit Talk, WordPress will take care of showing the edit screen because… Yes, you guessed it right – because of the custom post type. However, we do want to show a UI for the additional fields like Presenter Name. To facilitate this, we would leverage the add_meta_boxes action callback implemented by C9_Wp_Talks_Admin_UI.

Interaction#4: Publish/Update Talk

  • Finally, we are at a point where we can create or save our Talk. Again, WordPress platform will take care of saving the custom post type. We just need to take care of the additional fields. And, for these we will leverage the save_post_c9_wp_talks_talk action callback implemented by C9_Wp_Talks_Admin_Delegate. Note the name of the hook (save_post_c9_wp_talks_talk). This shows the level of details WordPress platform developers have thought through to make it easy for plugin developers. Sweet!

How about seeing some actual code now?

C9_Wp_Talks_Admin_Delegate Code

<?php
/**
 * Class to provide the core admin logic.
 *
 * @since      1.0.0
 * @package    C9_Wp_Talks
 * @subpackage C9_Wp_Talks/includes/code/admin/core
 */
class C9_Wp_Talks_Admin_Delegate {

  /** The logger. */
  private $logger;


  public function __construct($logger) {
    $this->logger = $logger;
  }

  /** Initialize. */
  public function init() {
    add_action('init', [$this, 'register_cpt']);
    add_action('init', [$this, 'register_settings']);
    add_action(sprintf('save_post_%s', C9_Wp_Talks_Constants::$TALK_POST_TYPE), [$this, 'save_custom_fields']);
    $this->logger->debug("C9_Wp_Talks_Admin_Delegate::init(): Initialization completed.");
  }

  /**
   * Registers the custom post type.
   */
  public function register_cpt() {
    $labels = array(
      'name'               => __('Talks', 'c9-wp-talks'),
      'menu_name'          => __('Talks', 'c9-wp-talks'),
      'singular_name'      => __('Talk', 'c9-wp-talks'),
      'all_items'          => __('All Talks', 'c9-wp-talks'),
      'add_new_item'       => __('Add New Talk', 'c9-wp-talks'),
      'new_item'           => __('New Talk', 'c9-wp-talks'),
      'search_items'       => __('Search Talks', 'c9-wp-talks'),
      'edit_item'          => __('Edit Talk', 'c9-wp-talks'),
      'not_found'          => __('No talks found.', 'c9-wp-talks'),
      'not_found_in_trash' => __('No talks found in Trash.', 'c9-wp-talks')
    );
    $attribs = [
      'labels'              => $labels,
      'description'         => __('A custom post type for WordPress talks.', 'c9-wp-talks'),
      'public'              => false,
      'show_ui'             => true,
      'has_archive'         => true,
      'exclude_from_search' => true,
      'supports'            => ['title', 'editor', 'author', 'revisions'],
      'show_in_menu'        => C9_Wp_Talks_Constants::$MENU_SLUG,
      'parent_item'         => null,
      'menu_icon'           => null,
      'rewrite'             => ['slug'  => C9_Wp_Talks_Constants::$SLUG]
    ];
    $this->logger->debug("C9_Wp_Talks_Admin_Delegate::register_cpt(): Registering custom post type...");
    register_post_type(C9_Wp_Talks_Constants::$TALK_POST_TYPE, $attribs);
  }

  /**
   * Registers the settings.
   */
  public function register_settings() {
    $this->logger->debug("C9_Wp_Talks_Admin_Delegate::register_settings(): Registering settings...");
    register_setting(C9_Wp_Talks_Constants::$SETTINGS, C9_Wp_Talks_Constants::$DEBUG_MODE_SETTING);
  }

  /**
   * Saves the custom fields after save of the post.
   *
   * @param post_id: The post ID.
   * @param post: The post.
   */
  public function save_custom_fields() {
    global $post;

    wp_verify_nonce(C9_Wp_Talks_Constants::$NONCE_PARAM, C9_Wp_Talks_Constants::$UPDATE_TALK_ACTION);
    if ($post) {
      $post_id = $post->ID;
      $this->logger->debug("C9_Wp_Talks_Admin_Delegate::save_custom_fields(): Saving custom fields...");
      $presenter_name = isset($_POST[C9_Wp_Talks_Constants::$PRESENTER_NAME_FIELD]) ? $_POST[C9_Wp_Talks_Constants::$PRESENTER_NAME_FIELD] : '';
      $presenter_website = isset($_POST[C9_Wp_Talks_Constants::$PRESENTER_WEBSITE_FIELD]) ? $_POST[C9_Wp_Talks_Constants::$PRESENTER_WEBSITE_FIELD] : '';
      $recording_url = isset($_POST[C9_Wp_Talks_Constants::$RECORDING_URL_FIELD]) ? $_POST[C9_Wp_Talks_Constants::$RECORDING_URL_FIELD] : '';
      $this->logger->debug("C9_Wp_Talks_Admin_Delegate::save_custom_fields(): Presenter name: $presenter_name, website: $presenter_website, recording_url: $recording_url");
  
      // Save fields now
      update_post_meta($post_id, C9_Wp_Talks_Constants::$PRESENTER_NAME_FIELD, $presenter_name);
      update_post_meta($post_id, C9_Wp_Talks_Constants::$PRESENTER_WEBSITE_FIELD, $presenter_website);
      update_post_meta($post_id, C9_Wp_Talks_Constants::$RECORDING_URL_FIELD, $recording_url);
    }
  }
}

C9_Wp_Talks_Admin_Delegate provides the core admin logic.

  • The init() method registers callbacks for the required hooks so that the WordPress platform can invoke these at appropriate time.
  • The register_cpt() callback registers the c9_wp_talks_talk custom post type. Once registered, this type will be available for subsequent usage.
  • The register_settings() callback registers the debug mode setting. The idea being when the setting is on debug messages will be printed to the WordPress debug.log file. This is helpful for troubleshooting. When done, turn off the debug mode.
  • Lastly, the save_custom_fields() callback is invoked after a talk is saved. In this method, we take care of saving the additional attributes – Presenter Name, Presenter Website and Recording URL.
    • Note the use of nonce (number used once). A nonce is a measure to prevent malicious security attacks by generating a unique string that is only valid for a limited time (typically, in WordPress for 24hours). WordPress offers convenient methods to generate and verify nonce.

C9_Wp_Talks_Admin_UI Code

<?php
/**
 * Class to provide the admin UI logic.
 *
 * @since      1.0.0
 * @package    C9_Wp_Talks
 * @subpackage C9_Wp_Talks/includes/code/admin/ui
 */
class C9_Wp_Talks_Admin_UI {

  /** The logger. */
  private $logger;

  /** The delegate. */
  private $delegate;


  public function __construct($logger, $delegate) {
    $this->logger = $logger;
    $this->delegate = $delegate;
  }

  /** Initialize. */
  public function init() {
    add_action('admin_menu', [$this, 'register_menu']);
    add_action('add_meta_boxes', [$this, 'add_presenter_info_meta_box']);
    $this->logger->debug("C9_Wp_Talks_Admin_UI::init(): Initialization completed.");
  }

  /**
   * Registers the menu.
   */
  public function register_menu() {
    $menu_slug = C9_Wp_Talks_Constants::$MENU_SLUG;
    add_menu_page(__('Talks', 'c9-wp-talks'), __('Talks', 'c9-wp-talks'), 'manage_options', $menu_slug, '', 'dashicons-welcome-view-site');
    add_submenu_page($menu_slug, __('Settings', 'c9-wp-talks'), __('Settings', 'c9-wp-talks'), 'manage_options', C9_Wp_Talks_Constants::$SETTINGS_PAGE, [$this, 'show_settings_page']);
  }

  /**
   * Shows the settings page.
   */
  public function show_settings_page() {
    if ( !current_user_can( 'manage_options' ) )  {
      wp_die(__('You do not have sufficient permissions to access this page.'));
    }
?>
<div class="wrap">
  <h1><?php _e('C9 WordPress Talks Settings', 'c9-wp-talks'); ?></h1>
  <?php settings_errors(); ?>
  <h2 class="nav-tab-wrapper">
    <a href="#" class="nav-tab nav-tab-active"><?php _e('General', 'c9-wp-talks'); ?></a>
  </h2>
  <form method="post" action="options.php">
    <?php settings_fields(C9_Wp_Talks_Constants::$SETTINGS); ?>
    <?php do_settings_sections(C9_Wp_Talks_Constants::$SETTINGS); ?>
    <table>
      <tr>
        <td colspan="2">&nbsp;</td>
      </tr>
      <?php $this->show_settings_fields(); ?>
      <tr>
        <td colspan="2" align="center"><?php submit_button(); ?></td>
      </tr>
    </table>
  </form>
</div>
<?php
  }
    
  /**
   * Shows settings fields.
   */
  public function show_settings_fields() {
?>
      <tr>
        <th><?php _e('Enable Debug Mode', 'c9-wp-talks'); ?></th>
        <td><input type="checkbox" name="<?php echo C9_Wp_Talks_Constants::$DEBUG_MODE_SETTING; ?>" value="true" <?php echo esc_attr(get_option(C9_Wp_Talks_Constants::$DEBUG_MODE_SETTING, C9_Wp_Talks_Constants::$DEBUG_MODE_SETTING_DEFAULT)) == 'true' ? 'checked="checked"' : ''; ?>/></td>
      </tr>
<?php
  }

  /** Adds the presenter information meta box. */
  public function add_presenter_info_meta_box() {
    add_meta_box(C9_Wp_Talks_Constants::$ADDITIONAL_DATA_META_BOX_ID, __('Presenter Information', 'c9-wp-talks'), [$this, 'add_presenter_info_meta_box_content'], C9_Wp_Talks_Constants::$TALK_POST_TYPE, 'side');
  }
    
  /** Adds the presenter information meta box content. */
  public function add_presenter_info_meta_box_content() {
    global $post;
        
    // Add the security field
    wp_nonce_field(C9_Wp_Talks_Constants::$UPDATE_TALK_ACTION, C9_Wp_Talks_Constants::$NONCE_PARAM);
    // meta data fields
    $presenter_name = get_post_meta($post->ID, C9_Wp_Talks_Constants::$PRESENTER_NAME_FIELD, true);
    $presenter_website = get_post_meta($post->ID, C9_Wp_Talks_Constants::$PRESENTER_WEBSITE_FIELD, true);
    $recording_url = get_post_meta($post->ID, C9_Wp_Talks_Constants::$RECORDING_URL_FIELD, true);
    $this->logger->debug("C9_Wp_Talks_Admin_UI::add_presenter_info_meta_box_content(): Presenter name: $presenter_name, website: $presenter_websit, recording_url: $recording_urle");
?>
<table>
  <tr>
    <th><?php _e('Name', 'c9-wp-talks'); ?></th>
    <td>
      <input type="text" id="<?php echo C9_Wp_Talks_Constants::$PRESENTER_NAME_FIELD; ?>" name="<?php echo C9_Wp_Talks_Constants::$PRESENTER_NAME_FIELD; ?>" value="<?php echo esc_attr($presenter_name); ?>" size="15"/>
    </td>
  </tr>
  <tr>
    <th><?php _e('Website', 'c9-wp-talks'); ?></th>
    <td>
      <input type="text" id="<?php echo C9_Wp_Talks_Constants::$PRESENTER_WEBSITE_FIELD; ?>" name="<?php echo C9_Wp_Talks_Constants::$PRESENTER_WEBSITE_FIELD; ?>" value="<?php echo esc_attr($presenter_website); ?>" size="15"/>
    </td>
  </tr>
  <tr>
    <th><?php _e('Recording URL', 'c9-wp-talks'); ?></th>
    <td>
      <input type="text" id="<?php echo C9_Wp_Talks_Constants::$RECORDING_URL_FIELD; ?>" name="<?php echo C9_Wp_Talks_Constants::$RECORDING_URL_FIELD; ?>" value="<?php echo esc_attr($recording_url); ?>" size="15"/>
    </td>
  </tr>
</table>
<?php
  }
}

C9_Wp_Talks_Admin_UI provides the core logic for the admin UI.

  • Again, the init() method registers callbacks for the required hooks.
  • The register_menu() callback shows a Talks menu (shown in the admin sidebar). In addition, it also adds a sub-menu item for the Settings page.
    • The show_settings_page() method shows the settings page with the debug mode setting using the show_settings_fields() method. Note how it uses the out-of-the-box methods like settings_errors() to display messages as opposed to writing code specifically for this.
  • The add_presenter_info_metabox() callback shows the Presenter Information fields in the sidebar when a Talk is being edited. Note the use of wp_nonce_field() method to generate a nonce field that will be verified at the backend.

C9_Wp_Talks_Public_Delegate Code

<?php
/**
 * Class to provide the core public logic.
 *
 * @since      1.0.0
 * @package    C9_Wp_Talks
 * @subpackage C9_Wp_Talks/includes/code/public/core
 */
class C9_Wp_Talks_Public_Delegate {

  /** The logger. */
  private $logger;


  public function __construct($logger) {
    $this->logger = $logger;
  }

  /** Initialize. */
  public function init() {
    add_shortcode(C9_Wp_Talks_Constants::$INSERT_TALKS_SHORTCODE, [$this, 'get_talks']);
    add_shortcode(C9_Wp_Talks_Constants::$INSERT_TALK_SHORTCODE, [$this, 'get_talk']);
    $this->logger->debug("C9_Wp_Talks_Public_Delegate::init(): Initialization completed.");
  }

  /**
   * Returns the talks.
   *
   * @param attribs: The shortcode attributes.
   * @param content: The shortcode content.
   */
  public function get_talks($attribs=[], $content=null) {
    $id_header = __('ID', 'c9-wp-talks');
    $title_header = __('Title', 'c9-wp-talks');
    $out = <<<EOL
<table>
  <tr>
    <th>$id_header</th>
    <th>$title_header</th>
  </tr>
EOL;
    $results = new WP_Query([C9_Wp_Talks_Constants::$POST_TYPE_FIELD => C9_Wp_Talks_Constants::$TALK_POST_TYPE]);
    if ($results->have_posts()) {
      $talks = $results->posts;
      foreach ($talks as $talk) {
        $id = $talk->ID;
        $title = $talk->post_title;
        $out .= <<<EOL
  <tr>
    <td>$id</td>
    <td>$title</td>
  </tr>
EOL;
      }
            
      // Clean up
      wp_reset_postdata();
    } // if (talks found)
    else {
      $out .= <<<EOL
  <tr>
    <td colspan='2'><?php _e('No talks found.', 'c9-wp-talks'); ?></td>
  </tr>
EOL;
    }
    $out .= <<<EOL
</table>
EOL;
    return $out;
  }

  /**
   * Returns the talk with the specified ID.
   *
   * @param attribs: The shortcode attributes.
   * @param content: The shortcode content.
   */
  public function get_talk($attribs=[], $content=null) {
    $out = "";
    if (isset($attribs[C9_Wp_Talks_Constants::$ID_ATTRIB])) {
      $id = $attribs[C9_Wp_Talks_Constants::$ID_ATTRIB];
      $talk = get_post($id);
      if ($talk) {
        $title = $talk->post_title;
        $description = $talk->post_content;
        $presenter_name = get_post_meta($id, C9_Wp_Talks_Constants::$PRESENTER_NAME_FIELD, true);
        $presenter_website = get_post_meta($id, C9_Wp_Talks_Constants::$PRESENTER_WEBSITE_FIELD, true);
        $recording_url = get_post_meta($id, C9_Wp_Talks_Constants::$RECORDING_URL_FIELD, true);
        $out = <<<EOL
<table>
  <tr>
    <th>Title:</th>
    <td><strong>$title</strong></td>
  </tr>
  <tr>
    <th>Presenter Name:</th>
    <td>$presenter_name</td>
  </tr>
  <tr>
    <th>Presenter Website:</th>
    <td>$presenter_website</td>
  </tr>
  <tr>
    <th>Recording URL:</th>
    <td><a href='$recording_url' target='_blank'>$recording_url</a></td>
  </tr>
</table>
EOL;
      }
    }

    return $out;
  }
}

C9_Wp_Talks_Public_Delegate provides the core logic for the public.

  • The init() method registers the following shortcodes.
    • c9-wpt-insert-talks: A shortcode to embed a list of talks.
    • c9-wpt-insert-talk: A shortcode to embed information about a specific task by providing it’s ID.
  • The get_talks() method provides the logic to retrieve all talks.
    • It uses WP_Query to retrieve all objects of custom post type c9_wp_talks_talk. You can update this query to use pagination especially if there are a lot of talks.
    • It iterates through the results and prepares an HTML formatted output.
    • Finally, it cleans up any opened resources to ensure no data leakage happens.
  • Similarly, the get_talk() method retrieves information about the specified talk using the get_post() method. It formats the output as HTML and returns it.

C9_Wp_Talks Changes

Note: Following code shows updated snippets only.

  /**
   * Register all of the hooks related to the admin area functionality
   * of the plugin.
   *
   * @since    1.0.0
   * @access   private
   */
  private function define_admin_hooks() {
    $debug_enabled = get_option(C9_Wp_Talks_Constants::$DEBUG_MODE_SETTING, C9_Wp_Talks_Constants::$DEBUG_MODE_SETTING_DEFAULT);
    $logger = new C9_Log_Writer($debug_enabled);
    $delegate = new C9_Wp_Talks_Admin_Delegate($logger);
    $ui = new C9_Wp_Talks_Admin_UI($logger, $delegate);
    $plugin_admin = new C9_Wp_Talks_Admin( $this->get_plugin_name(), $this->get_version() );

    // Initialize
    $delegate->init();
    $ui->init();

    $this->loader->add_action( 'admin_enqueue_scripts', $plugin_admin, 'enqueue_styles' );
    $this->loader->add_action( 'admin_enqueue_scripts', $plugin_admin, 'enqueue_scripts' );
  }

  /**
   * Register all of the hooks related to the public-facing functionality
   * of the plugin.
   *
   * @since    1.0.0
   * @access   private
   */
  private function define_public_hooks() {
    $debug_enabled = get_option(C9_Wp_Talks_Constants::$DEBUG_MODE_SETTING, C9_Wp_Talks_Constants::$DEBUG_MODE_SETTING_DEFAULT);
    $logger = new C9_Log_Writer($debug_enabled);
    $delegate = new C9_Wp_Talks_Public_Delegate($logger);
    $plugin_public = new C9_Wp_Talks_Public( $this->get_plugin_name(), $this->get_version() );

    // Initialize
    $delegate->init();

    $this->loader->add_action( 'wp_enqueue_scripts', $plugin_public, 'enqueue_styles' );
    $this->loader->add_action( 'wp_enqueue_scripts', $plugin_public, 'enqueue_scripts' );
  }

Now that we have our custom classes ready, it is time to hook these up in the generated skeleton code so that the WordPress platform can start interacting with these. This is done in the C9_Wp_Talks class.

  • Update the load_dependencies() method to load the custom classes.
  • The define_admin_hooks() method is updated to create the admin delegate and UI objects and to call their respective init() methods.
  • The define_public_hooks() method is updated to create the public delegate and to call it’s init() method.

Let’s see our plugin in action

Our plugin is ready to roll! Here is a screenshot of a simple page that uses our plugin shortcodes.

C9 WordPress Talks - Shortcodes Demo Page

And here is the output of the page.

WordPress Talks - Shortcodes Demo

Hurray! We can see our plugin in action.

Unit Testing Your Plugin

For Unit Testing, we will leverage WordPress itself! So, these tests will simulate closely how your code will run in an actual WordPress installation.

Unit Testing Setup

  • Install PHPUnit.
    https://phpunit.de/
  • Install the WordPress CLI.
    https://wp-cli.org/
  • Create an empty WordPress installation for our tests.
    bin/install-wp-tests.sh cnapps_wp_test <dbadmin> <password> localhost latest

    This does 2 main things.

    • Downloads the latest WordPress release and installs in a temporary directory.
    • Creates a database for this Unit Testing WordPress setup. This database creation is needed one time only.
  • Initialize the test setup. This creates the unit test related files and a sample unit test file.
    wp scaffold plugin-tests c9-wp-talks
  • Update the phpunit.xml.dist so that code coverage is generated correctly. Otherwise, when you run unit tests, you will likely note an error about incorrect whitelist config.
    <?xml version="1.0"?>
    <phpunit
      bootstrap="tests/bootstrap.php"
      backupGlobals="false"
      colors="true"
      convertErrorsToExceptions="true"
      convertNoticesToExceptions="true"
      convertWarningsToExceptions="true"
      >
      <filter>
        <whitelist>
          <directory suffix=".php">./</directory>
          <exclude>
            <directory suffix=".php">./tests</directory>
          </exclude>
        </whitelist>
      </filter>
      <logging>
        <log type="coverage-text" target="php://stdout" showUncoveredFiles="false"/>
        <log type="coverage-html" target="coverage/report"/>
      </logging>
      <testsuites>
        <testsuite>
          <directory prefix="test-" suffix=".php">./tests/</directory>
        </testsuite>
      </testsuites>
    </phpunit>
    
  • You should now be able to check your setup is ready by running the following command.
    phpunit

Let’s add real Unit Tests

Now that we have our unit test setup working, lets add a couple real unit tests. Here’s the code.

<?php
/**
 * C9_Wp_Talks_Admin Unit Tests.
 *
 * @package C9_Wp_Talks
 */

/**
 * C9_Wp_Talks_Admin Unit Tests.
 */
class C9_Wp_Talks_Admin_Test extends WP_UnitTestCase {

  /** Initializes the test. */
  public function setUp() {
    parent::setUp();
    $this->admin_user_id = $this->factory->user->create(['role' => 'admin']);
    wp_set_current_user($this->admin_user_id);
  }

  /** Clean up. */
  public function tearDown() {
    parent::tearDown();
  }

  /** Tests successful creation of a talk. */
  public function test_plugin_loaded_success() {
    // Validate
    $this->assertTrue(class_exists('C9_Wp_Talks'), "Main plugin class 'C9_Wp_Talks' not found.");
  }

  /** Tests successful creation of a talk. */
  public function test_create_talk_success() {
    // Initialize
    $presenter_name = 'Nitin Patil';
    $presenter_website = 'https://cloudnineapps.com';
    $recording_url = 'https://cloudnineapps.com/';

    // Execute
    $talk = $this->factory->post->create_and_get([
      'post_type'    => C9_Wp_Talks_Constants::$TALK_POST_TYPE,
      'post_title'   => $this->getName() . ' - Talk',
      'post_content' => $this->getName() . ' - Content'
    ]);
    update_post_meta($talk->ID, C9_Wp_Talks_Constants::$PRESENTER_NAME_FIELD, $presenter_name);
    update_post_meta($talk->ID, C9_Wp_Talks_Constants::$PRESENTER_WEBSITE_FIELD, $presenter_website);
    update_post_meta($talk->ID, C9_Wp_Talks_Constants::$RECORDING_URL_FIELD, $recording_url);

    // Validate
    $this->assertTrue($talk->ID > 0, "Talk did not get created successfully.");
    $fetched_presenter_name = get_post_meta($talk->ID, C9_Wp_Talks_Constants::$PRESENTER_NAME_FIELD, $presenter_name);
    $fetched_presenter_website = get_post_meta($talk->ID, C9_Wp_Talks_Constants::$PRESENTER_WEBSITE_FIELD, $presenter_website);
    $fetched_recording_url = get_post_meta($talk->ID, C9_Wp_Talks_Constants::$RECORDING_URL_FIELD, $recording_url);
    $this->assertEquals($presenter_name, $fetched_presenter_name, "Incorrect presenter name.");
    $this->assertEquals($presenter_website, $fetched_presenter_website, "Incorrect presenter website.");
    $this->assertEquals($recording_url, $fetched_recording_url, "Incorrect recording url.");
  }
}

Let’s review the key points.

  • The test class extends from WP_UnitTestCase, which provides a set of convenience methods, such as, factory methods to create test post objects, etc.
  • The setUp() and tearDown() methods perform the common unit testing behavior – per test set up and clean up, respectively. You should use these to leverage any common initialization and clean up code.
  • The test_plugin_loaded_success() method shows a quick way to check whether our plugin is loaded successfully by checking existence of the main plugin class.
  • The test_create_talk_success() method tests successful creation of a talk object.
    • It first creates a task object using the factory method – create_and_get(). Note the use of the custom post type.
    • It then sets the additional attributes just like a real user would when using our plugin.
    • Finally, it validates the talk got persisted successfully and validates the meta data indeed got associated with the talk.

This is to give an idea. You can certainly write more functional unit tests for various scenarios including some negative testing scenarios.

I recommend using the actual scenarios for test names and appending a string like ‘_success’ to indicate this is a happy path/normal scenario.

Resources

All the source code for the plugin including unit tests is available at the following GIT repository.

https://github.com/nitinc9/c9-wp-talks

Happy developing!
– Nitin

Build smart reusable content that is easy to maintain.

Leave a Reply

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