WordPress Custom Posts and Plugins
A lot has been written about WordPress over the years with a lot of the literature describing how flexible it is as a CMS and how you can, with a little know-how and a little work, turn it into whatever you want it to be. Much of this flexibility comes from the ability to register custom post types, where you can create “post types” of any kind within the WordPress CMS. For example, if I were using WordPress to run a company website I could create a “staff” post type, where every entry that I create would contain custom fields for the name of the staff member, their picture, their e-mail, and links to social media profiles. The same thing goes for just about any other post type you could think of: image galleries, sliders, portfolios, etc.
And if there is some functionality that you need that happens to be lacking in the native installation of WordPress, chances are you’ll be able to find something close to what you need in one of the tens of thousands of plugins that exist out on the web. By combining these 2 features (along with a host of others) WordPress is an immensely flexible content management system (CMS) and this is likely one of the reasons it has gained such popularity since it’s inception.
In what follows, we’re going to combine custom post types and WordPress plugin development and create a plugin that registers a custom “portfolio” post type and wraps all of the functionality for both the front and the back end within it. So we’re going to need to to create fields for the name of the portfolio item, fields for featured images, and fields for links to where the project is showcased. After we get through everything, this should give you a good roadmap to create any post type/plugin that you like. In fact, a lot of plugins do little more than register post types inside of some generic WordPress plugin scaffolding code. Because there is so much you can do with custom post types, this is often all you need.
We’re going to be wrapping our entire custom post type plugin up in a class. For reasons discussed in this article it’s a good idea to wrap functionality up in classes. Because we are using classes, it would be beneficial for you to have a fairly good grasp on some basics of object-oriented programming (OOP).
To get started we’re going to want to register all of the functionality we need initially within our constructor. Below is an empty constructor which we’ll be adding to. Notice that immediately following the class we create a new instance of the class. This is how we’ll actually get all of our code to run.
class NBS_Portfolio { function __construct() { } } $portfolio = new NBS_Portfolio();
You may have noticed that we’ve added “NBS_” to our class name. It’s often a good idea to add prefixes to your class names to prevent potential naming collisions. Although we could probably just name our class “Portfolio,” there is the outside possibility that some plugin or theme you have installed has a class that is also named “Portfolio”. I use NBS (for nine bit studios), but you could use whatever prefix you want. The only caveat to this is that you want to make sure that you don’t use “WP_” as a prefix. WordPress uses this prefix for its own native classes.
To start all we need is an activation and deactivation functions to our class. These are functions that WordPress looks for when users activate and deactivate plugins. We’re not actually going to be doing much with these functions. As a best practice, we’ll include a reference to an upgrade.php file that will be used to allow users to update their plugins if we were to ever host this plugin on the WordPress plugins repository.
class NBS_Portfolio { function __construct() { register_activation_hook(__FILE__, array($this, 'activation')); register_deactivation_hook(__FILE__, array($this, 'deactivation')); } /**** ACTIVATION ****/ function activation() { require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); } /**** DEACTIVATION ****/ function deactivation() { /* Nothing to do here */ } }
__FILE__ is one of the magic constants in PHP. Essentially, it means path of the current file on the server.
Now we’re going to want to some action hooks. Action hooks along with filters are two of the most important functions used in WordPress plugin development. The way that WordPress works is that on every page load — a blog post, a page, or even a section of the dashboard/admin panel — there are all of these events that fire. These are events like “init” or “plugins_loaded” or “after_setup_theme” … basically a sequence of events that occur each time a WordPress page is loaded (which, again, also includes admin pages). Your job, as a WordPress developer, is to add to the functionality that occurs when one or more of these actions fire by adding callback functions to run code at the proper time. This is why it’s referred to as “hooking” to these actions with action hooks. We’re essentially hooking functionality onto the WordPress core by telling WordPress: “Load the WordPress core functionality when this action event occurs but also register this custom post type and add these pages to the admin panel to manage it.”
We can also add filters to WordPress events. Filters modify the output and/or the display of any data that WordPress shows be it posts, lists, images or anything else. Often times filters will be used to override the default display of certain types of result sets retrieved from the WordPress database.
Obviously, this is just a brief introduction and is not an exhaustive discussion of action hooks and filters. For more on these topics and other topics related to them, please consult the WordPress codex. But hopefully this is enough of an introduction to get started and things will likely get a little bit clearer we go along.
So getting back to our class that we were working on, let’s add our action hooks and filters to our constructor code…
class NBS_Portfolio { function __construct() { register_activation_hook(__FILE__, array($this, 'activation')); register_deactivation_hook(__FILE__, array($this, 'deactivation')); add_action('init', array($this, 'create_portfolio')); add_filter("manage_edit-portfolio-post-type_columns", array($this, "portfolio_edit_columns")); add_action("manage_posts_custom_column", array($this,"portfolio_columns_display")); add_action("admin_init", array($this,"add_portfolio_meta")); add_action('save_post', array($this,'update_portfolio_meta')); } function create_portfolio() { } function portfolio_edit_columns($portfolio_columns) { } function portfolio_columns_display($portfolio_columns) { } function add_portfolio_meta() { } function update_portfolio_meta() { } }
Here we have added a number of actions and filters in our constructor. The first parameter is the string name of the event that we are attaching our actions and filters to and the second parameter specifies the name of the callback function that will run when that particular event occurs. Notice too that we have added all of the callback functions that will run on those particular events to the class as well (though they’re empty now, we’ll be adding code to them shortly).
Registering a Custom Post Type
With the constructor code set up declaring the methods that our plugin will use, we can now fill out the body of each of these methods with code. The first thing we’re going to want to do is register our new post type that will be the backbone of the new functionality that our plugin adds to WordPress. We can use the register post type function to register our custom post type.
class NBS_Portfolio { function __construct() { register_activation_hook(__FILE__, array($this, 'activation')); register_deactivation_hook(__FILE__, array($this, 'deactivation')); add_action('init', array($this, 'create_portfolio')); add_filter("manage_edit-portfolio-post-type_columns", array($this, "portfolio_edit_columns")); add_action("manage_posts_custom_column", array($this,"portfolio_columns_display")); add_action("admin_init", array($this,"add_portfolio_meta")); add_action('save_post', array($this,'update_portfolio_meta')); } function create_portfolio() { $portfolio_args = array( 'label' => __('Portfolio', 'ninebit'), 'singular_label' => __('Portfolio', 'ninebit'), 'public' => true, 'show_ui' => true, 'capability_type' => 'post', 'hierarchical' => false, 'rewrite' => array('slug' => __('portfolio', 'ninebit')), 'supports' => array('title', 'editor', 'thumbnail') ); register_post_type('portfolio-post-type', $portfolio_args); } function portfolio_edit_columns($portfolio_columns) { } function portfolio_columns_display($portfolio_columns) { } function add_portfolio_meta() { } function update_portfolio_meta() { } }
NOTE: For my string values, I’m using the __(‘String name’, ‘ninebit’) convention because it is used to support localization where ‘ninebit’ is my unique domain for gettext. You should follow this convention wherever possible, but if perfer not to you could just pass the string value in directly.
Notice a couple of things here. The “rewrite” setting allows us to set what our URL will be when viewing single items of our particular post type. So in this case our URL for viewing a portfolio item’s page is going to be something like: https://yourwebsite.com/portfolio/my-portfolio-item. If you want “portfolio” to be something else, you just have to set it in the “rewrite” setting.
One thing that’s particularly important, notice how our actual post type is called “portfolio-post-type”? We could have easily just called our post type “portfolio.” However, with the way that WordPress handles rewriting rules if both the actual name of the post-type and the “rewrite” setting are exactly the same you can run into problems with 404 errors when trying to access single custom post type items. So just be sure to keep these 2 different. It’s unlikely that someone is going to want to name their rewrite “portfolio-post-type”, so we’re probably okay there.
Notice too that we can also set what our post type supports. The title field and editor field you’re probably familiar with as they are standard in WordPress pages and posts. But there may be some cases where we wouldn’t need the title, or editor field. Say we had an “image” post type for an image gallery and all we needed was one meta field to enter the URL to the image. In that case, we could leave “title” and “editor” out of the “supports” setting.
We’ll also give the user the ability to add a thumbnail for each post of our custom post type (which you may have seen before if you’ve worked with themes). We won’t really be using it because we’re going to set the path to our image in a meta field, but we’ll put it in there in case you want to use it in some manner later. It might actually be easier because with the thumbnail you get all of the handy native WordPress image picking functionality from the media manager when you add a thumbnail. Then to display the image on the front end, you’d just have to use the get_the_post_thumbnail function to get the path to the image.
Registering a Custom Taxonomy
Now we’re going to want to do something else: create a custom taxonomy by using the register_taxonomy function. A taxonomy in WordPress describes a concept like “categories” or “tags.” It’s essentially data that you can use to categorize your posts and custom posts in different ways. We could use the native WordPress categories here, but we’re going to want to have something that we can use to specifically categorize *only* our portfolio items. So we’ll create a “portfolio categories” taxonomy. We’ll call the register_taxonomy function and pass in a bunch of initializing data. We can just register our taxonomy in the same method that we registered our custom post type…
class NBS_Portfolio { function __construct() { register_activation_hook(__FILE__, array($this, 'activation')); register_deactivation_hook(__FILE__, array($this, 'deactivation')); add_action('init', array($this, 'create_portfolio')); add_filter("manage_edit-portfolio-post-type_columns", array($this, "portfolio_edit_columns")); add_action("manage_posts_custom_column", array($this,"portfolio_columns_display")); add_action("admin_init", array($this,"add_portfolio_meta")); add_action('save_post', array($this,'update_portfolio_meta')); } function create_portfolio() { $portfolio_args = array( 'label' => __('Portfolio', 'ninebit'), 'singular_label' => __('Portfolio', 'ninebit'), 'public' => true, 'show_ui' => true, 'capability_type' => 'post', 'hierarchical' => false, 'rewrite' => array('slug' => __('portfolio', 'ninebit')), 'supports' => array('title', 'editor', 'thumbnail') ); register_post_type('portfolio-post-type', $portfolio_args); register_taxonomy('portfolio-category', array('portfolio-post-type'), array( 'hierarchical' => true, 'labels' => array( 'name' => __( 'Portfolio Categories', 'ninebit' ), 'singular_name' => __( 'Portfolio Category', 'ninebit' ), 'add_new_item' => __( 'Add Portfolio Category','ninebit' ), 'separate_items_with_commas' => __( 'Separate categories with commas', 'ninebit' ), 'add_or_remove_items' => __( 'Add or remove portfolio categories', 'ninebit' ), 'choose_from_most_used' => __( 'Choose from the most used portfolio categories', 'ninebit' ), ), 'show_ui' => true, 'rewrite' => array('slug' => __('portfolio-category', 'ninebit')), 'query_var' => true, ) ); } function portfolio_edit_columns($portfolio_columns) { } function portfolio_columns_display($portfolio_columns) { } function add_portfolio_meta() { } function update_portfolio_meta() { } }
There are a lot of different options that you can set for your custom taxonomy. A taxonomy can function like either categories or tags in that they can be grouped into sub-groups (like categories can be for regular blog posts) or not (like tags). The ‘hierarchical’ true/false setting that you pass in specifies whether to turn this component on or not (true = on, false = off). If this is set to true, then your custom taxonomy will behave like categories. If it is set to false, it will behave like tags do.
Customizing the Display of Columns in the WordPress Panel
For our next two functions, we’re going work on the display of the different columns in the WordPress admin panel when we enter the portfolio suction. The first uses the manage_edit_post_type function and sets the column headers (at the top of the table in the dashboard). The second uses the manage_posts_custom_column function and this handles what is actually going to be displayed in each column row. In our case we’re going to want to display the project title (which is a supported default built-in type) and our portfolio categories taxonomy (which is custom). Because we have set that as a custom configuration, we’re going to need to write some code to tell WordPress how to display this custom column.
class NBS_Portfolio { function __construct() { register_activation_hook(__FILE__, array($this, 'activation')); register_deactivation_hook(__FILE__, array($this, 'deactivation')); add_action('init', array($this, 'create_portfolio')); add_filter("manage_edit-portfolio-post-type_columns", array($this, "portfolio_edit_columns")); add_action("manage_posts_custom_column", array($this,"portfolio_columns_display")); add_action("admin_init", array($this,"add_portfolio_meta")); add_action('save_post', array($this,'update_portfolio_meta')); } function create_portfolio() { $portfolio_args = array( 'label' => __('Portfolio', 'ninebit'), 'singular_label' => __('Portfolio', 'ninebit'), 'public' => true, 'show_ui' => true, 'capability_type' => 'post', 'hierarchical' => false, 'rewrite' => array('slug' => __('portfolio', 'ninebit')), 'supports' => array('title', 'editor', 'thumbnail') ); register_post_type('portfolio-post-type', $portfolio_args); register_taxonomy('portfolio-category', array('portfolio-post-type'), array( 'hierarchical' => true, 'labels' => array( 'name' => __( 'Portfolio Categories', 'ninebit' ), 'singular_name' => __( 'Portfolio Category', 'ninebit' ), 'add_new_item' => __( 'Add Portfolio Category','ninebit' ), 'separate_items_with_commas' => __( 'Separate categories with commas', 'ninebit' ), 'add_or_remove_items' => __( 'Add or remove portfolio categories', 'ninebit' ), 'choose_from_most_used' => __( 'Choose from the most used portfolio categories', 'ninebit' ), ), 'show_ui' => true, 'rewrite' => array('slug' => __('portfolio-category', 'ninebit')), 'query_var' => true, ) ); } function portfolio_edit_columns($portfolio_columns) { $portfolio_columns = array( "cb" => "<input type=\"checkbox\" />", "title" => __('Project Title', 'ninebit'), "portfolio_categories" => __('Portfolio Categories', 'ninebit'), ); return $portfolio_columns; } function portfolio_columns_display($portfolio_columns) { global $post; switch ($portfolio_columns) { case "portfolio_categories": $item_terms = get_the_terms($post->ID, 'portfolio-category'); $numItems = count($item_terms); $i = 0; if(!$item_terms) echo "--"; else { foreach($item_terms as $item) { if(++$i === $numItems) { // last item echo $item->name; } else { echo $item->name.", "; } } } break; } } function add_portfolio_meta() { } function update_portfolio_meta() { } }
If you wanted to add more columns for other taxonomies, you’d just need to add a key to the array for the column headers. You don’t have to do anything if it’s one of the built-in types, but if it’s another custom set of data then you’d need to add a case matching the associative array key to show the output in each column item.
Meta Boxes
Next we’re going to define some functionality when editing a single portfolio item in WordPress. More specifically, we’re going to add a “meta box” to our post type using the add_meta_box function. A meta box is simply some fields where we can attach some additional data to each custom post type. In our case, we’re going to want to add a field for a URL to a preview image and a field for a URL to our actual portfolio site. A lot of times a portfolio demo is external (because it is often a project you’ve done for a client that’s now live on the client’s web site). Because of this, we’re going to want to set a place where we can link to somewhere where our work is hosted.
In the function where we register our meta box using add_meta_box we need to specify a callback where we can set the actual markup for each field. We’ll do that by specifying the “portfolio_meta” function as this callback. We’ll also need to attach the functionality that we need when we’re actually saving a post. When this event occurs, we’re going to want to dig the current values out of our meta fields (based on the “name” attribute set in the <input> field HTML) and save them to the database using the update_post_meta function.
class NBS_Portfolio { function __construct() { register_activation_hook(__FILE__, array($this, 'activation')); register_deactivation_hook(__FILE__, array($this, 'deactivation')); add_action('init', array($this, 'create_portfolio')); add_filter("manage_edit-portfolio-post-type_columns", array($this, "portfolio_edit_columns")); add_action("manage_posts_custom_column", array($this,"portfolio_columns_display")); add_action("admin_init", array($this,"add_portfolio_meta")); add_action('save_post', array($this,'update_portfolio_meta')); } function create_portfolio() { $portfolio_args = array( 'label' => __('Portfolio', 'ninebit'), 'singular_label' => __('Portfolio', 'ninebit'), 'public' => true, 'show_ui' => true, 'capability_type' => 'post', 'hierarchical' => false, 'rewrite' => array('slug' => __('portfolio', 'ninebit')), 'supports' => array('title', 'editor', 'thumbnail') ); register_post_type('portfolio-post-type', $portfolio_args); register_taxonomy('portfolio-category', array('portfolio-post-type'), array( 'hierarchical' => true, 'labels' => array( 'name' => __( 'Portfolio Categories', 'ninebit' ), 'singular_name' => __( 'Portfolio Category', 'ninebit' ), 'add_new_item' => __( 'Add Portfolio Category','ninebit' ), 'separate_items_with_commas' => __( 'Separate categories with commas', 'ninebit' ), 'add_or_remove_items' => __( 'Add or remove portfolio categories', 'ninebit' ), 'choose_from_most_used' => __( 'Choose from the most used portfolio categories', 'ninebit' ), ), 'show_ui' => true, 'rewrite' => array('slug' => __('portfolio-category', 'ninebit')), 'query_var' => true, ) ); } function portfolio_edit_columns($portfolio_columns) { $portfolio_columns = array( "cb" => "<input type=\"checkbox\" />", "title" => __('Project Title', 'ninebit'), "portfolio_categories" => __('Portfolio Categories', 'ninebit'), ); return $portfolio_columns; } function portfolio_columns_display($portfolio_columns) { global $post; switch ($portfolio_columns) { case "portfolio_categories": $item_terms = get_the_terms($post->ID, 'portfolio-category'); $numItems = count($item_terms); $i = 0; if(!$item_terms) echo "--"; else { foreach($item_terms as $item) { if(++$i === $numItems) { // last item echo $item->name; } else { echo $item->name.", "; } } } break; } } function add_portfolio_meta(){ add_meta_box("portfolio_details", __("Portfolio Item Details", 'ninebit'), array($this, "portfolio_meta"), "portfolio-post-type", "normal", "low"); } function portfolio_meta() { global $post; $custom = get_post_custom($post->ID); $website_url = isset( $custom['website_url'] ) ? $custom['website_url'][0] : "";; $preview_image = isset( $custom['preview_image'] ) ? $custom['preview_image'][0] : ""; wp_nonce_field('nbs_meta_box_nonce', 'meta_box_nonce'); echo '<p><label>'.__('Preview Image', 'ninebit').':</label><br />'; echo '<input name="preview_image" id="nbs-meta-preview-image" style="width: 97%;" value="'.$preview_image.'" /></a></p>'; echo '<p><label>'.__('Website URL', 'ninebit').':</label><br />'; echo '<input name="website_url" style="width: 97%;" value="'.$website_url.'" /></p>'; } function update_portfolio_meta() { global $post; if( defined('DOING_AUTOSAVE') && DOING_AUTOSAVE ) return; if(!isset( $_POST['meta_box_nonce'] ) || !wp_verify_nonce( $_POST['meta_box_nonce'], 'nbs_meta_box_nonce' )) return; if(!current_user_can('edit_post')) return; if(isset($_POST["website_url"])) update_post_meta($post->ID, "website_url", $_POST["website_url"]); if(isset($_POST["preview_image"])) update_post_meta($post->ID, "preview_image", $_POST["preview_image"]); } }
We are also using a nonce for a little extra security.
Front End
Now that we have all of our functionality there in our plugin, how would we go about displaying this on the front-end? To do this, all we need to do is use WP_Query to loop through our custom post type entries and display them. We could do something like the following in a page template file or a shortcode that we’ve registered…
$paged = (get_query_var('paged')) ? get_query_var('paged') : 1; $wp_query = new WP_Query( array('post_type' => 'portfolio-post-type', 'ignore_sticky_posts'=>1, 'posts_per_page' => 6, 'paged'=>$paged) ); while ( $wp_query->have_posts() ) : $wp_query->the_post(); $custom = get_post_custom($post->ID); $preview_image = $custom["preview_image"][0]; $website_url = $custom["website_url"][0]; echo '<a href="'.$website_url.'"><img src="'.$preview_image.'" alt="'.get_the_title().'" /></a>'; echo '<h3>'.get_the_title().'</h3>'; echo '<a href="'.$website_url.'">View Project</a>'; endwhile;
Of course, we’d probably want to add a few more <div> elements to our output with some classes so we could use CSS to style it up all nice, but all of the basic elements that we need are there.
Page templates are nice, but sometimes you also may want to access the elements of your portfolio on another page (say like on the home page of your website to showcase your most recent projects). To do this, we can register a shortcode to our plugin. A lot of the code will be very similar to what we did in the page template.
The first thing we’re going to want to add a shortcode to our constructor…
class NBS_Portfolio { function __construct() { register_activation_hook(__FILE__, array($this, 'activation')); register_deactivation_hook(__FILE__, array($this, 'deactivation')); add_action('init', array($this, 'create_portfolio')); add_filter("manage_edit-portfolio-post-type_columns", array($this, "portfolio_edit_columns")); add_action("manage_posts_custom_column", array($this,"portfolio_columns_display")); add_action("admin_init", array($this,"add_portfolio_meta")); add_action('save_post', array($this,'update_portfolio_meta')); add_shortcode('portfolio', array($this, 'display_portfolio_shortcode')); } ...
Then we just have to add the following method to our class…
... function display_portfolio_shortcode($atts, $content = null) { $wp_query = new WP_Query( array('post_type' => 'portfolio-post-type', 'ignore_sticky_posts'=>1, 'posts_per_page' => 6, ) ); $portfolioMarkup = ''; while ( $wp_query->have_posts() ) : $wp_query->the_post(); $custom = get_post_custom($post->ID); $preview_image = $custom["preview_image"][0]; $website_url = $custom["website_url"][0]; $portfolioMarkup .= '<a href="'.$website_url.'"><img src="'.$preview_image.'" alt="'.get_the_title().'" /></a>'; $portfolioMarkup .= '<h3>'.get_the_title().'</h3>'; $portfolioMarkup .= '<a href="'.$website_url.'">View Project</a>'; endwhile; return $portfolioMarkup; }
The difference here is that in this case we are returning markup instead of echoing it out to the browser.
Now when the user types the [portfolio] shortcode into their editor WordPress will parse the shortcode and execute our code to return the portfolio markup. The name of the shortcode is specifed as the first parameter in the add_shortcode function. We set it to “portfolio” but you could change it to anything else that you wanted to.
If we wanted to we could also set some attributes for our shortcodes for the user to pass in to customize the output of the portfolio items. For more information on how to do that, see the WordPress shortcode documentation.
Conclusion
So there you have it. What we have done here is useful because we have covered a substantial amount of the details involved in customizing WordPress via custom posts the form of a plugin. There will be challenges involved in extending the functionality further in other ways, but most of it likely involves expanding on the topics discussed above in some manner or another. You could basically repeat this process with other custom post types and extend the functionality of WordPress however you want! Because we’ve covered such a wide array of topics, we’ve only scratched the surface of many of these topics. So be sure to consult the WordPress codex for all of the options that you can utilize for the functions that we’ve covered to further customize your post types to suit your needs.
You can grab the final code below. Extract the plugin to the wp-content/plugins directory and then login to your WordPress dashboard and activate it. It also has some metadata in the comment added to it to distinguish it from other plugins in the plugin section of the WordPress dashboard. This is discussed in the WordPress documentation for writing plugins
Until next time… happy WordPressing! PHP, web development, WordPress, WordPress plugins![9bit Studios E-Books](/public/web/ads/books.png)
0 Responses to WordPress Custom Posts and Plugins