If you have worked with WordPress, you know how useful post meta can be. If you have used custom post types, you even know how post meta extends the default post object to create really complex functionality like what WooCommerce does with the product post type.
You can add the same flexibility to your custom objects if you add metadata support to it. But, if you try and implement WordPress style metadata on your own, you’ll find out that it’s quite a task and another large piece of code to maintain.
In this guide, we’ll learn that there’s no need to go through all that trouble, no need to write a lot of code or MySQL queries. You can simply extend the default metadata functionality with a little bit of code.
The Aim of This Guide
It’ll be easy to explain stuff if we work with an example. Let’s say, you are building a simple badge feature to gamify a website. We’ll not go into a lot of details but let’s say you store a list of badges in a custom table. Each badge will have a couple of attributes but there’ll at least be an ID. We’ll assume that it is named badge_id.
At the end of this guide, your badge feature will have metadata support with the following functions:
- get_badge_meta();
- add_badge_meta();
- update_badge_meta();
- delete_badge_meta();
It doesn’t have to be about badges. You can add metadata support to any custom object. It just needs to have an id. You could replace the string badge with any other string and everything will still make sense.
In this guide, we’ll learn how to add such metadata support in a meaningful way:
- We will dig into the Metadata API gradually, starting with a quick introduction.
- While exploring the Metadata API, we’ll discover the benefits of this approach instead of writing a custom implementation.
- Finally, using our understanding, we’ll build a complete solution that you can start using right away.
A Quick Technical Introduction to Metadata API
All WordPress objects (posts, taxonomies, users and comments) come with a Metadata API implementation. Let’s see the source code for just one of those functions, get_post_meta():
get_post_meta() is just a wrapper for get_metadata(). You can go ahead and check the source code of other post_meta functions – add_post_meta(), update_post_meta() and delete_post_meta(). You’d find that they are just wrappers for the default Metadata API functions – add_metadata(), update_metadata() and delete_metadata().
All WordPress Objects Use the Same Metadata Functions
In fact, go ahead and check out get_term_meta(), get_user_meta() and get_comment_meta(). They are also wrappers of get_metadata(). If you explore the add, update and delete functions for each object, you’d find the the same underlying metadata functions.
This could mean that we could create similar wrapper functions like this:
All Meta Tables Have the Same Structure
If you compare the structure of all meta tables, wp_postmeta, wp_usermeta, wp_termmeta and wp_commentmeta, you’ll find uncanny similarities:
The names of the table follow the same pattern. The columns use the same pattern except for wp_usermeta‘s umeta_id column. This could potentially mean that we could add a table called wp_badgemeta with this structure:
meta_id | badge_id | meta_key | meta_value |
Digging Deeper into the Metadata API
Actually, you need to do a little more than writing the wrapper functions and creating the meta table, to make this work. Don’t take my word for it, let’s find out for ourselves by looking into the source code for get_metadata(). By doing so, we confirm our earlier findings and discover a couple other things:
Caching by Default
The first thing you’d notice is that get_metadata() does not query the database directly. It retrieves the meta from cache. If there’s no meta in cache, it runs another function called update_meta_cache() to (obviously) update the meta cache and then attempt to retrieve it again before giving up.
That has obvious optimisation benefits. Using the Metadata API means fewer database queries since data is cached by default.
Default Hooks
You may also notice a filter, get_{$meta_type}_metadata. This will become available to us automatically.
If you look at the source code for wp-includes/meta.php, you’ll find 7 filters (Ctrl/Cmd + F, ‘apply_filters’) and 14 actions (Ctrl/Cmd + F, ‘do_action’) that become automagically available to your custom meta.
Let’s dig deeper and explore the source code of the update_meta_cache():
This is where the actual database query takes place:
There are five PHP variables in use – $column, $table, $column, $id_list and $id_column. For our object, the $column variable becomes meta_id (Recall that usermeta table is the only black sheep here). $id_list is just a comma separated list of IDs and $id_column in our case, becomes badge_id. So, we could rewrite the query as:
Now, the table structure that we thought of is absolutely fine. It should mean that our custom meta functions should run properly. However, the name of the meta table ($table in the query) does not come from a string (‘badge’), but via a function _get_meta_table():
What this function does is check if the table is already registered as a property of the $wpdb class. If you explore the class WPDB, names of all the default meta tables are defined as properties. Since, our custom table is not registered, this function will return false and the functionality won’t work.
All we need to do is register our custom meta table with $wpdb by extending its properties early enough in the WordPress load sequence:
For the sake of completeness, we’re also adding our custom table to the default array of table names stored in $wpdb->tables.
No Need to Write Database Queries
You may have noticed that nowhere did we write MySQL queries. We’re simply piggybacking on the queries that WordPress runs by itself. Otherwise, you’d have to write all the CRUD functionality, implement some sort of caching and add hooks for extensibility.
The only MySQL query that we’ll write is for creating the meta table in the first place. Even for that, since all the meta tables have a similar structure, we can just copy and modify the schema for any default meta table from wp-admin/includes/schema.php. Let’s pick the first one itself, for term meta:
Now, we can cleverly modify it for our own purpose:
Unlike terms and other default objects, our table will get installed, along with any other tables, when the plugin gets activated. On plugin activation, our table will not be registered with $wpdb. That’s why we can’t use $wpdb->badgemeta.
Conclusion: A Complete Solution
Let’s put it all together. To implement Metadata API for a custom object in WordPress, we need to do these things:
- Create our custom meta table on plugin activation (alongside other custom tables).
- Integrate the table to the $wpdb object early in the WordPress load sequence.
- Optionally, write wrapper functions for our object.
-
Create Meta Table on Activation
You could either use this function as it is or copy over the relevant parts into your database installation function:
Make sure you use the register_activation_hook function so that the table is created on activation.
-
Integrate Meta Table with $wpdb
-
Replicate Wrapper Functions
Just copy over usermeta functions from wp-includes/user.php and search and replace term with badge (or your custom object name):
That’s it. You are ready to add metadata support to custom objects using the Metadata API. You can clone or download the final code from the gist here: https://gist.github.com/actual-saurabh/48d7fb34ad1de2e912e651890ae29690
Shiva Poudel says
Is there any way we can prefix the `badgemeta` so plugin conflict won’t arise in the long run?
Connie Taylor says
Hi, is there anyone who could help me implement this in a plugin. I have the basic plugin for the cpt but would need help with this and am broke due to medical bills so cannot hire anyone. Any help would be very much appreciated.
Ajit Bohra says
Super awesome easy to understand and implement 🙂