How does WordPress Store Taxonomy Terms in the Database?

While programming my Organize Drafts WordPress Plugin, I did a lot of research on how WordPress uses taxonomies and I spent some time digging around in the WordPress core.  Here’s what I found.

Creating your own taxonomy for use in a WordPress plugin or theme isn’t very difficult, and for a primer I would suggest checking out the WordPress documentation on taxonomies.

However, I came across some confusion on the web about whether WordPress stores the taxonomies in the database.  This confusion seems to be caused by the fact that you have to call register_taxonomy on every load of the WordPress core.  If you don’t, your taxonomy won’t be recognized.

Here’s an abbreviated example of how to call register_taxonomy in a WordPress plugin that uses a class.  As described in the codex, this function needs to be called at a specific time.  When writing WordPress plugins, one of the tricky things is to always make sure your code is being called at the correct time during the WordPress loading process.  The init action hook can be used to call code that needs to run after WordPress has finished loading but before any headers have been sent.

        final class LSW_Organize_Drafts {
        public function __construct() {
        private function hooks() {
	    add_action('init', array($this, 'setup_taxonomy'));

        public function setup_taxonomy() {
	    if(!taxonomy_exists('lswdrafttype')) {
		register_taxonomy( 'lswdrafttype', $post_types, $args );

       new LSW_Organize_Drafts();

In actuality, my code is more complex, as I made the class a Singleton, and also applied filters to the arguments, but hopefully the above code will help new WordPress plugin developers see how they can go about calling register_taxonomy inside of the init hook when using OOP design principles.

So what does register_taxonomy actually DO?

Register_taxonomy actually adds your taxonomy to the global variable $wp_taxonomies.  This happens on line 461 of taxonomy.php, as shown here:

Excerpt from the WordPress core file taxonomy.php

$wp_taxonomies is a global array of registered taxonomies. Before adding your taxonomy to the array, it checks the arguments that you supplied, and merges them with default arguments.

The $args that you supply tell WordPress what to do with your custom taxonomy. You get to specify whether it’s public, hierarchical, whether it should be shown in the menu, etc.  Here’s a screenshot that shows an example of what the $args might contain:

Screenshot taken while debugging WordPress in PHPStorm — this shows what the $args contain after some processing — for what you can actually supply, view the codex 

So, it’s true that the settings for your taxonomy are NOT saved in the database.  They are generated from a PHP array that’s coded in a plugin file or a theme file.

OK, so what if I don’t register my taxonomy?

If you don’t register the taxonomy, then it won’t be visible on any WordPress pages and you won’t be able to use it. However, your taxonomy terms and relationships that you may have previously created are still in the database.

What are Taxonomy Terms?

The terms are the actual category names or tag names, for example.  Or in my case, the terms are the draft types that I’ve created.

If you’re having trouble understanding taxonomy terms and what the difference is between the taxonomy name and the term names, then here are some real-world examples:

Taxonomy: Birds

Terms: Ducks, Geese, Eagles, Chickens, Sparrows

Taxonomy: Property Zoning Types

Terms: Commercial, Residential

Taxonomy: Medication Types

Terms: Over-the-counter, Prescription, Controlled

Why should I use a custom Taxonomy instead of just categories?

Using categories for everything will get messy fast.   Let’s say that you have a real estate website with a page for a house.  You probably want to organize that house by location, sale price, number of bedrooms, etc.  You could just simply add it to multiple categories, so the house could be in the cheap category, the Seattle category, the 4 bedroom category, etc.

However, custom taxonomies make life easier!  You can query each taxonomy separately and also display objects with the same taxonomy easily.  (NOTE: In WordPress, a post is an object!  A page is an object!  A custom post type is an object!  Knowing this becomes important when you look at the database tables.) If you have the taxonomies of sale_price, number_of_bedrooms, and location, you can then easily get all houses that are in the same location or the same price range.

For example, if we have custom taxonomies of sale_price and location, we could use this code to get all cheap houses in Seattle:

$args = array(
	 'posts_per_page' => 8,
	 'orderby' => 'rand',
	 'post_type' => 'houses',
	 'sale_price' => 'cheap',
	 'location' => 'seattle'
$cheap_houses_in_seattle = get_posts( $args );

Where does WordPress store Taxonomy Terms?

The actual terms are in the terms table.  All WordPress tables have a prefix.  In my example, the prefix is linsoft_, but the default prefix is wp_ so the actual name of your table might be wp_terms.

The taxonomy names are stored in the term_taxonomy table.

If you are familiar with database design, you would probably think that there would be a ONE TO MANY relationship between the taxonomy name and the terms, but in fact these tables have a ONE TO ONE relationship.  The WordPress database is not normalized!  There is duplicate data…  See this screenshot:

Screenshot from MySQL Workbench of the WordPress Term Taxonomy Table

As you can see, the taxonomy name is repeated many times. The term_id actually references the term_id that’s in the terms table.

Here’s what’s the terms table looks like:

Screenshot from MySQL Workbench of the WordPress terms table

How does get_terms work?

If you want to get the terms associated with the taxonomy, you use the function get_terms.  To display all of them, including the empty ones, you would do this:

get_terms( 'lswdrafttype', 'hide_empty=0');

Where ‘lswdrafttype’ is the name of your taxonomy.

The get_terms function begins at line 1070 of taxonomy.php  It’s a long function and most of it deals with forming the MySQL query.

The query is formed on line 1437:

$query = "SELECT $distinct $fields FROM $wpdb->terms AS t $join WHERE $where $orderby $order $limits";


As you can see, it’s going to join tables together.

The final query will look different depending on the paramters you gave to get_terms. In my case, the final query looks like this:

SELECT t.*, tt.* FROM wp_terms
AS t INNER JOIN wp_term_taxonomy
AS tt ON
t.term_id = tt.term_id
WHERE tt.taxonomy IN ('lswdrafttype') ORDER BY ASC

What does the WordPress function taxonomy_exists do?

Taxonomy_exists does NOT check the database!  All it does is check the global array $wp_taxonomies.  If you have not called register_taxonomy, then your taxonomy will not be in the $wp_taxonomies variable, and the result will be false, even if you have terms associated with that taxonomy stored in the database!

So, do not fear, your taxonomies might still be in the database, even if taxonomy_exists returns false!

OK, so how does WordPress know which terms are associated with which posts?

Good question!  There’s another table for that, called term_relationships (remember, with your prefix before the table name, so commonly it might be called wp_term_relationships).

Screenshot from MySQL Workbench of the term_relationships WordPress table

Does this table look confusing?  Boring?  Remember how I told you that posts are objects in WordPress?  Well, this is why I mentioned it.  The object_id may be a post_id or an id of another object.  The term_taxonomy_id is the term_id found in the terms table.

You can get all of the taxonomy terms with the WordPress function wp_get_post_terms.  This function ends up called wp_get_object_terms, which creates a query that uses an inner join between the terms table and the term_taxonomy table.  See line 2401 of taxonomy.php.

In its simplest use, wp_get_post_terms can be supplied a $post_id and it will return the tags associated with it.  For example, calling this:


In my database set-up, this function ends up running this MySQL query:

SELECT t.*, tt.* FROM linsoft_terms AS t INNER JOIN linsoft_term_taxonomy AS tt ON tt.term_id = t.term_id INNER JOIN linsoft_term_relationships AS tr ON tr.term_taxonomy_id = tt.term_taxonomy_id  WHERE tt.taxonomy IN ('post_tag') AND tr.object_id IN (636) ORDER BY ASC

If you want to get your custom taxonomy terms instead of the tags, then you will want to pass wp_get_post_terms a second parameter, which is an array of taxonomy names.

For example:

wp_get_post_terms($post_id, 'my_custom_taxonomy');

I hope that this helps!   If you have any questions or clarifications, please do not hesitate to post a comment. Thank you!

WordPress Plugin Tutorial: Create a Widget that uses Post Meta Data

There are a lot of beginner tutorials for developing WordPress plugins, but most focus on creating one simple feature, for example a shortcode, and this may leave programmers new to WordPress feeling lost about how to do something a little more complicated.

What you Will Learn

  • How to create a Meta Box, which is a custom user interface that displays on the post edit page
  • How to save the values entered in the Meta Box into the wp_postmeta database table
  • How to retrieve these values and display them in a dynamic widget
  • How to create a WordPress widget by extending the WP_Widget class
  • How to use the Singleton design pattern (we will use Object Oriented Programming (OOP) principles)
  • Best practices for sanitizing inputs to prevent security concerns


  • PHP 5.3+
  • WordPress 4.4+ (This may work with earlier versions but has not been tested)
  • A general understanding of PHP and HTML

What we Will Make

In this tutorial, we are going to build a WordPress plugin that displays an Amazon link in a widget.  This Amazon link will be different for every post, as it will use the post’s custom fields to determine which link to display.

Here is what our finished product will look like:

Amazon Widget - Link with Affiliate Tag
We will create this widget, which has an input field for entering the affiliate tag that you want to use for your product link. This widget can go anywhere your theme allows widgets – typically this means the sidebar and (maybe) the footer.


WordPress Meta Box Example
We will create this meta box, which has input fields for specifying the Amazon title and the asin. The widget will use these fields to determine which product link to display.
This Plugin is simple – it displays an Amazon link. HOW it looks will depend on your theme and any custom CSS you write. This might not seem too exciting, but it allows you to specify a different product for every single post/page!  If you click on “Amazon Fire Tablet” it will send you to Amazon, using your affiliate link!

A Tutorial for Beginner WordPress Plugin Developers

This tutorial is aimed at people just beginning WordPress plugin development, to show you how to create a widget that displays dynamic content.  It’s easy to display the same content on every page — just add a text widget to your sidebar, footer, or any other widgetized area.  However, displaying custom content that varies based on which page/post you are viewing, requires a custom solution.

Note that the plugin we are going to build doesn’t interact with the Amazon Product Advertising API, which means that it doesn’t grab any database information from Amazon.  This means that it can’t display the current price or the product image.  However, you could extend this plugin to do that if you so desire.  You’ll want to use the WordPress HTTP API to retrieve the data and then you will need to parse the XML.  I have written several plugins that work with the Amazon database, and the whole process is a bit challenging, but definitely doable!

Everyone Learns Differently

This tutorial is written in a step-by-step way, where I only show small parts of the code at a time.  This is so that I can explain what they’re doing and provide relevant tips.  You can start with a blank text editor and follow along.

However, if you learn best by looking at the whole code first, you can get the completed code on Github.

Setting up our Plugin Folder and File

This is going to be a simple plugin with only one file.  We could put this file directly in the \wp-content\plugins directory, but a better idea is to create a directory for it. That allows us to easily extend the plugin in the future, by adding additional files, like CSS stylesheets or Javascript scripts or additional PHP classes.

  1. Create a directory titled lsw-amazon-in-widget
  2. In that directory, create the file lsw-amazon-widget.php

Note: Throughout this tutorial, we will preface many things with “lsw”.  When writing WordPress plugins, it’s a good idea to preface class names, function names, and setting names to decrease the chance of your plugin conflicting with another plugin.

Creating the Plugin File Header

All plugins have to have a file header at the top of their main PHP file. The file header is used by the WordPress File Header API to extract information about your plugin, like the plugin’s name.  Without this file header, your plugin won’t show up in the list of plugins in WordPress admin.   The file header is written inside of PHP comments.

Place this at the top of lsw-amazon-widget.php:

 * Plugin Name: Amazon Widget
 * Plugin URI:
 * Description: Display Amazon Product in Widget
 * Author: Linnea Wilhelm
 * Author URI:
 * Version: 1.0.0
 * License: GPL2
 * License URI:
 * Text Domain: lsw-amazon-widget
 * Tags: amazon, widget, amazon affiliate

 Creating the Outline of the Plugin

Our plugin will have two classes, which will both be in the same PHP file.  The first class, Lsw_Amazon, will deal with initialization of our plugin and it will contain the functions having to do with the meta box.  The second class, Lsw_Amazon_Widget, will extend WP_Widget and it will handle the front and back end display of the widget.

class Lsw_Amazon  {

	protected function __construct() {
		// constructor

	public static function add_custom_meta_box( $post_type, $post ) {
		//this function will be called by the add_meta_boxes hook

	public function init() {
		//this function will have all of the hooks for setting up our plugin

	public static function render_meta_box() {
		// this function will contain the html for displaying the meta box

	public static function save_meta_details( $post_id ) {
		// this function will be called by the save_post hook
		// it will save the values entered in the meta box

class Lsw_Amazon_Widget extends WP_Widget {

	function __construct() {
                // constructor
	public function widget( $args, $instance ) {
		//Front-end display of widget.

	public function form( $instance ) {
		//Back-end display of widget form.

	public function update( $new_instance, $old_instance ) {
		//Process widget options on save

	private function getAmazonUrl($asin, $affiliate_tag) {
		//a helper function


Looking over this outline, one thing you may notice is a lot of static methods (Also called “functions”).  When using WordPress hooks, the method that is called by the hook must be both public and static.


Implementing the Singleton Design Pattern

We are going to use the singleton design pattern for our Lsw_Amazon class because we only want it to be instantiated once.  If we allowed our class to be instantiated multiple times, not only could this use up more server memory, but it could cause unexpected behavior, like duplicate meta boxes.

Alter the beginning of your Lsw_Amazon class so that it looks like this:

class Lsw_Amazon  {

	public $version = '1.0.0';
	public static $text_domain = 'lsw-amazon-widget';

	protected static $_instance = null;

	protected function __construct() {

	public static function instance() {
		if ( is_null( self::$_instance ) ) {
			self::$_instance = new self();
		return self::$_instance;



Since we are using the Singleton pattern, when we want an instance of the Lsw_Amazon class, we will call Lsw_Amazon::instance() instead of calling the constructor directly with the word new. The instance method checks to see if we already have an instance, and if we do, it returns that instance.  If not, then it creates the instance with the new keyword and the constructor runs.   This means that the constructor runs only once each time WordPress runs. The constructor has the keyword protected in front of it to prevent it from being called with the new keyword.

We also added a couple of class properties, the $version and $text_domain, for our later use.  It is a good idea to save the version as a variable so that you can check it programmatically.  Sometimes, you may use this version number to run specific code when upgrading your plugin.

Let’s add the Action Hooks

For organization, all hooks and filters should go in the same place, if possible.  We’re going to put them in our init() function, which is called by the constructor.

	public function init() {
		add_action( 'add_meta_boxes', array('Lsw_Amazon', 'add_custom_meta_box'), 10, 2 );
		add_action( 'save_post',  array('Lsw_Amazon', 'save_meta_details') );
		add_action( 'widgets_init', function(){ register_widget( 'Lsw_Amazon_Widget' );});

As you can see with the above code, it’s possible to call functions that are inside of classes.  Instead of passing the function name as the second parameter to add_action, you pass an array that contains the class name and function name.

For the widgets_init hook, we are using an anonymous function.  Anonymous functions first became available in PHP with version 5.3.  If you are using an earlier version, you will have to rewrite that line to use a named function, as we are using in the first two lines.

Let’s Add the Meta Box and write the HTML to Display It

	public static function add_custom_meta_box( $post_type, $post ) {
		$post_types = apply_filters('lsw_amazon_post_types', array('post', 'page'));
		add_meta_box( 'lsw_amazon_settings_box',
                    __( 'Amazon Product Settings', self::$text_domain ), 
		    $post_types, 'normal', 'default' );

	public static function render_meta_box() {
		global $post;
		$lsw_amazon = get_post_meta($post->ID, 'lsw_amazon', true);
		$title = isset($lsw_amazon['title']) ? $lsw_amazon['title'] : '';
		$asin = isset($lsw_amazon['asin']) ? $lsw_amazon['asin'] : '';
		<label for="lsw_title">Title   </label><input class="widefat" type="text" name="lsw_title"
		     id="lsw_title" value="<?php echo esc_attr($title); ?>"> <br>
		<label for="lsw_asin">ASIN  </label><input class="widefat" type="text" name="lsw_asin"
		     id="lsw_asin" value="<?php echo esc_attr($asin); ?>">

Tip: Get your case right! $post->id will NOT work.  The correct field name is all capitalized, so it’s $post->ID.

Let’s take a moment to look at this line:

$post_types = apply_filters('lsw_amazon_post_types', array('post', 'page'));

That line is saying take the array and perform all of the filters that are registered with the add_filter hook.

In our case, we are filtering an array of post types.  The default action of our plugin is to only add the meta box to posts and pages.  However, if someone wanted to modify this and have the meta box appear on a custom post type, they could add this code snippet to their theme’s functions.php file.  (Or, they could add it to another plugin, or to a php file in the mu-plugin directory. See here: Must Use Plugins)

add_filter('lsw_amazon_post_types', 'associate_post_types_with_amazon_box');
function associate_post_types_with_amazon_box($post_types) {
   $post_types[] = 'YOUR_CUSTOM_POST_TYPE';
 return $post_types;

A few other notes about the meta box code:

  • We are using functions for internationalization. The function _() translates the text before displaying.
  • We escaped attribute values with esc_attr.  Wordpress has a variety of functions for sanitizing and validating text. (The WordPress Functions Reference is invaluable!)  It’s important to treat all user entered data and all data from the database as potentially dangerous.  User input data includes $_POST and $_SERVER and $_GET data, since those can all be changed by the user.  If you don’t sanitize or validate the data before displaying it on a webpage, your plugin is vulnerable to cross-site scripting attacks.
  • We’re using the PHP ternary operator along with the isset() function to check if our values are set, and to set them to an empty string if they are not set.  If you try to use a variable that has not been set, you will get a Notice: Undefined Variable error. This won’t stop execution of your plugin, but it may cause a very ugly error to appear.  The practice of using a ternary operator with the isset function is so common throughout PHP programming, that the latest version of PHP, PHP 7, has a new null coalescing operator which is much simpler!

Let’s Save the Values Entered in the Meta Box

	public static function save_meta_details( $post_id ) {
		if( isset($_POST['lsw_asin']) && isset($_POST['lsw_title'] )) {
			$lsw_amazon = array('asin'=>sanitize_text_field($_POST['lsw_asin']),
			update_post_meta( $post_id, 'lsw_amazon', $lsw_amazon);

A note about this code:

  • Once again, we sanitize or escape user input before using it. Remember, values from the database can contain dangerous characters and should be treated just like user input. If you’re familiar with PHP but not WordPress, you probably have used the filter_var() function. Well, sanitize_text_field is a WordPress helper function that you can use in its place when you are sanitizing a string.


Now, Let’s Create the Widget

In WordPress, widgets are created by extending the class WP_Widget. This means that there is a set of methods that they must have. Specifically, the WordPress core says that “WP_Widget::widget(), WP_Widget::update() and WP_Widget::form() need to be overridden.” This means that you implement these methods in your child class so that the child methods run instead of the parent methods. If you have followed other tutorials for creating WordPress widgets, or have had a peak at the documentation, then you won’t find any surprises here.

The Widget Constructor

	 * Register widget with WordPress.
	function __construct() {
			'lsw_amazon_widget', // Base ID
			'Amazon in Widget', // Name
			array( 'description' => 'Display an Amazon Product' )


Things to note about this code:

  • This calls the parent’s constructor method (in this case, WP_Widget::__construct())
  • The ID should be unique, and the other parameters are displayed in the Appearance-->Widgets admin area.

The Front-End Display of the Widget


	public function widget( $args, $instance ) {
		if(!is_single()) {
		global $post;
		$lsw_amazon = get_post_meta($post->ID, 'lsw_amazon', true);
		if(isset($lsw_amazon['title']) && isset($lsw_amazon['asin'])) {
			$tag = isset( $instance['tag'] ) ? $instance['tag'] : '';
			echo $args['before_widget'];
                        <h2>You Might Be Interested In...</h2>
			<a href="<?php echo $this->getAmazonUrl( $lsw_amazon['asin'], $tag ); ?>">
				<?php echo sanitize_text_field( $lsw_amazon['title'] ); ?>
			echo $args['after_widget'];

What this code is doing:

This widget method is called when WordPress tries to display your Widget in the dynamic sidebar or whatever widget area you have in your theme, on the front-end. It uses is_single() to check if this is a single page or single post. It doesn’t make sense to display this widget if there is no single post or page being displayed because this widget wouldn’t know what Amazon product to display. You could alter this to have it display a default, but that’s beyond the scope of this tutorial.

So, if is_single() is false, the function completes without doing anything.

The global $post variable holds the current WP_Post object, which has an ID field. We use this ID field as the parameter for get_post_meta, which does exactly what it sounds: it gets the post meta data from the database. In this case, we provide the name of our data as the second parameter and the third parameter, true, indicates that we only want this single value. This value is actually an array that contains the title and the ASIN.

The widget echoes the before_widget html (this is specified by the theme usually), and then echoes a link to the product, based on the ASIN and the affiliate tag. And, finally, the after_widget html is displayed.

The Back-End Display of the Widget

	public function form( $instance ) {
		// outputs the options form on admin
		$tag  = isset( $instance['tag'] ) ? $instance['tag'] : '';
		<label for="tag">Affiliate Tag:</label><input type="text"
		         name=" <?php echo $this->get_field_name( 'tag' ); ?>"
		id="<?php echo $this->get_field_id( 'tag' ); ?>" value="<?php echo esc_attr($tag); ?>"> <br>

The back-end display of the widget is a very simple form with one input field for an Affiliate Tag.

Saving the Widget Values

	public function update( $new_instance, $old_instance ) {
		// processes widget options to be saved
		$instance             = $old_instance;
		$instance['tag']     = sanitize_text_field( $new_instance['tag'] );
		return $instance;

If you don’t explicitly save the form value(s) in the update() method, then the form data won’t be saved. So, make sure you “wire it up” in this way. Once again, you see that we sanitized the input data for security reasons.

The Helper Function

	private function getAmazonUrl($asin, $affiliate_tag) {
		return "" . $asin . "?tag=" . $affiliate_tag;

Whenever possible, you should use helper functions to do small jobs. This follows the DRY programming principle: Don’t Repeat Yourself. This allows for reusable code and makes test driven development (TDD) possible. Perhaps you later decide to extend this plugin to display links in other ways. You can change how the url is generated by simply changing one function. Or maybe you want to use this plugin on your Amazon UK site and need to change it to Better yet would be to provide an option to specify the country (Amazon has numerous international sites).

The Finishing Touches


At the bottom of your plugin file, instantiate your class. If you don’t instantiate it, your plugin won’t run! Remember that we used that Singleton pattern so we don’t use the new keyword.

Ways this could be improved

This plugin is fully functioning now, but we could make it prettier and give it more options!

  • This plugin needs some CSS. Learn how to enqueue a CSS stylesheet.
  • Document your code! I omitted the PHPDoc to make the snippets smaller.

Get the complete code on Github

Here’s the finished product that we developed in this tutorial.

Troubleshooting Tips

If you run into an issue developing your WordPress plugin, there are lots of tools for troubleshooting.  One place to start is the WordPress Developer Plugin.  It offers an easy interface for installing a bunch of helpful plugins, including the debug bar and the debug bar console.   Here’s a screenshot I took while I was debugging this plugin with the Debug Bar Console:

screenshot of debug bar console, wordpress plugin for developers


More WordPress Plugin Development Guides

WordPress Edit Post Screen Hooks: A Visual Guide

There are lots of reasons that a WordPress developer might want to customize the post edit screen when developing their plugin or theme.  However, using a custom post type is probably the most common reason to customize it.  The developer might want to add additional headers, instructions, styles, javascript, or add/remove input fields.

Note that the easiest way to add custom input fields to the WordPress edit screen is to add a metabox.  However, if you need more ways to customize it, keep reading!  I will even provide some code examples.

Wordpress Admin Edit Post Screen Hooks
WordPress Admin Edit Post Screen Hooks

Reference List of WordPress Hooks that You can use to Modify the Post Edit Screen

1. in_admin_header

in_admin_header is the hook you want to use if you need to alter the heading of any of the admin screens.  It fires at the very top of the page for all admin pages, including the comments page, media page, plugins page, tools page, appearance page, etc.  It even fires on custom settings pages and custom post type pages, like the WooCommerce admin screens for products and orders.  Like all hooks, this hook is called by the WordPress function do_action.  Wordpress core uses this hook to render the WordPress admin bar.  I can see using this hook if you want to add a banner along the top of all admin screens.

2. admin_notices

admin_notices is a great hook to use for posting messages at the top of admin screens.  Wordpress core makes adding notices extremely easy!  Simple use this hook to output a div.  Wrap the div in one or more of the following classes to make your notice stand out

For examples, see the Codex page on admin_notices.

.error = This class will make your notice have a white background and red left border

.update_nag = This class will make your notice have a yellow border and move it higher on the page.

.notice and .is-dismissible = This class will make your notice dismissible!   A great option if you don’t want to annoy your users.  Wordpress core handles adding a close button and removing the notice for you.   (See this article on how to make your notices dismissible.)

3. all_admin_notices

There is not much documentation on this hook.  I looked at the code and it looks like this should fire in some cases when the admin_notices hook doesn’t fire (for example on the network admin screen for WordPress Multisite).  Take a look at WordPress Core for more details (\wp-admin\admin-header.php – around line 228)

4. edit_form_top

This is a hook for adding to the edit screen after the “Add New Post” title.  Note that if you want to change the words “Add New Post” for a custom post type, you should do that when registering a new post type. See the arguments that you can add.  The array of labels has lots of options.   That’s right, you set up those words upon registering the custom post type.  There is no need to try to filter it later.

5. edit_form_after_title

This hook is perfect for adding html to the post edit screen after the heading and above the post content box.  By the way, the rich text editor you use in WordPress is called TinyMCE, which is an open source HTML WYSIWYG editor.

6. edit_form_after_editor

This fires after the post content editor but before the excerpt, the slug, the author, and other optional/add-on meta boxes.

7. edit_form_advanced

This fires near the very bottom of the edit post screen.  It will display your custom HTML after all meta boxes.

Note: This does NOT fire on the edit page screen.  It will fire on all other post types.

8. post_submitbox_misc_actions

Want to add a checkbox, dropdown input field, or other input field or text to the publish post box?  This is the hook to use!  See the Codex for an example.

9. media_buttons

Add a button next to the add media button, for uploading files that your custom plugin or theme deals with in a specialized way.  Tutorial for using the media_buttons action hook.

How to Only Make Changes to the Edit Screen for a Custom Post Type?

In the function that you hook, you can check which screen is being displayed, and then only make the custom changes if the screen ID and screen post type are a match.  For example:

add_action('in_admin_header', 'in_admin_header_lw');
function in_admin_header_lw() {
	$screen = get_current_screen();
	if($screen->post_type=='post' && $screen->id=='post') {
		echo "This is the edit post screen.";

You actually only need to check the id and not the post_type if you only want to make the changes on one page.  But, if you want the change to appear on all pages with the same post_type, then checking the post_type property of the WP_Screen object is the way to go.  The function get_current_screen returns a WP_Screen object.  Each screen has a unique ID.

Everyone has their own workflow, but my personal preference in figuring out how PHP code works is to use a debugger and put a breakpoint at the location where I want to check the contents of variables.  When I did this in PHPStorm, while on the WooCommerce add product screen, I was able to see the contents of the WP_Screen object, therefore making it really obvious what  I should be checking for in my code.

PhpStorm Debugging WordPress WP_Screen Object
PhpStorm Debugging WordPress WP_Screen Object

How to Add Styles and Scripts to the Post Edit Screen?

Need to add javascript or CSS styles to only one admin screen?  Here’s the way to add it (this code assumes that admin-style.css is the name of your CSS file and it’s in the same directory as the plugin file).

add_action( 'admin_enqueue_scripts', 'lw_load_custom_wp_admin_style' );
function lw_load_custom_wp_admin_style() {
	$screen = get_current_screen();
	if( $screen->id=='post') {
		wp_register_style( 'lsw_custom_wp_admin_css', 
                        plugins_url( '/admin-style.css', __FILE__ ),
			false, '1.0.0' );
		wp_enqueue_style( 'lsw_custom_wp_admin_css' );

Questions, Improvements?

Are you puzzled about something?  Or did I make an error?  If so, please take a moment to comment.   Thank you!

Using the restrict_manage_users Action Hook in WordPress 4.4

I recently participated in an interesting discussion on Stack Exchange about how to add a filter button just above the Users list table on the Users page in WordPress Admin. To sum up the whole issue, if you used the restrict_manage_users hook to do this in an earlier version of WordPress, your code for filtering the users list will probably not work correctly in WordPress 4.4 without making a small change.

Examining The Issue

In WordPress 4.4, the restrict_manage_users hook is called twice when loading the Users page.   (It is only called once in WordPress 4.3.1)  This means that if you use that hook to add a drop down select and filter button (or any other input field or html code), your additions will actually show up on the Users page twice, both above the table and below it.

The restrict_manage_users action hook is called twice in WordPress 4.4
The restrict_manage_users action hook is called twice in WordPress 4.4

Now, normally you would think that this addition is a great convenience. However, unless you change your code, when the user makes a selection in one of the dropdowns, the other dropdown remains the same. Then when the Filter button is clicked, the form is submitted with two values for that input. One value will contain the selection and the other will be blank. If you use Chrome Developer tools, you can inspect the query vars sent with the get request and you will see what I mean:

Analyzing the Query Vars sent with the GET request
Analyzing the Query Vars sent with the GET request


Tip:  Follow these instructions to inspect the query string parameters:

  • Open the page in Google Chrome Web Browser and press CTRL+SHIFT+I to open Developer Tools
  • At the top of the tools, select the Network tab.
  • With the developer tools still open, click on the filter button or submit button or whatever other button that you want to examine the request for.  You should see a whole bunch of requests appear.
  • Click on the request you are interested in.  In this case, it’s going to start with users.php.
  • Click on the Headers tab and scroll down to “Query String Parameters.”

How this Effects Your Code

If you wrote a WordPress plugin that uses both the restrict_manage_users hook and the pre_get_users filter, you likely looked at the $_GET global variable to alter the WP_User_Query.  Because of this change, the $_GET variable will be different, and your code might only work if the user uses the bottom dropdown menu and not the top one.  This is a very big bug, but fortunately there are several different ways to fix this issue.

The Solution

There are at least 3 different solutions to handling this problem.  Visit this WordPress Stack Exchange discussion for example code on how to fix this using PHP.

Here I’m going to present a solution that uses Javascript.  Simply add this to your theme’s functions.php file and change the NAME_OF_YOUR_INPUT_FIELD to the name of your input field!  Since WordPress automatically loads jQuery on the admin side, you do not have to enqueue any scripts.  This snippet of code simply adds a change listener to the dropdown inputs and then automatically updates the other dropdown to match the same value.

add_action( 'in_admin_footer', function() {
	<script type="text/javascript">
		var el = jQuery("[name='NAME_OF_YOUR_INPUT_FIELD']");
		el.change(function() {
} );


If you tried this solution and are still having an issue, please comment below.  There may be use cases where this doesn’t work but I can’t think of any right now.  I’m interested in learning and your feedback is important.

Even if you use my javascript solution, I suggest that you read over the other solutions on Stack Exchange because they provide a lot of insight into how to use a static variable and/or your own custom action hook to deal with the issue of WordPress core calling a hook multiple times.   Using a static variable is a great way to manage how often your plugin performs certain actions, and doing so could help you future-proof your code.


WordPress Plugin or Theme Development with Ajax and jQuery

A little knowledge of Javascript, or the popular Javascript library jQuery, can go a long way in making your plugin or theme “do” things.  As you may know, WordPress is written in PHP.  However, for client-side code, Javascript is the typical solution.  How much you do in Javascript and how much you do in PHP depends on your particular task, but at the very least you will likely use Javascript to send POST requests and parse the data received from the XMLHttpRequest object.

WordPress has an action hook called wp_ajax that allows you to access the WordPress core functions when handling HTTP post requests.

The jQuery functions and jQuery.ajax can be used to send the requests. The requests need to be sent to the ajaxurl, which is a javascript variable already defined on the admin side. On the front end, you need to define this variable yourself to point to the location of admin-ajax.php.

For a complete tutorial on how to use jQuery & Ajax with WordPress, see my article: How to use Ajax with your WordPress Plugin or Theme?

Another option for using Javascript in your theme or plugin is to use WordPress’s Customizer API, which is something I plan to address in a future tutorial.

How to make your plugin have universal settings on Multisite WordPress?

While updating my Check Amazon Links plugin to work with WordPress Multisite, I struggled with how I could make my plugin settings universal across all network installs.  More specifically, I wanted it to be an option that could be turned on and off.  I tried to find a way to implement a hook or filter for when the plugin options were being updated via the settings page, but I had so much trouble with that, that I finally decided to use some Javascript.  Yay for javascript!  Sometimes it’s the easier option.

First, I added the option to my settings page, but only on multisite installs.  Here’s what it looks like:

Multisite Option on my WordPress Plugin (Check Amazon Links)

Next, I wrote the Javascript (jQuery) code to respond to submit events:

jQuery(document).ready(function () {
    var option_page_name = jQuery('input[name=option_page]').val();
    if (typeof(option_page_name) != "undefined") {
        if(option_page_name === 'amazon_link_plugin_options') {
            jQuery('form').submit(function(e) {
                var update_all = jQuery('select[name="azlc_multisite_same[toggle]"]').val();
                if(update_all === '1') {
          , 'action=azlc_ms', function(){});



As you see, my code checks the page name to insure that it only sends the AJAX request if it’s MY plugin’s page.   I add that javascript code to a javascript file I already enqueue.

Here’s the PHP WordPress Backend code for handling the Ajax request:

if(is_multisite()) {
	add_action('wp_ajax_azlc_ms', 'azlc_ms');

function azlc_ms() {
	add_site_option( 'azlc_update_from', get_current_blog_id() );


And here’s the PHP WordPress code the updates the options:

if(is_multisite()) {
	add_action('plugins_loaded', 'azlc_multisite');

function azlc_multisite() {
	global $wpdb;

	// check for flag 
        // I call this a flag because its existence means that this code needs to run.
        // You could write this to make it more human readable
	$blog_id = get_site_option('azlc_update_from');
	if($blog_id) {
		// switch to the blog to copy the actions from
		$options = get_option('azlc_plugin_options');
		// copy settings to all sites
		AmazonLinkCheckerCore::copy_settings_from($blog_id, 'azlc_plugin_options', $options );
		// remove flag
		// switch back


And here’s the code that actually does the copying:

	public static function copy_settings_from($source_id, $option_name, $option_value) {
				global $wpdb;
				$sql = "SELECT blog_id FROM $wpdb->blogs";
				$blog_ids = $wpdb->get_col($sql);
				foreach($blog_ids as $blog_id) {
					// update option on other blogs
					if($blog_id!=$source_id) {
						update_option($option_name, $option_value);


Related Tutorial

Don’t miss this related article: How to Enable WordPress Multisite for your Plugin

Your feedback is important

I write this blog with sincere hope that I can help other WordPress developers.  Please let me know if this helped you, or if I made a bad error.  Thanks!

How to make your plugin compatible with WordPress Multisite?

How to Enable WordPress Multisite for your Plugin

So you have written a WordPress plugin, and need to enable it to work with WordPress Multisite.  Multisite allows the Network admins to install plugins on all their network blogs at once.

When it comes to plugin development, adding this feature is really not too difficult, but there are not many tutorials available to follow.  And while working on this feature, I came across one prominent tutorial with code that just didn’t  work.  So here’s the code that actually works!  This is written for version 4.3 and should work for WordPress versions as early as 3.0.0.

The code shown below is an excerpt from my Check Amazon Links WordPress Plugin.  Of course, you will have to change the function and class names to the names used in your plugin.

Code that actually works!

First, the Hook.  My hook looks like this because my activate function is inside of the class AmazonLinkCheckerCore:

register_activation_hook( __FILE__, array( 'AmazonLinkCheckerCore', 'activate' ) );

This hook will run on both single sites and multisite WordPress installations.  There is no need to specify a different activation hook for multisite, if you write the function as follows.

The function that’s called:

     public static function activate($network_wide) {
		if(is_multisite() && $network_wide) { 
                // running on multi site with network install
		        global $wpdb;
			$activated = array();
			$sql = "SELECT blog_id FROM $wpdb->blogs";
			$blog_ids = $wpdb->get_col($sql);
			foreach($blog_ids as $blog_id) {
				$activated[] = $blog_id;
			update_site_option('azlc_multisite_activated', $activated);

		} else { // running on a single blog

		// this sets a transient and should only be done once 	
                // put any code that should only happen once, network-wide, here:


Note that my activation code uses a transient to display the settings page after activation. Setting this transient on each blog caused a big bug, so I moved that code outside of the foreach loop.  If you don’t use a transient, just delete that line, but I left it here for demonstration purposes.  If you do use a transient, then change the code to point to the function that you wrote that generates the transient.

The function that’s called by the above function:

      public static function implement_activation() {
            // put your activation code here
            // for example, install databases
            // initialize options
            // whatever your plugin already does on activation, move here


You will also need to change your deactivation code so that it works in a similar manner to the above activation code.  Depending on your plugin, you may need to loop through all of the blogs and perform your deactivate code on each one.

Add this to your deactivation code:


Also, add code to check if any new blogs were installed on the network

This code will check if there are any new blogs installed and it will activate the plugin on those too.   This ensures that your plugin always runs on every single blog on the network.

	add_action('admin_init', array('AmazonLinkCheckerCore', 'multisite_check_activated'));

	public static function multisite_check_activated() {
		global $wpdb;
		$activated = get_site_option('azlc_multisite_activated');
		if($activated == 'false') {
			return false;
		} else {
			$sql = "SELECT blog_id FROM $wpdb->blogs";
			$blog_ids = $wpdb->get_col($sql);
			foreach($blog_ids as $blog_id) {
				if(!in_array($blog_id, $activated)) {
					$activated[] = $blog_id;
			update_site_option('azlc_multisite_activated', $activated);



Other Considerations

  • If you have a settings page, it will appear on each blog.  If you want to make the settings universal across all network blogs, you will have to implement a way to copy the settings from one blog to all.  Read this tutorial: How to make your plugin have universal settings on Multisite WordPress.
  • You may find settings that should always be the same on all blogs.  In that case, you want the setting to be a SITE OPTION instead of a regular option.  Instead of “update_option” use “update_site_option.”

Your feedback is important

I write this blog with sincere hope that I can help other WordPress developers.  Please let me know if this helped you, or if I made a bad error.  Thanks!