CPTs are what really makes WordPress a content management system. With them, you can quickly build out custom functionality and store data in a consistent way.
With a default installation of WordPress, you have several post types already being used. The post types you may be most familiar with are pages and posts, but there are a few more. These post type values are all stored in the database wp_posts
table, and they all use the post_type
field to separate them.
Your posts are your blog or news or whatever you want to call your constant barrage of content to be indexed by internet search engines. You can categorize your posts, tag them with keywords, set publish dates, and more. In general, posts are shown in some kind of list view in reverse chronological order on the frontend of your website.
WordPress has your back and saves your posts as revisions every time you or anyone edits a post. This feature is on by default, and you can use it to revert your content to what it was if something got messed up along the way.
Sometimes, if your application is set up to make a lot of post_content
changes, the wp_posts
table becomes flooded with post revisions. So you may want to limit the amount of revisions stored in the wp_posts
table. To do this, add the following code to your wp-config.php file:
define
(
'WP_POST_REVISIONS'
,
5
);
Here, 5 is the number of revision posts to store for a given post. A value of 0
will turn off post revisions. A value of true
or -1
will store an infinite number of revisions (it can take a lot of disk space to store infinity something).
The Customizer has a setting for “Additional CSS.” This CSS is stored in a custom post of the type custom_css
. The additional CSS is specific to the active theme, and each theme will have a separate custom_css
post to store the CSS in. It might seem odd to store these additional CSS rules in a custom post, but it was done that way for performance reasons. You could imagine a site with a lot of custom CSS. Options are sometimes automatically loaded or cached, and those systems could break or slow down with large amounts of CSS.
Changesets are like post revisions but for your customizer settings instead of post content. When changing settings in the Customizer, those changes are stored in a post of the type customize_changeset
. If you accidentally close your browser tab and then return to the customizer, WordPress will prompt you to restore those changes from the changeset.
WordPress allows you to embed content from supported oEmbed providers by placing the URL to the content on its own line in the post editor. If you place the URL to a YouTube video on its own line, WordPress will detect that the URL is for YouTube.com, then make a call to the corresponding oEmbed URL to insert the YouTube player into the post on the frontend of your site. The embed code from that request is cached into a custom post of type oembed_cache
and set as a child of the post you were editing.
Since WordPress 4.9, administrators have been able to manage personal data exports or personal data deletions on behalf of users.1 You can find the screen to manage personal data exports and deletions on the Tools menu in the dashboard. These requests are stored in a custom post of type user_request
. See more about how these requests are handled in the WordPress reference for the wp_create_user_request()
function.
Part of the new block-based editor introduced in WordPress 5.0 is the ability to save blocks you configure within a post as a “reusable block.” These reusable blocks are stored in a custom post of the type wp_block
. Reusable blocks are covered in Chapter 11.
Just like the default WordPress post types, you can create your own CPTs to manage any data you need, depending on what you are building. Every CPT is really just a post used differently. You could register a CPT for a dinner menu at a restaurant, for cars for an auto dealer, for people to track patient information and documents at a doctor’s office, or for pretty much anything you can think of. No, really, any type of content you can think of can be stored as a post with attached files, custom metadata, and custom taxonomies.
In our SchoolPress example, we are going to be building a CPT for managing homework assignments on a teacher’s website. Our teacher wants to make a post of some kind where they can add assignments and their students can find them on the class website. The teacher would also like to be able to upload supporting documents and have commenting available in case any students have questions. A CPT sounds in order, doesn’t it?
We can store this information the same way posts are dealt with and display them to the end user in the theme using the same wp_query
loop we would with posts.
You can register a CPT by using the function register_post_type()
. And in most cases, you’ll register your CPT in your theme’s functions.php file or in a custom plugin file. This function expects two parameters—the name of the post type you are creating and an array of arguments:
$post_type
The name of your CPT; in our example, our CPT name is “homework.” This string must be no longer than 20 characters and can’t have capital letters, spaces, or any special characters except a hyphen or an underscore. If you are making a plugin for public consumption, you will want to use a prefix on your CPT’s name to avoid conflicts if another plugin uses the same name.
$args
This is an array of many different arguments that will dictate how your CPT will be set up.
Following is a list of all available arguments and what they are used for:
label
The display name of your post type. In our example, we use “Homework.”
labels
An optional array of labels to use for describing your post type throughout the user interface:
name
The plural display name of your post type. This will overrides the label
argument.
singular_name
The singular name for any particular post. This defaults to the name if not specified.
add_new
Defaults to the string “Add New.”
add_new_item
Defaults to “Add New Post.”
edit_item
Defaults to “Edit Post.”
new_item
Defaults to “New Post.”
view_item
Defaults to “View Post.”
search_items
Defaults to “Search Posts.”
not_found
Defaults to “No Posts Found.”
not_found_in_trash
Defaults to “No posts found in Trash.”
parent_item_colon
Defaults to “Parent Page:” and is only used on hierarchical post types.
all_items
Defaults to “All Posts.”
menu_name
The menu name for the post type, usually the same as label
or labels->name
.
description
publicly_queryable
An optional Boolean that specifies whether queries on your post type can be run on the frontend or theme of your application. By default, publicly_queryable
is turned on.
exclude_from_search
An optional Boolean that specifies whether your post type posts can be queried and displayed in the default WordPress search results. This is off by default so that your posts will be searchable.
capability_type
An optional string or array. If not specifically defined, capability_type
will default to post
. You can pass in a string of an existing post type, and the new post type you are registering will inherit that post type’s capabilities. You can also define your own capability type, which will set default capabilities for your CPT for reading, publishing, editing, and deleting. You can also pass in an array if you want to use different singular and plural words for your capabilities. For example, you can just pass in the string “homework” since the singular and plural forms for “homework” are the same, but you would pass in an array like array( 'submission', 'submissions' )
when the forms are different.
capabilities
An optional array of the capabilities of the post type you are registering. You can use this instead of capability_type
if you want more granular control over the capabilities you are assigning to your new CPT.
There are two types of capabilities: meta and primitive. Meta capabilities are tied to specific posts, whereas primitive capabilities are more general purpose. In practice, this means that when checking if a user has a meta capability, you must pass in a $post_id
parameter:
//meta capabilities are related to specific posts
if
(
current_user_can
(
"edit_post"
,
$post_id
))
{
//the current user can edit the post with ID = $post_id
}
Unlike meta capabilities, primitive capabilities aren’t checked against a specific post:
//primitive capabilities aren't related to specific posts
if
(
current_user_can
(
"edit_posts"
))
{
//the current user can edit posts in general
}
The capabilities that can be assigned to your custom post type are as follows:
edit_post
A meta capability for a user to edit a particular post.
read_post
A meta capability for a user to read a particular post.
delete_post
A meta capability for a user to delete a particular post.
edit_posts
A primitive capability for a user to be able to create and edit posts.
edit_others_posts
A primitive capability for a user to be able to edit others’ posts.
publish_posts
A primitive capability for a user to be able to publish posts.
read_private_posts
A primitive capability for a user to be able to read private posts.
read
A primitive capability for a user to be able to read posts.
delete_posts
A primitive capability for a user to be able to delete posts.
delete_private_posts
A primitive capability for a user to be able to delete private posts.
delete_published_posts
A primitive capability for a user to be able to delete posts.
delete_others_posts
A primitive capability for a user to be able to delete other people’s posts.
edit_private_posts
A primitive capability for a user to be able to edit private posts.
edit_published_posts
A primitive capability for a user to be able to publish posts.
map_meta_cap
Whether to use the internal default meta capability handling (capabilities and roles are covered in Chapter 6). Defaults to false
. You can always define your own capabilities using capabilities
; but if you don’t, setting map_meta_cap
to true
will make the following primitive capabilities be used by default or in addition to using capability_type
: read
, delete_posts
, delete_private_posts
, delete_published_posts
, delete_others_posts
, edit_private_posts
, and edit_published_posts
.
hierarchical
An optional Boolean that specifies whether a post can be hierarchical and have a parent post. WordPress pages are set up like this so you can nest pages under other pages. The hierarchical argument is turned off by default.
public
An optional Boolean that specifies if a post type is supposed to be used publicly or not in the backend or frontend of WordPress. By default, this argument is false
; so without including this argument and setting it to true
, you couldn’t use this post_type
in your theme. If you set public
to true
, it automatically sets exclude_from_search
, publicly_queryable
and show_ui_nav_menus
to true
unless otherwise specified.
Most CPTs will be public so they are shown on the frontend or available to manage through the WordPress dashboard. Other CPTs (like the default Revisions CPT) are updated behind the scenes based on other interactions with your app and would have public
set to false
.
rewrite
An optional Boolean or array used to create a custom permalink structure for a post type. By default, this is set to true
, and the permalink structure for a custom post is /post_type/post_title/
. If set to false
, no custom permalink would be created. You can completely customize the permalink structure of a post by passing in an array with the following arguments:
slug
Defaults to the post_type
but can be any string you want. Remember not to use the same slug in more than one post type because they must be unique.
with_front
Whether or not to prepend the “front base” to the front of the CPT permalink. If set to true
, the slug of the “front page” set on the Settings → Reading page of the dashboard will be added to the permalink for posts of this post type.
feeds
Boolean that specifies whether a post type can have an RSS feed. The default value of this argument is set to the value of the has_archive
argument. If feeds
is set to false
, no feeds will be available.
pages
Boolean that turns on pagination for a post type. If true
, archive pages for this post type will support pagination.
ep_mask
EP, or endpoints, can be very useful. With this argument you assign an endpoint mask for a post type. For instance, we could set up an endpoint for a post type of homework called “pop-quiz.” The permalink would look like /homework/post-title/pop-quiz/
. In MVC terminology, a CPT is similar to a module, and endpoints can be thought of as different views for that module. (We cover endpoints and other rewrite functions in Chapter 7.)
has_archive
An optional Boolean or string that specifies whether a post type can have an archive page. By default this argument is set to false
, so you will want to set it to true
if you would like to use it in your theme. The archive-<post_type>.php file in your theme will be used to render the archive page. If that file is not available, the archive.php or index.php file will be used instead.
query_var
An optional Boolean or string that sets the query_var
key for the post type. This is the name of your post type in the database and used when writing queries to work with this post type. The default value for this argument is set to the value of post_type
argument. In most cases you wouldn’t need your query_var
and your post_type
to be different, but you can imagine a long post type name like directory_entry
for which you would want to use a shorter slug like “dir.”
supports
An optional Boolean or array that specifies what meta box features will be made available on the new post or edit post page. By default, an array with the arguments of title
and editor
is passed in. Following is a list of all available arguments (to use one of these features with your CPT, be sure it’s included in the supports
array):
title
editor
comments
revisions
trackbacks
author
excerpt
page-attributes
thumbnail
custom-fields
post-formats
register_meta_box_cb
An optional string that allows you to provide a custom callback function for integrating your own custom meta boxes.
permalink_epmask
An optional string for specifying which endpoint types you would like to associate with a custom post type. The default rewrite endpoint bitmask is EP_PERMALINK
. For more information on endpoints, see Chapter 7.
taxonomies
An optional array that specifies any built-in (categories and tags) or custom registered taxonomies you would like to associate with a post type. By default, no taxonomies are referenced. For more on taxonomies, see “Creating Custom Taxonomies”.
show_ui
An optional Boolean that specifies whether the basic post UI will be made available for a post type in the backend. The default value is set to the value of the public
argument. If show_ui
is false
, you will have no way of populating your posts from the backend administration area.
It’s a good idea to set show_ui
to true
, even for CPTs that won’t generally be added or edited through the administrator dashboard. For example, the bbPress plugin adds Topics and Replies as CPTs that are added and edited through the forum UI on the frontend. However, show_ui
is set to true
, providing another interface from which administrators can search, view, and manage topics and replies.
menu_position
An optional integer used to set the menu order of a post type menu item in the backend, left-side navigation.
The WordPress Codex provides a nice list of common menu position values to help you figure out where to place the menu item for your CPT:
5: below Posts
10: below Media
15: below Links
20: below Pages
25: below comments
60: below first separator
65: below Plugins
70: below Users
75: below Tools
80: below Settings
100: below second separator
menu_icon
An optional string of a URL to a custom icon that can be used to represent a post type.
can_export
An optional Boolean that specifies whether a post type can be exported via the WordPress exporter in Tools → Export. This argument is set to true
by default, allowing the administrator to export.
show_in_nav_menus
An optional Boolean that specifies whether posts from a post type can be added to a custom navigation menu in Appearance → Menus. The default value of this argument is set to the value of the public argument.
show_in_menu
An optional Boolean or string that specifies whether to show the post type in the backend admin menu and possibly where to show it. If set to true
, the post type is displayed as its own item on the menu. If set to false
, no menu item for the post type is shown. You can also pass in a string of the name of any other menu item. Doing this places the post type in the submenu of the passed-in menu item. The default value of this argument is set to the value of the show_ui
argument.
show_in_admin_bar
An optional Boolean that specifies whether a post type is available in the WordPress administrator bar. The default value of this argument is set to the value of the show_in_menu
argument.
delete_with_user
An optional Boolean that specifies whether to delete all the posts for a post type created by a given user. If set to true
, posts the user created will be moved to the trash when the user is deleted. If set to false
, posts will not be moved to the trash when the user is deleted. By default, posts are moved to the trash if the argument post_type_supports
has author
within it. If not, posts are not moved to the trash.
show_in_rest
An optional Boolean that specifies whether this CPT is exposed through the REST API. The default value of this argument is false
. The REST API is covered in Chapter 10.
rest_base
An optional string to change the base slug that is used when accessing CPTs of this type through the REST API. The default value for this argument is the post type name set as the first parameter to the register_post_type()
function.
rest_controller_class
An optional string to change the controller used when accessing this post type through the REST API. This argument should be set to WP_REST_Posts_Controller
or the name of a PHP class that inherits the WP_REST_Posts_Controller
class. The default value for this argument is WP_REST_Posts_Controller
.
_builtin
You shouldn’t ever need to use this argument. Default WordPress post types use this to differentiate themselves from CPTs.
_edit_link
The URL of the edit link on the post. This is also for internal use, and you shouldn’t need to use it. If you’d like to change the page linked to when clicking to edit a post, use the get_edit_post_link
filter, which passes the default edit link along with the ID of the post.
Example 5-1 illustrates registering new “Homework” and “Submissions” CPTs using register_post_type()
. You can find the code for the register_post_type()
function in wp-includes/post.php. Notice that in our example we are using only a few of the many available arguments.
<?
php
// custom function to register a "homework" post type
function
schoolpress_register_post_type_homework
()
{
register_post_type
(
'homework'
,
array
(
'labels'
=>
array
(
'name'
=>
__
(
'Homework'
),
'singular_name'
=>
__
(
'Homework'
)
),
'public'
=>
true
,
'has_archive'
=>
true
,
)
);
}
// call our custom function with the init hook
add_action
(
'init'
,
'schoolpress_register_post_type_homework'
);
// custom function to register a "submissions" post type
function
schoolpress_register_post_type_submission
()
{
register_post_type
(
'submissions'
,
array
(
'labels'
=>
array
(
'name'
=>
__
(
'Submissions'
),
'singular_name'
=>
__
(
'Submission'
)
),
'public'
=>
true
,
'has_archive'
=>
true
,
)
);
}
// call our custom function with the init hook
add_action
(
'init'
,
'schoolpress_register_post_type_submission'
);
?>
If you dropped the preceding code in your active theme’s functions.php file or an active plugin, you should notice two new menu items on the WordPress admin called Homework and Submissions under the Comments menu item.
If you get tired of writing functions to register the various custom post types that you want to use, you can use this cool plugin called Custom Post Type UI.
We briefly touched on taxonomies in Chapter 2, but what exactly is a taxonomy? Taxonomies group posts by terms. Think post categories and post tags; these are just built-in taxonomies attached to the default “post” post type. You can define as many custom taxonomies or categories as you want and span them across multiple post types. For example, we can create a custom taxonomy “Subject” that has all school-related subjects as its terms and is tied to our “Homework” CPT.
One question you will often encounter when you want to attach bits of data to posts is whether to use a taxonomy or a post meta field (or both). Generally, terms that group different posts together should be coded as taxonomies, while data specific to each individual post should be coded as post meta fields.
These work well for data specific to individual posts and not used to group posts together. In SchoolPress, it makes sense to code things like “required assignment length” (e.g., 500 words) as a meta field. In practice, only a few different lengths will ever be used, but we won’t ever need to “get all assignments that require 500 words.” So a post meta field is adequate for this information.
These are good for data that is used to group posts together. In SchoolPress, it makes sense to code things such as an assignment’s subject (e.g., math or English) as a taxonomy. Unlike assignment length, we will want to run queries like “get all math assignments.” This is easily done through a taxonomy query. More important, queries like this run faster on taxonomy data than on meta fields.
Why are taxonomy queries generally faster? Meta fields are stored in the wp_postmeta
. If we were storing an assignment’s due date as a post meta field, it would look like this:
|
|
|
|
|
|
|
|
|
|
|
|
The meta_id
, post_id
, and meta_key
columns are indexed, but the meta_value
column is not. This means that queries like “get the due date for this assignment” will run quickly, but queries like “get all assignments due on 2018-09-07” will run slower, especially if you have a large site with lots of data piled into the wp_postmeta
table. The reason the meta_value
key is the lone column in wp_postmeta
without an index is that adding an index here would greatly increase both the storage required for this table and also the insert times. In practice, a site will have many different meta values, whereas there will be a smaller set of post IDs and meta keys to build indexes for.
If you stored assignment due dates in a custom taxonomy, the “get all assignments due on this date” query will run much faster. Each specific due date would be a term in the wp_terms
table with a corresponding entry in the wp_terms_taxonomy
table. The wp_terms_relationships
table that attaches terms to posts has both the object_id
(posts are objects here) and term_taxonomy_id
fields indexed. So “get all posts with this term_taxonomy_id
" is a speedy query.
If you just want to show the due date on the assignment page, you should store it in the post meta fields. If you want to offer a report of all assignments due on a certain date, you should consider adding a taxonomy to track due dates.
On the other hand, due to the nature of due dates (you potentially have 365 new terms each year), using a taxonomy for them might be overkill. You would end up with a lot of useless terms in your database keeping track of which assignments were due two years ago.
Also, in this specific case, the speed increases might be negligible because the due date report is for a subset of assignments within a specific class group. In practice, we won’t be querying for assignments by due date across the entire wp_postmeta
table. We’ll filter the query to run only on assignment posts for a specific class. While there may be millions of rows in the wp_postmeta
table for a SchoolPress site at scale (hundreds of schools, thousands of teachers and classes), there will only be a few assignments for a specific class or group of classes one student is in.
Another consideration when choosing between meta fields and taxonomies is how that data will be managed by users. If a field is only going to be used in the backend code, and you don’t have query speed issues, storing it in post meta is as simple as one call to update_post_meta()
. If you’d like administrators to be able to create new terms, write descriptions for them, build hierarchies, and use drop-downs or checkboxes to assign them to posts, well then we’ve just described exactly what you get for free when you register a taxonomy. When using post meta fields, you need to build your own UI into a meta box.
Finally, I did mention earlier that there are times when you want to use both a meta field and a taxonomy to track one piece of data. An example of this in the context of the SchoolPress app could be tracking a textbook and chapter for an assignment. Imagine you want a report for a student with all of their assignments organized by textbook and ordered by chapter within those books. Because you want to allow teachers to manage textbooks as terms in the administrator menu, and you’ll want to do queries like “get all assignments for this textbook,” it makes sense to store textbooks in a custom taxonomy.
On the other hand, chapters can be stored in post meta fields. Chapters are common across books and assignments, but it doesn’t make sense to query for “all chapter 1 assignments” across many different textbooks. Since we’ll be able to prefilter to get all assignments by textbook or by student, we can use a chapter meta field, or possibly a textbook_chapter
meta field with data like “PrinciplesOfMath.Ch1” to order the assignments for the report.
Phew…now that we’ve figured out when we’ll want to use taxonomies, let’s find out how to create them.
The register_taxonomy()
function accepts the following three parameters:
$taxonomy
A required string of the name of your taxonomy. In our example, our taxonomy name is “subject.”
$object_type
A required array or string of the CPT(s) you are attaching this taxonomy to. In our example, we are using a string and attaching the subject taxonomy to the homework post type. We could set it to more than one post type by passing in an array of post type names.
$args
This is optional array of many arguments dictates how your custom taxonomy is set up. Notice that in our example we use only a few of the many available arguments that could be passed into the register_taxonomy()
function.
Following is a list of all available arguments:
label
labels
Optional array of labels to use for describing your taxonomy throughout the user interface:
name
The plural display name of your taxonomy. This will override the label argument.
singular_name
The name for one object of this taxonomy. Defaults to “Category.”
search_items
Defaults to “Search Categories.”
popular_items
This string isn’t used on hierarchical taxonomies. Defaults to “Popular Tags.”
all_items
Defaults to “All Categories.”
parent_item
This string is only used on hierarchical taxonomies. Defaults to “Parent Category.”
parent_item_colon
The same as the parent_item
argument but with a colon at the end.
edit_item
Defaults to “Edit Category.”
view_item
Defaults to “View Category.”
update_item
Defaults to “Update Category.”
add_new_item
Defaults to “Add New Category.”
new_item_name
Defaults to “New Category Name.”
separate_items_with_commas
This string is used on nonhierarchical taxonomies. Defaults to “Separate tags with commas.”
add_or_remove_items
This string is used on nonhierarchical taxonomies. Defaults to “Add or remove tags.”
choose_from_most_used
This string is used on nonhierarchical taxonomies. Defaults to “Choose from the most used tags.”
hierarchical
Optional Boolean that specifies whether a taxonomy is hierarchical or if a taxonomy term may have parent terms or subterms. This is like the default categories taxonomy, and nonhierarchical taxonomies are like the default tags taxonomy. The default value for this argument is set to false
.
update_count_callback
Optional string that works like a hook. It’s called when the count of the associated post type is updated.
rewrite
Optional Boolean or array used to customize the permalink structure of a taxonomy. The default rewrite value is set to the taxonomy slug.
query_var
Optional Boolean or string that can be used to customize the query_var
, ?$query_var=$term
. By default, the taxonomy name is used as the query_var
.
public
Optional Boolean that specifies whether a taxonomy should be used publicly in the backend or frontend of WordPress. By default, this argument is false
; so if you didn’t include this argument and set it to true
, you couldn’t use this taxonomy in your theme. If you set public
to true
, it automatically sets show_ui
, publicly_queryable
, and show_in_nav_menus
to true
unless otherwise noted.
publicly_queryable
Optional Boolean that specifies whether the taxonomy should be publicly queryable on the frontend. The default is set to true
.
show_ui
Optional Boolean that specifies whether the taxonomy will have a backend admin UI, similar to the categories or tags interface. The default value of this argument is set to the value of the public
argument.
show_in_nav_menus
Optional Boolean specifying if a taxonomy will be available in navigation menus. This argument’s default value is set to the value of the public
argument.
show_in_rest
Optional Boolean that specifies whether this taxonomy is exposed through the REST API. The default value of this argument is false
. The REST API is covered in Chapter 10.
rest_base
Optional string to change the base slug that is used when accessing CPTs with this taxonomy through the REST API. The default value for this argument is the taxonomy name set as the first parameter to the register_taxonomy()
function.
rest_controller_class
Optional string to change the controller used when accessing posts with this taxonomy through the REST API. This argument should be set to WP_REST_Terms_Controller
or the name of a PHP class that inherits that class. The default value for this argument is WP_REST_Terms_Controller
.
show_tagcloud
Optional Boolean that specifies whether the taxonomy can be included in the Tag Cloud Widget. The default value of this argument is set to the value of the show_ui
argument.
show_admin_column
Optional Boolean that specifies whether a new column will be created for your taxonomy on the post type it is attached to on the post type’s edit/list page in the backend. This is false
by default.
capabilities
Optional array of capabilities for this taxonomy with a default of none
. You can pass in the following arguments and/or any custom-created capabilities:
manage_terms
edit_terms
delete_terms
assign_terms
In our homework
post type example, we are going to make a taxonomy called “Subject” so we can create a term for each subject like math, science, language arts, and social studies:
<?
php
// custom function to register the "subject" taxonomy
function
schoolpress_register_taxonomy_subject
()
{
register_taxonomy
(
'subject'
,
'homework'
,
array
(
'label'
=>
__
(
'Subjects'
),
'rewrite'
=>
array
(
'slug'
=>
'subject'
),
'hierarchical'
=>
true
)
);
}
// call our custom function with the init hook
add_action
(
'init'
,
'schoolpress_register_taxonomy_subject'
);
?>
Notice in the preceding code the subject taxonomy is set up like categories on a post because it’s hierarchical argument is set to true
. You can create as many subjects as you would like and nest them.
Under Homework → Subjects in the backend, you can add your terms the same way you would add new categories to a post.
What if you want to use a default taxonomy on a CPT? If you want to use the same tags taxonomy attached to the posts post type on our homework
post type, you can use the register_taxonomy_for_object_type()
function to attach any taxonomies to any post types. The register_taxonomy_for_object_type()
function is also located in wp-includes/taxonomy.php.
The register_taxonomy_for_object_type()
function accepts two parameters:
$taxonomy
Required string of the name of the taxonomy.
$object_type
Required string of the name of the post type to which you want to attach your taxonomy.
In this example, we are attaching the default tags taxonomy to our custom homework post type:
<?
php
function
schoolpress_register_taxonomy_for_object_type_homework
(){
register_taxonomy_for_object_type
(
'post_tag'
,
'homework'
);
}
add_action
(
'init'
,
'schoolpress_register_taxonomy_for_object_type_homework'
);
?>
If you run the example, notice that the “tags” taxonomy is now available under the Homework menu item. The Custom Post Types UI plugin also has a UI for creating and managing custom taxonomies.
In the following sections, we cover some things to keep in mind when using CPTs in your themes or plugins.
Most WordPress themes will have an archive.php file that renders your posts on a archive/listing page, and a single.php file that is responsible for rendering information about a single post. You can easily create dedicated archive and single files for your registered CPTs.
Make a copy of archive.php and name it archive-homework.php. You should now automatically have a listing archive page of all your homework assignment posts in the same format of your regular posts archive page (at domain.com/homework/).
You can apply the same method to the single.php file. Copy it and call it single-homework.php. You should now have a single page for each of your homework assignments (at domain.com/homework/science-worksheet/). Now you can change the markup of the CPT archive or single file to display your data differently from how your blog posts are displayed.
To use a custom archive file, you must set the has_archive
argument when registering your custom post type to true
. The has_archive
argument is part of the register_post_type()
function.
In some instances, creating an archive and single .php file for your custom post type may not be enough for the custom functionality you require. What if you want to loop through all the posts for a specific post type in a sidebar widget or in a shortcode on a page? With WP_Query
or get_posts()
, you can set the post_type
parameter to query and loop through your CPT posts the same way you would with regular posts.
In Example 5-2, we’ll build a homework submission form below any content provided for the single post of the homework
post type.
<?
php
function
schoolpress_the_content_homework_submission
(
$content
){
global
$post
,
$current_user
;
// Don't do this for any other post type than homework.
if
(
!
is_single
()
||
$post
->
post_type
!=
'homework'
)
return
$content
;
// Don't do this if the user is not logged in.
if
(
!
is_user_logged_in
()
)
return
$content
;
// Check if the current user has already made a submission
// to this homework assignment.
$submissions
=
get_posts
(
array
(
'post_author'
=>
$current_user
->
ID
,
'posts_per_page'
=>
'1'
,
'post_type'
=>
'submissions'
,
'meta_key'
=>
'_submission_homework_id'
,
'meta_value'
=>
$post
->
ID
)
);
foreach
(
$submissions
as
$submission
)
{
$submission_id
=
$submission
->
ID
;
}
// Process the form submission if the user hasn't already.
if
(
!
$submission_id
&&
isset
(
$_POST
[
'submit-homework-submission'
]
)
&&
isset
(
$_POST
[
'homework-submission'
]
)
)
{
$submission
=
$_POST
[
'homework-submission'
];
$post_title
=
$post
->
post_title
;
$post_title
.=
' - Submission by '
.
$current_user
->
display_name
;
// Insert the current users submission as a post into our
// submissions CPT.
$args
=
array
(
'post_title'
=>
$post_title
,
'post_content'
=>
$submission
,
'post_type'
=>
'submissions'
,
'post_status'
=>
'publish'
,
'post_author'
=>
$current_user
->
ID
);
$submission_id
=
wp_insert_post
(
$args
);
// Add post meta to tie this submission post to the homework post.
add_post_meta
(
$submission_id
,
'_submission_homework_id'
,
$post
->
ID
);
// Create a custom message.
$message
=
__
(
'Your homework has been submitted and is
awaiting review.'
,
'schoolpress'
);
$message
=
'<div class="homework-submission-message">'
.
$message
.
'</div>'
;
// Drop message before the filtered $content variable.
$content
=
$message
.
$content
;
}
// Add a link to the user's submission if a submission was already made.
if
(
$submission_id
)
{
$message
=
sprintf
(
__
(
'Click %s here %s to view your submission to this homework
assignment.'
,
'schoolpress'
),
'<a href="'
.
get_permalink
(
$submission_id
)
.
'">'
,
'</a>'
);
$message
=
'<div class="homework-submission-link">'
.
$message
.
'</div>'
;
$content
.=
$message
;
// Add a basic submission form after the $content variable being filtered.
}
else
{
ob_start
();
?>
<h3>
<?php
_e
(
'Submit your Homework below!'
,
'schoolpress'
);
?>
</h3>
<form method="post">
<?php
wp_editor
(
''
,
'homework-submission'
,
array
(
'media_buttons'
=>
false
)
);
?>
<input type="submit" name="submit-homework-submission" value="Submit" />
</form>
<?php
$form
=
ob_get_contents
();
ob_end_clean
();
$content
.=
$form
;
}
return
$content
;
}
// Add a filter on 'the_content' so we can run our custom code
// to deal with homework submissions
add_filter
(
'the_content'
,
'schoolpress_the_content_homework_submission'
,
999
);
?>
You have probably noticed that we haven’t yet discussed the following functions:
ob_start()
This PHP function is used to turn output buffering on. While output buffering is active, no output is sent to the browser; instead, the output is stored in an internal buffer.
wp_editor()
This WordPress function outputs the same WYSIWYG editor that you get while adding or editing a post. You can call this function anywhere you would like to stick an editor. We thought the homework submission form would be a perfect place. We cover all the parameters of this function in Chapter 7.
ob_get_contents()
We set a variable called $form
to this PHP function. This makes all content between calling the ob_start()
function and this function into a variable called $form
.
ob_end_clean()
This PHP function clears the output buffer and turns off output buffering.
We used these functions in the previous sequence because the wp_editor()
function does not currently have an argument to return the editor as a variable and outputs it to the browser when it’s called. If we didn’t use these functions, we wouldn’t be able to put our editor after the $content
variable passed into the the_content
filter.
In the following code, we’ll make sure that only administrators have access to all homework submissions and that other users can access only the homework submissions they created:
<?
php
function
schoolpress_submissions_template_redirect
(){
global
$post
,
$user_ID
;
// only run this function for the submissions post type
if
(
$post
->
post_type
!=
'submissions'
)
return
;
// check if post_author is the current user_ID
if
(
$post
->
post_author
==
$user_ID
)
$no_redirect
=
true
;
// check if current user is an administrator
if
(
current_user_can
(
'manage_options'
)
)
$no_redirect
=
true
;
// if $no_redirect is false redirect to the home page
if
(
!
$no_redirect
)
{
wp_redirect
(
home_url
()
);
exit
();
}
}
// Use the template_redirect hook to call a function that decides if the
// current user can access the current homework submission.
add_action
(
'template_redirect'
,
'schoolpress_submissions_template_redirect'
);
?>
You can utilize the same post meta functions we went over in detail in Chapter 2 with any CPT you create. Getting, adding, updating, and deleting post metadata is consistent across all posts types.
If you registered a CPT and added custom-fields
in the supports
argument, by default, when adding a new post or editing a post of that post type, you will see a meta box called “Custom Fields.” You may already be familiar with this meta box; it’s a very basic form used to maintain metadata attached to a post.
If you don’t see the Custom Fields section on the edit post page, you may need to enable it. If you are using the Block Editor, click the More Tools and Options button (the three dots in the upper right). At the bottom of the page, click Options and then, in the Advanced Panels section, find the checkbox for Custom Fields. If you’re using the Classic Editor, at upper right, click the Screen Options tab, then find the Custom Fields checkbox.
What if you require a slicker UI for adding metadata on the backend? Well, building a custom meta box would be the solution for you.
The add_meta_box()
function will add a meta box to one or more screens:
$id
A required string of a unique identifier for the meta box you are creating.
$title
A required string of the title or visible name of the meta box you are creating.
$callback
A required string of a function name that’s called to output the HTML inside the meta box you are creating.
$screen
The optional screen or screens where your meta box will appear. Accepts a screen ID, WP_Screen
object, or array of screen IDs. The default is null
.
$context
An optional string of the context within the page where your meta box should show (normal
, advanced
, side
). The default is advanced.
$priority
An optional string of the priority within the context where the boxes should show (high
, low
).
$callback_args
An optional array of arguments that will be passed in the callback function you referenced with the $callback
parameter. Your callback function will automatically receive the $post
object and any other arguments you set here.
While we focus here on how to add meta boxes to the edit post screen, meta boxes can be used on any admin screen. To use meta boxes on your own page, use a unique value for the screen
parameter and then call do_meta_boxes()
for that screen in your page’s callback function. This is good for reporting pages or other pages where you’d like users to be able to hide boxes, rearrange boxes, or add new boxes through custom hooks.
In Example 5-3, we are going to build a custom meta box for all posts of our homework post type. This meta box will contain a checkbox indicating whether a homework submission is required as well as a date selector for the homework assignment’s due date.
<?
php
// Callback for adding a custom meta box.
function
schoolpress_homework_add_meta_boxes
(){
add_meta_box
(
'homework_meta'
,
'Additonal Homework Info'
,
'schoolpress_homework_meta_box'
,
'homework'
,
'side'
);
}
// Use the add_meta_boxes hook to call a custom function to add a new meta box.
add_action
(
'add_meta_boxes'
,
'schoolpress_homework_add_meta_boxes'
);
// This is the callback function called from add_meta_box.
function
schoolpress_homework_meta_box
(
$post
){
// Using 2 liens here so the url will fit in the book ;)
$smoothness_url
=
'http://ajax.googleapis.com/ajax/libs/'
;
$smoothness_url
.=
'jqueryui/1.12.1/themes/smoothness/jquery-ui.css'
;
// Enqueue jquery date picker.
wp_enqueue_script
(
'jquery-ui-datepicker'
);
wp_enqueue_style
(
'jquery-style'
,
$smoothness_url
);
// Set metadata if already exists.
$is_required
=
get_post_meta
(
$post
->
ID
,
'_schoolpress_homework_is_required'
,
1
);
$due_date
=
get_post_meta
(
$post
->
ID
,
'_schoolpress_homework_due_date'
,
1
);
// Output metadata fields.
?>
<p>
<input type="checkbox"
name="is_required" value="1"
<?php
checked
(
$is_required
,
'1'
);
?>
>
This assignment is required.
</p>
<p>
Due Date:
<input type="text"
name="due_date" id="due_date" value="
<?php
echo
$due_date
;
?>
">
</p>
<script>
// Attach jQuery date picker to our due_date field.
jQuery(document).ready(function() {
jQuery('#due_date').datepicker({
dateFormat : 'mm/dd/yy'
});
});
</script>
<?php
}
// Callback for saving custom metadata to the database.
function
schoolpress_homework_save_post
(
$post_id
){
// Don't save anything if WP is auto saving.
if
(
defined
(
'DOING_AUTOSAVE'
)
&&
DOING_AUTOSAVE
)
return
$post_id
;
// Check if correct post type and that the user has correct permissions.
if
(
'homework'
==
$_POST
[
'post_type'
]
)
{
if
(
!
current_user_can
(
'edit_page'
,
$post_id
)
)
return
$post_id
;
}
else
{
if
(
!
current_user_can
(
'edit_post'
,
$post_id
)
)
return
$post_id
;
}
// Update homework metadata.
update_post_meta
(
$post_id
,
'_schoolpress_homework_is_required'
,
$_POST
[
'is_required'
]
);
update_post_meta
(
$post_id
,
'_schoolpress_homework_due_date'
,
$_POST
[
'due_date'
]
);
}
// Call a custom function to handle saving our metadata.
add_action
(
'save_post'
,
'schoolpress_homework_save_post'
);
?>
If you are a good developer, you’re probably thinking to yourself: where are the nonces? How come these $_POST
values aren’t sanitized? If you aren’t thinking this, you should be, because security is very important! If you don’t know what we are talking about, see Chapter 8, where will cover these best practices (and nonces) in more detail. We deliberately left out this additional code to keep the example short and sweet, but know that when you are writing custom code, you should always use nonces and sanitize your data.
When creating meta boxes and custom meta fields, we recommend utilizing the CMB2 plugin. You can easily include CMB2 in your theme or any custom plugin to give you a fast and easy way to create custom meta boxes and the meta fields inside them.
The add_meta_box()
function works with both the Block Editor and the Classic Editor. In both cases, meta boxes added with the “side” context will show up in the right sidebar of the edit post screen. With the Classic Editor, all sidebar meta boxes are always visible.2 With the Block Editor, sidebar meta boxes appear under the Document tab in the right sidebar.
With both versions of the editor, meta boxes added with the normal
or advanced
contexts will show up below the main editor area.
Because you can also use blocks to update post meta, some meta boxes may work better as a block instead. We cover blocks more fully in Chapter 11, but following are some questions to ask yourself when deciding whether a certain field or function should be developed as a Custom Block Type or a meta box. The answers here are not hard-and-fast rules, but should be read as a suggestion to lean toward one implementation over another.
Use a meta box with the “normal” context to show up below the post editor. Alternatively, you can use a block along with a block template so every new post contains the block by default. More on block templates in Chapter 11.
If not, use a meta box with the “normal” context to show up below the post editor. There will be a larger area for your forms and fields to fit into. Paid Memberships Pro’s “Require Membership” meta box, with its single set of checkboxes, fits nicely into the sidebar. WooCommerce’s pricing fields, with tabs of their own, fit better below the post body.
Use a Custom Block Type so you can add multiple copies to the same post. Alternatively, you can use a meta box below the post along with PHP and JavaScript code to add extra fields on demand. A version of this is shown in Chapter 9.
CPTs are just posts. So you can use a call like get_post( $post_id )
to get an object of the WP_Post
class to work with. For complex CPTs, it helps to create a wrapper class so you can interact with your CPT in a more object-oriented way.
The basic idea is to create a custom-defined PHP class that includes as a property a post object generated from the ID of the CPT post. In addition to storing that post object, the wrapper class also houses methods for all of the functionality related to that CPT.
Example 5-4 shows the outline of a wrapper class for our homework
CPT.
<?
php
/*
Class wrapper for homework CPT
/wp-content/plugins/schoolpress/classes/class.homework.php
*/
class
Homework
{
// Constructor can take a $post_id.
function
__construct
(
$post_id
=
NULL
)
{
if
(
!
empty
(
$post_id
)
)
$this
->
getPost
(
$post_id
);
}
// Get the associated post and prepopulate some properties.
function
getPost
(
$post_id
)
{
//get post
$this
->
post
=
get_post
(
$post_id
);
//set some properties for easy access
if
(
!
empty
(
$this
->
post
)
)
{
$this
->
id
=
$this
->
post
->
ID
;
$this
->
post_id
=
$this
->
post
->
ID
;
$this
->
title
=
$this
->
post
->
post_title
;
$this
->
teacher_id
=
$this
->
post
->
post_author
;
$this
->
content
=
$this
->
post
->
post_content
;
$this
->
required
=
$this
->
post
->
_schoolpress_homework_is_required
;
$this
->
due_date
=
$this
->
post
->
due_date
;
}
// Return post id if found or false if not.
if
(
!
empty
(
$this
->
id
)
)
return
$this
->
id
;
else
return
false
;
}
}
?>
The constructor of this class can take a $post_id
as a parameter and will pass that to the getPost()
method, which attaches a $post
object to the class instance and also prepopulates a few properties for easy access. Example 5-5 shows how to instantiate an object for a specific homework assignment and print out the contents.
$assignment_id
=
1
;
$assignment
=
new
Homework
(
$assignment_id
);
echo
'<pre>'
;
print_r
(
$assignment
);
echo
'</pre>'
;
//Outputs:
/*
Homework Object
(
[post] => WP_Post Object
(
[ID] => 1
[post_author] => 1
[post_date] => 2013-03-28 14:53:56
[post_date_gmt] => 2013-03-28 14:53:56
[post_content] => This is the assignment...
[post_title] => Assignment #1
[post_excerpt] =>
[post_status] => publish
[comment_status] => open
[ping_status] => open
[post_password] =>
[post_name] => assignment-1
[to_ping] =>
[pinged] =>
[post_modified] => 2013-03-28 14:53:56
[post_modified_gmt] => 2013-03-28 14:53:56
[post_content_filtered] =>
[post_parent] => 0
[guid] => http://schoolpress.me/?p=1
[menu_order] => 0
[post_type] => homework
[post_mime_type] =>
[comment_count] => 3
[filter] => raw
[format_content] =>
)
[id] => 1
[post_id] => 1
[title] => Assignment 1
[teacher_id] => 1
[content] => This is the assignment...
[required] => 1
[due_date] => 2013-11-05
)
*/
Another option here would be to extend the WP_Post
class, but currently this is not possible, as the WP_Post
class is defined as final, meaning it is a class that can’t be extended. The WordPress core team has said they’re doing this to keep people from building plugins that rely on extending the WP_Post
object since WP_Post
is due for overhaul in future versions of WordPress. We think they’re being big fuddy duddies.3
In Chapter 6, we extend the WP_User
class (which isn’t defined as final). But the best we can do with WP_Post
is to create a wrapper class for it.
Building a wrapper class for your CPT is a good idea for a few reasons:
You can put all your code to register the CPT in one place.
You can put all your code to register related taxonomies in one place.
You can build all your CPT-related functionality as methods on the wrapper class.
Your code will read better.
Put all of your code to register the CPT and taxonomies in one place. Instead of having one block of code to register a CPT and define the taxonomies and a separate class wrapper to handle working with the CPT, you can place your CPT and taxonomy definitions into the class wrapper itself:
/*
Class wrapper for homework CPT with init function
/wp-content/plugins/schoolpress/classes/class.homework.php
*/
class
Homework
{
// Constructor can take a $post_id.
function
__construct
(
$post_id
=
NULL
)
{
if
(
!
empty
(
$post_id
))
$this
->
getPost
(
$post_id
);
}
// Get the associated post and prepopulate some properties.
function
getPost
(
$post_id
)
{
/* snipped */
}
// Register CPT and taxonomies on init.
function
init
()
{
// Register the homework CPT.
register_post_type
(
'homework'
,
array
(
'labels'
=>
array
(
'name'
=>
__
(
'Homework'
),
'singular_name'
=>
__
(
'Homework'
)
),
'public'
=>
true
,
'has_archive'
=>
true
,
)
);
// Register the subject taxonomy.
register_taxonomy
(
'subject'
,
'homework'
,
array
(
'label'
=>
__
(
'Subjects'
),
'rewrite'
=>
array
(
'slug'
=>
'subject'
),
'hierarchical'
=>
true
)
);
}
}
// Run the Homework init on init.
add_action
(
'init'
,
array
(
'Homework'
,
'init'
));
This code has been snipped (the full version can be found on this book’s GitHub site), but shows how you would add an init()
method to your class that is hooked into the init
action. The init()
method then runs all the code required to define the CPT. You could also define other hooks and filters here, with the callbacks linked to other methods in the Homework
class.
You can organize things in other ways, but we find that having all of your CPT-related code in one place helps a lot.
Build all of your CPT-related functionality as methods on the wrapper class. When we registered our homework
CPT, a page was added to the dashboard allowing us to “Edit Homework.” Teachers can create homework like any other post, with a title and body content. Teachers can publish the homework when it’s ready to be pushed out to students. All of this post-related functionality is available for free when you create a CPT.
On the other hand, much of the functionality around many CPTs, including our homework
CPT, needs to be coded up. With a wrapper class in place, this functionality can be added as methods of our Homework
class.
For example, one thing we want to do with our homework posts is to gather all the submissions for a particular assignment. Once we do this, we can render them in a list or process them in some way. Example 5-6 shows a couple of methods we can add to our Homework
class to gather related submissions and to calculate a flat-scale grading curve.
<?
php
class
Homework
{
/* Snipped constructor and other methods from earlier examples */
/*
Get related submissions.
Set $force to true to force the method to get children again.
*/
function
getSubmissions
(
$force
=
false
)
{
// Need a post ID to do this.
if
(
empty
(
$this
->
id
))
return
array
();
// Did we get them already?
if
(
!
empty
(
$this
->
submissions
)
&&
!
$force
)
return
$this
->
submissions
;
// Okay get submissions.
$this
->
submissions
=
get_children
(
array
(
'post_parent'
=>
$this
->
id
,
'post_type'
=>
'submissions'
,
'post_status'
=>
'published'
));
// Make sure submissions is an array at least.
if
(
empty
(
$this
->
submissions
))
$this
->
submissions
=
array
();
return
$this
->
submissions
;
}
/*
Calculate a grade curve
*/
function
doFlatCurve
(
$maxscore
=
100
)
{
$this
->
getSubmissions
();
// Figure out the highest score.
$highscore
=
0
;
foreach
(
$this
->
submissions
as
$submission
)
{
$highscore
=
max
(
$submission
->
score
,
$highscore
);
}
// Figure out the curve.
$curve
=
$maxscore
-
$highscore
;
// Fix lower scores.
foreach
(
$this
->
submissions
as
$submission
)
{
update_post_meta
(
$submission
->
ID
,
"score"
,
min
(
$maxscore
,
$submission
->
score
+
$curve
)
);
}
}
}
?>
In addition to organizing your code to make things easier to find, working with wrapper classes also makes your code easier to read and understand. With fully wrapped Homework
and Submission
CPTs and special user classes (covered in Chapter 6), code like the following is possible:
<?
php
// Use a method of the Student class to check if the current user is a student
if
(
Student
::
is_student
()
)
{
// Student defaults to current user.
$student
=
new
Student
();
// Let's figure out when their next assignment is due.
$assignment
=
$student
->
getNextAssignment
();
// Display info and links.
if
(
!
empty
(
$assignment
)
)
{
?>
<p>Your next assignment
<a href="
<?php
echo
get_permalink
(
$assignment
->
id
);
?>
">
<?php
echo
$assignment
->
title
;
?>
</a>
for the
<a href="
<?php
echo
get_permalink
(
$assignment
->
class_id
);
?>
">
<?php
echo
$assignment
->
class
->
title
;
?>
</a>
class is due on
<?php
echo
$assignment
->
getDueDate
();
?>
.</p>
<?php
}
}
?>
The code would be much more complicated if all the get_post()
calls and loops through arrays of child posts were out in the open. Using an object-oriented approach makes your code more accessible to other developers working it.
1 The new privacy controls were motivated by the EU GPDR, passed in 2018. The regulation applies not just to all EU residents, but also all sites with EU visitors. So pretty much all sites. The idea of allowing users to export or delete noncritical personal data is a good one no matter where you live.
2 Although, clicking the heading of a meta box, no matter where it is displayed, hides or shows the full meta box content.
3 But seriously, the core team is really smart and makes a good point. If someone extended the WP_Post
class and created new properties and methods for their new class, it would cause trouble if WordPress core later chose to use properties and methods with the same names in the base WP_Post
class.