A Full WordPress Plugin Example — Part 1 Custom Post Type

In this part, we’ll create a simple WordPress plugin named “GCMovie” which enables movie posts through registering a custom post type.

Compared to a normal post, a movie post will contain some information related to a movie, such as director, writer, stars, etc. For simplicity, our movie posts will support trailer, director, location, language and description.

Now let’s see the features that would be provided by the plugin “GCMovie”.

  • Create and edit a moive post. In a movie post we can input the movie title, director, location, language, description and trailer.
  • Show the movies in the front pages.
  • List movies. It will be able to list all the movies we’ve created.

Plugin folder structure

Before we start, let’s take a look at the plugin’s folder structure that we will create in this part.

gcmovie/        // The plugin folder
    gcmoive.php // The main plugin file
    assets/     // Resoure folder        
        css/
            gcmovie-admin.css  // Styles for admin pages
            gcmovie-puglic.css // Styles for public pages

There are one main PHP file with the same name as the plugin name, two CSS files one for public pages and the other for admin pages.

Plugin Main File

In the beginning of the main plugin file, we add header comment to declare the plugin’s information.

gcmovie.php

<?php
/*
 * Plugin Name: GCMovie
 * Plugin URI: https://www.gloomycorner.com/gcmovie/
 * Description: A plugin example with custom post type, shortcode, widget, etc.
 * Author: Gloomic
 * Version: 0.1
 * Author URI: https://gloomycorner.com
 * License: GPL2+
 */

if ( ! defined( 'WPINC' ) ) {
    die( 'No direct access.' );
}

define( 'GCMOVIE_MINIMUM_WP_VERSION', '5.3' );

define( 'GCMOVIE_VERSION',            '0.1' );
define( 'GCMOVIE_PLUGIN_DIR',         plugin_dir_path( __FILE__ ) );
define( 'GCMOVIE_PLUGIN_URL',         plugin_dir_url( __FILE__ ) );
define( 'GCMOVIE_PLUGIN_FILE',        __FILE__ );

class GCMovie {
}

Here we declare the plugin name, URI, description, and other information.

Note: You need to include at least “Plugin name” field in the header comment.

Then we add the following code to block direct access. If the file is accessed through browser directly, it will show “No direct access”.

if ( ! defined( 'WPINC' ) ) {
    die( 'No direct access.' );
}

Next we define some constants that will be used inside the plugin.

We will use classes to structure the code, therefore we declare a class named “GCMovie” here. In the following sections we will define some functions inside the class.

Add Plugin Hooks

Add plugin hooks in the constructor function.

    private function __construct() {
        // Custom post type: gcmovie
        add_action( 'init', array( $this, 'register_post_type' ) );

        // Custom content for gcmovie
        add_filter( 'the_content', array( $this, 'get_movie_html' ) );
        add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_public_scripts' ) ); // public scripts and styles

        // Edit for gcmovie
        add_action( 'add_meta_boxes', array( $this, 'add_meta_boxes' ) );
        add_action( 'save_post_gcmovie', array( $this, 'save_movie' ) );
        add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_scripts' ) ); // admin scripts and style

        register_activation_hook( __FILE__, array( $this, 'activate' ) );
        register_deactivation_hook( __FILE__, array( $this, 'deactivate' ) );
        add_action( 'after_switch_theme', array( $this, 'activate' ) );
    }

Let’s look at the hooks one by one.

add_action( 'init', array( $this, 'register_post_type' ) );

It registers a custom post type.

add_filter( 'the_content', array( $this, 'get_movie_html' ) );

It filters the content before it is shown. When a movie is shown in the front page, we filter the default content so we can display our custom content like director, location, language, etc.

add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_public_scripts' ) );

It enqueues JavaScript and styles in the front page. We use this hook to format the content of movies.

add_action( 'add_meta_boxes', array( $this, 'add_meta_boxes' ) );

