Proposal: Status API for taxonomy terms

Below is a proposal for a Term Status API. Feedback and comments are welcome below.

Summary

Of WordPress’s four main content types – posts, comments, users, and terms – only terms does not have the concept of “status”. The wp_posts, wp_comments, and wp_users tables all have status columns. The semantics and implementation details differ across posts, comments, and users. But in each case, the idea of “status” has allowed for new and improved user experience: autosave for posts, pending user registrations, spammed comments, and so on. It’s time for terms to have their own ‘status’ too.

Use cases

The immediate use case for term status comes from the customizer. Recent developments have made it possible to draft nav menus and other content in the customizer before publishing to your site. The post_status API makes this possible in the case of most nav menu items: items are set to ‘auto-draft’ until the changes have been saved, which ensures that they’re never visible in the Dashboard. This corresponds nicely to the driving philosophy of the Customizer project, which is that it should be possible to preview changes to your site, with the confidence that the changes will be discarded if you choose not to save them.

Taxonomy terms, on the other hand, cannot yet have corresponding nav items created in the customizer; see #37915. Without resorting to odd techniques (such as a “shadow taxonomy” that is hidden from normal view), it’s not possible to create a taxonomy term that is not immediately available in all relevant interfaces: metaboxes, term queries, etc. Allowing the creation of auto-draft terms will create parity between term-related nav items and other types of nav items, creating a more consistent experience for users setting up their sites in the customizer.

Once the fundamentals of the term status API have been built, it’s possible to imagine a number of other following user-facing improvements:

“Trash” status for terms, including the ability to restore items from the trash

Private terms, which can only be seen and assigned by users with the proper capabilities

Autosave when editing terms in the Dashboard

“Pending” terms, submitted by Contributors but not generally available until approved by an Editor

AND SO MUCH MORE!!!!1!

Technical outline and proposed implementation plan

I’ve reviewed the developer-facing parts of the Taxonomy API to catalog those areas that would need adjustment with the introduction of term status. I’ve also reviewed the post_status API to get a better sense of what a relatively well-rounded implementation of “status” has to recommend (and recommend against!). Here’s how I see the implementation process, broken down into phases that may span releases.

Database schema upgrade
While it’d be possible to implement term status using termmeta, it’d almost certainly result in significant performance problems. A dedicated ‘status’ column in wp_term_taxonomy is fastest, and best parallels the other content types.

Upgrade routine
The upgrade routine would include the schema update, as well as the filling-in of the new database column for all existing terms.

Status registration and fetching API functions
The minimal functions needed for ‘auto-draft’ support are probably as follows:

register_term_status() (‘publish’ and ‘auto-draft’ would likely be the first two statuses implemented)

get_term_status(), _status_object() and _stati(), to be used when whitelisting, etc

‘Status’ support when creating or updating terms
Presumably, this will be a ‘status’ argument in wp_update_term() and wp_insert_term(), with checks against a whitelist of registered statuses.

‘Status’ support when querying terms
For backward compatibility, get_terms() and wp_get_object_terms() should default to returning only those terms with the ‘publish’ status. A ‘status’ parameter will allow more fine-grained filtering. This parameter will work similarly to other item queries, with support for arrays of statuses as well as magic terms like ‘any’. More specific functions like get_term_by() and term_exists() will either ignore status or presume ‘public’; more discussion is needed. We’ll also need to make decisions about how non-public terms are handled in hierarchical queries – get_pages() and friends may be helpful benchmarks.

Status “transition” logic
Similar to wp_transition_post_status(), we want hooks that fire on term status transitions.

I take items 1-6 to be a minimal API for term statuses. Next are the details related to the first use case: ‘auto-draft’.

‘Auto-draft’ and slugs
Posts with status ‘auto-draft’ do not get slugs, so that they don’t interfere with the creation of other posts. See wp_unique_post_slug(). ‘Auto-draft’ should probably act similarly.

‘Auto-draft’ terms should be excluded from XHR exports
Just like posts.

‘Auto-draft’ deletion on a schedule
Posts with ‘auto-draft’ are deleted when they’re older than 7 days. This is not likely to be a huge problem for terms, but should probably be addressed anyway.

Protect auto-draft (and other terms from non-public statuses) in canonical, permalinks, and rewrites
Things that it (probably) shouldn’t be possible to do:

Get a permalink of the archive for a non-public term

Load the archive of a non-public term on the front-end

Convenience functionsSomething like wp_publish_term() would be convenient. Maybe others.

I believe that 7-11 are pretty close to what we need to support the Customizer use case. There are a few more obvious things that can happen in future releases, independent of any specific feature:

Status interface when editing/creating a term
Auto-draft won’t be a ‘public’ status, but once another ‘public’ status is available, a dropdown should appear.

Status filters for the Terms list table
Like we have for posts.

Integration with capabilities
Work is being done on fine-grained capabilities for terms #35614. Certain statuses will probably integrate with this.

These last items aren’t necessary for the initial use case, but are the kinds of things that developers will expect as they start using term statuses in plugins.

Potential problems

A couple of potential gotchas:

Performance. Term queries are currently pretty fast. Adding a status column, and including an additional WHERE clause with every query, is not going to make things faster. We should think about the proper use of indexes, etc.

Direct database queries. Plugins making direct queries against the terms tables will not properly exclude non-public terms. Not much we can do about this from a technical point of view, but we should write some good documentation to help avoid problems.