It adds some sections in the edit page of a movie. We can use this hook to add our custom fields in these sections like director, location, language, etc.

add_action( 'save_post_gcmovie', array( $this, 'save_movie' ) );

It allows us to save the custom content for a movie when it is saved.

add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_scripts' ) );

It enqueues JavaScript and styles to the admin pages.

register_activation_hook( __FILE__, array( $this, 'activate' ) );
register_deactivation_hook( __FILE__, array( $this, 'deactivate' ) );
add_action( 'after_switch_theme', array( $this, 'activate' ) );

We use these three hooks to flush rewrite rules to get permalinks work for the custom post type on activation, deactivation and theme switched.

Note

In order to see the effect of our code in each step, you may put the last step “Initiate a GCMoive Instance” right after this step and just add those hooks and filters that have been defined for the moment.

Register a Custom Post Type

We will register a custom post type gcmovie.

    function register_post_type() {
        $labels = array(
            // General name for the post type.
            'name'               => __( 'GCMovies', 'gcmovie' ),
            'menu_name'          => __( 'GCMovie', 'gcmovie' ),
            'singular_name'      => __( 'Movie', 'gcmovie' ),
            'all_items'          => __( 'Movies', 'gcmovie' ),
            'search_items'       => __( 'Search Movies', 'gcmovie' ),
            'add_new'            => __( 'Add New', 'gcmovie' ),
            'add_new_item'       => __( 'Add New Movie', 'gcmovie' ),
            'new_item'           => __( 'New Movie', 'gcmovie' ),
            'view_item'          => __( 'View Movie', 'gcmovie' ),
            'edit_item'          => __( 'Edit Movie', 'gcmovie' ),
            'not_found'          => __( 'No Movies Found.', 'gcmovie' ),
            'not_found_in_trash' => __( 'Movie not found in Trash.', 'gcmovie' ),
            'parent_item_colon'  => __( 'Parent Movie', 'gcmovie' ),
        );

        $args = array(
            'labels'             => $labels,
            'description'        => 'Movie',
            'menu_position'      => 5,
            'menu_icon'          => 'dashicons-media-video',
            'public'             => true,
            'publicly_queryable' => true,
            'show_ui'            => true,
            'show_in_menu'       => true,
            'show_in_admin_bar'  => true,
            'query_var'          => true,
            'capability_type'    => 'post',
            'has_archive'        => true,
            'hierarchical'       => false,
            'supports'           => array( 'title', 'thumbnail', 'editor' ),
        );

        register_post_type( 'gcmovie', $args );
    }

Let’s take look at some of the options.

  • ‘labels’

    Labels for this post type that are shown in front pages and admin pages.

  • ‘public’

    Whether the post type is visible in the front page.

  • ‘show_ui’

    Whether to generate a UI for managing this post type in the admin.

  • ‘show_in_menu’

    Where to show the post type in the admin menu.

    In order to let WordPress creates a menu in the admin page, set both 'show_ui' and 'show_in_menu' to true.

  • ‘supports’

    When creating or editing a movie, which standard fields will be supported.

    Here we support three fields, title, thumbnail (used for trailer), and editor (used to input description).

See register_post_type for the whole parameters.

Note: You may have noticed that in the code, we use functions like __() when we needs a string. __() is used to retrieve a translated string. Therefore this is part of internationalization introduced in the part 6. We do this from the beginning to avoid modifications of all such string literals in later time.

After this plugin is activated, WordPress will create a menu in the admin pages for us.

GCMovie menu

Flushing Rewrite on Activation

To get permalinks to work when the plugin is activated for our custom post type, add the following callbacks used on plugin activation, deactivation and theme switched.

    // Make permalinks to work when this plugin is activated.
    function activate() {
        $this->register_post_type();

        // Flush permalinks
        flush_rewrite_rules();
    }

    function deactivate() {
        flush_rewrite_rules();
    }

Enqueue Styles to Admin Pages