How to handle single term-fetching functions.get_term_by(), get_term(), and term_exists() could all be used in ways that expect the returned value to be a public term (since all terms are now, in fact, public). It would be bad if someone got back an ‘auto-draft’ term for get_term_by( ‘name’, ‘foo’ ). We can explicitly blacklist ‘auto-draft’ terms. Or we can always exclude non-public terms. Either way, we probably want to offer flexibility for developers who want to return non-public terms. I’m not sure of the strategy here: we want something that balances developer expectations with backward compatibility.

First off, looking at this I don’t particularly see a need for 1-6 to be done in a separate release prior to 7+. Obviously they’re prerequisites for 7+, but do you see any need for them to be in separate releases?

Thoughts on Potential Problems:

It’s kind of too bad we can’t rely on the ability to manipulate tables. It seems like something like enum would be especially efficient here. As is, the new column with probably need to be indexed, but we can probably also look at potentially useful multi-column indexes that involve it if needed.

I think that we’ve set the precedent for long enough that we can’t account for direct queries when we worry about backwards compatibility.

My first reaction here is that it would be best for these single-term functions to ignore all non-public statuses unless someone specifies otherwise. Since we currently don’t have non-public statuses, it seems like this would be most in line with the current functionality.

Re separate releases: I agree that 1-6 doesn’t need to come before. I separated them out because, as you note, they’re a prerequisite for what comes after. If 1-11 came in a single release, that would be fine.

Seems like making the simple complicated for the sake of the Customizer (not everyone is a fan). How about just making taxonomy changes in the Customizer permanent. This way we can maintain the current speed and not overload WordPress with more optional code and slowing performance further. Concrete5 smokes WordPress these days in head to head performance. I’d rather see us fighting for performance improvements than compromising performance further.

Thanks for the feedback. As noted in the proposal, the customizer is the initial use case, but there are lots of other reasons why such an API might be helpful.

As for processing speed, we should absolutely run benchmarks on different schemas and strategies, and take the result into consideration.

I can’t say anything about Concrete5, but I do know that the developer experience of having disparity between content types has the potential to be an impediment that’s worth trade-offs in other areas. For this reason alone, the proposal is worth considering.

This is not about the customizer. Terms are created in many places in core, and for most of them, they probably shouldn’t be published to the site immediately. Term status would allow that functionality in addition to the numerous future features outlined in the proposal.

I tried Concrete5 once and it quickly became extremely unpleasant. Regardless, there may not be a lot of value in performance comparisons between inherently different platforms.

I don’t fully understand #12706, but it seems that at least part of the issue has to do with the way that statuses are exposed and used in the user-facing publication flow. The basics of what I’m proposing above (at least, the first 11 items) don’t expose anything to the user at all. And in any case, the publication workflow for terms – such as it currently exists – is pretty different from, and simpler than, the post workflow.

That said, #12706 and related tickets may suggest that we should take a step back and consider what a “status” really is. In the case of posts, status plays a lot of roles, some of which are at odds with each other. (See: ‘future’ vs ‘private’.) Comment status is a bit clearer: it’s primarily about moderation status. User status is not really fleshed out in a meaningful way, and so doesn’t do much. It would be helpful to discuss what term status ought to be – and ought *not* to be – both at the outset and in the long run.

I honestly don’t think this will make that any harder to fix. It won’t make it any easier either, but I don’t think that really prevents this from happening.

The big thing we need to avoid so we don’t make that particular issue worse, is hard coding any statuses (with the exception there being to hard code “publish” as the initial status to be used during the upgrade routine). When we ignore “private” statuses, we should make sure that a developer can decide whether a status is private or not, etc.

I do not think it’s best for WordPress core to start term statuses until post statuses are finished, and user statuses need untangling, too: #34316.

I’d like to see this worked on outside of the constraints & pressure of core release cycles. If we have not figured post statuses out in 6 years, it’s unlikely that we will for terms in 6-9 weeks. The plugin-first approach seems to better highlight blockers in core, too.

I have a plugin that also adds an order column to wp_term_taxonomy. Adding a status column is another step down a slippery slope of making taxonomy-term objects act like post objects, and still without a many-to-many relationship table between them.

In #37686, I had proposed adding an object_type column to wp_term_relationships to allow terms to be shared between objects (posts, comments, users, etc…) — I think the same arguments against that there work against this here.

You picked the most stable parts of the post-status API to build off of, the procedural bits, and that I think makes a lot of sense.

It would be bad if someone got back an ‘auto-draft’ term for get_term_by( ‘name’, ‘foo’ ). We can explicitly blacklist ‘auto-draft’ terms

I think parity with get_post/s() is more important. Arguably, if I was getting a term before, I still want it now, regardless of its status attribute. What happens if you try to get_post() an auto-draft, revision, or inherit, and what’s the history there?

Performance will not take a hit so long as this new column is indexed.

User impact with direct database queries should be infinitesimally small, and it’s likely only a small subset of those users that would be victimized by using this type of code naively.

I like this idea. Statuseso could be useful for a lot of things. scheduled terms right be really cool for example. Couple of points of feedback.

I agree with above comment that we should be working towards a single apiece for statuses in wordpress. adding a new thing seems crazy at this point.

If we are changing the database for this, is it worth thinking about adding an author as well. I know @jjj has a plugin for it. I know it has been an issue for me with sites with large editor team to backtrace bad categories and who created them.

One thing you didn’t mention is what the api will look like for taxonomy queries. Will a new parameter be added that defaults to public.