Before we make customizations in the admin pages for our gcmovie post type, let’s define some styles that will be used later and enqueue them to the admin pages.

Add below styles in assets/css/gcmovie-admin.css file.

gcmovie-admin.css

/*
 * GCMovie admin styles
 */
.gcmovie-edit-table {
     width: 100%;
}
.gcmovie-edit-table td {
    padding: 10px;
}
.gcmovie-edit-table td.left {
    width: 30px;
}
.gcmovie-edit-table input {
    width: 100%;
}

Enqueue this file to the admin pages.

    function enqueue_admin_scripts() {
        wp_enqueue_style( 'gcmovie-admin.css', GCMOVIE_PLUGIN_URL . 'assets/css/gcmovie-admin.css', '', GCMOVIE_VERSION );
    }

Add Meta Boxes to Hold Custom Fields

Meta boxes are modular edit screen elements. We use them to collect information related to the edited movie post.

In add_meta_boxes(), we add one meta box and specify display_meta_box_information() to display HTML inside it.

    function add_meta_boxes() {
        add_meta_box(
            'gcmovie_meta_box_information',       // id
            __( 'Information', 'gcmovie' ),       // name
            array( $this, 'display_meta_box_information' ),  // display function
            'gcmovie'                             // post type
        );
    }

   function display_meta_box_information( $post ) {
        wp_nonce_field( 'gcmovie_my_nonce', 'gcmovie_nonce_field' );

        $director = get_post_meta( $post->ID, '_gcmovie_director', true );
        $location = get_post_meta( $post->ID, '_gcmovie_location', true );
        $language = get_post_meta( $post->ID, '_gcmovie_language', true );

        do_action( 'gcmovie_edit_start' );
        ?>

        <table class="gcmovie-edit-table" role="presentation">
            <tbody>
                <tr>
                    <td class="left"><label for="gcmovie_director"><?php _e( 'Director', 'gcmovie' ); ?></label></td>
                    <td><input type="text" name="gcmovie_director" id="gcmovie_director" value="<?php echo $director; ?>"></td>
                </tr>
                <tr>
                    <td><label for="gcmovie_location"><?php _e( 'Location', 'gcmovie' ); ?></label></td>
                    <td><input type="text" name="gcmovie_location" id="gcmovie_location" value="<?php echo $location; ?>"></td>
                </div>
                <tr>
                    <td><label for="gcmovie_language"><?php _e( 'Language', 'gcmovie' ); ?></label></td>
                    <td><input type="text" name="gcmovie_language" id="gcmovie_language" value="<?php echo $language; ?>"></td>
                </tr>
            </tbody>
        </table>

        <?php
        do_action( 'gcmovie_edit_end' );
    }

When registering the custom post type gcmovie, we already have title, description, and trailer fields. In display_meta_box_information() function, we add three more fields respectively used for director, location and language. Before displaying the fields, remember to read their old values from database with which we fill in them.

Now we will see the meta box in the “new movie” page.

GCMovie meta box

Save Custom Fields

In save_movie(), we save the information from the custom fields that were added in previous code to database.

    function save_movie( $post_id ) {
        if( ! isset( $_POST['gcmovie_nonce_field'] ) ) {
            return $post_id;
        }

        if( ! wp_verify_nonce( $_POST['gcmovie_nonce_field'], 'gcmovie_my_nonce' ) ) {
            return $post_id;
        }

        // Check for autosave
        if( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
            return $post_id;
        }

        $director = isset( $_POST['gcmovie_director'] ) ? sanitize_text_field( $_POST['gcmovie_director'] ) : '';
        update_post_meta( $post_id, '_gcmovie_director', $director );

        $location = isset( $_POST['gcmovie_location']) ? sanitize_text_field( $_POST['gcmovie_location'] ) : '';
        update_post_meta( $post_id, '_gcmovie_location', $location );

        $language = isset( $_POST['gcmovie_language'] ) ? sanitize_text_field( $_POST['gcmovie_language'] ) : '';
        update_post_meta( $post_id, '_gcmovie_language', $language );

        do_action( 'gcmovie_save', $post_id );
    }

Now we can create a new movie post and save it. Then the movie will appear in the movie table list as following.

GCMvoie list

Enqueue Styles to Front Pages

In this step we enqueue styles to the front pages that will be used later to format the movie content.

    function enqueue_public_scripts() {
        wp_enqueue_style( 'gcmovie-admin.css', GCMOVIE_PLUGIN_URL . 'assets/css/gcmovie-public.css', '', GCMOVIE_VERSION );
    }

Add below styles in assets/css/gcmovie-public.css file.

gcmovie-public.css

/*
 * GCMovie front styles
 */

.gcmovie-information {
    display: block;
    padding: 20px 0;
    margin-bottom: 1.5em;
    border-bottom: 1px solid #eee;
}
.gcmovie-box {
    display: block;
    padding: 10px;
    margin-bottom: 1.5em;
    border: 1px solid #eee;
}
.gcmovie-box .top {
    display: block;
}
.gcmovie-box .bottom {
    display: block;
}

Display Movie Post in Front

As mentioned previously, we add custom content related to a movie before it is shown through the the_content filter. Here we define the filter’s callback function get_movie_html().

    function get_movie_html( $content ) {
        global $post, $post_type;

        if( $post_type != 'gcmovie' || ! is_singular() ) {
            return $content;
        }

        ob_start();
        $this->display_movie( $post );
        return ob_get_clean();
    }

    private function display_movie( $post ) {
        $post_id = $post->ID;

        do_action( 'gcmovie_content_start', $post );
        ?>
        <div class="gcmovie-information">
            <?php $this->display_movie_information( $post_id ); ?>
        </div>
        <h4><?php _e( 'Description', 'gcmovie' ); ?></h4>
        <?php
        echo $post->post_content;

        do_action( 'gcmovie_content_end', $post );
    }

    function display_movie_information( $post_id ) {
        $director = get_post_meta( $post_id, '_gcmovie_director', true );
        $location = get_post_meta( $post_id, '_gcmovie_location', true );
        $language = get_post_meta( $post_id, '_gcmovie_language', true );
        ?>
        <b><?php _e( 'Director', 'gcmovie' ); ?>: </b><?php echo $director; ?></br>
        <b><?php _e( 'Location', 'gcmovie' ); ?>: </b><?php echo $location; ?></br>
        <b><?php _e( 'Language', 'gcmovie' ); ?>: </b><?php echo $language; ?></br>
        <?php
    }

Inside the callback, first check the post type, if it is gcmovie then custom the content by calling display_movie(). Here we add director, location, language and description to the content.

In get_movie_html(), we use ob_start() and ob_get_clean(). They are a couple of PHP methods that ob_start() turns output buffering on (That means no output is sent from the script other than headers, instead the output is stored in an internal buffer.) and ob_get_clean() returns the contents of the output buffer.

Below is a movie front page.

GCMovie front page

Initiate a GCMovie Instance

We will adapt the singleton pattern to create a static instance.

class GCMovie {
    private static $instance = null;

    static function get_instance() {
        if ( is_null( self::$instance ) ) {
            self::$instance = new GCMovie();
        }

        return self::$instance;
    }

    static function init() {
        self::get_instance();
    }

    //...
}

GCMovie::init();

At last, call init() method outside the class to allow the plugin to run.

One Reply to “A Full WordPress Plugin Example — Part 1 Custom Post Type”

  1. can you please explain the importance of these lines of code :

    do_action( ‘gcmovie_content_start’, $post );
    do_action( ‘gcmovie_content_end’, $post );
    do_action( ‘gcmovie_save’, $post_id );
    do_action( ‘gcmovie_edit_start’ );
    do_action( ‘gcmovie_edit_end’ );

Leave a Reply