Technote

Development workflow Intro

Series Using hooks and filters properly Part 2 of 8

Hook timing: before init, the world is not there yet

The same code on the same hook behaves differently depending on when it runs. Taxonomies, post types and translations all see an incomplete world before init.

Getting the hook name right is only half of it. During a single request WordPress assembles the world in a fixed order, and your code sees something quite different depending on how far that assembly has got when you join in.

The order of a single request

Roughly this sequence. Anything a later stage produces simply does not exist in an earlier one.

Assembly order of one request — earlier stages cannot see what later stages will build

plugins_loaded fires once every plugin file has been read; from here you can safely call another plugin’s functions and classes. after_setup_theme is where a theme declares its capabilities, and init is the registration point — post types, taxonomies, rewrites, sessions and translations are all settled there.

wp_loaded runs after registration finishes, so it is the first place you may assume a complete world. From template_redirect onwards the main query has run, which is what makes conditional tags such as is_singular() meaningful.

What must not happen before init

Before init — the top four see an unfinished world; the bottom two are safe

Translation fails the most quietly of all. Call load_plugin_textdomain() earlier than init and the locale is not yet settled, so no language pack is attached and every later __() hands back the original string. There is no error — the screen is simply in the wrong language, which sends you hunting through .mo paths and filenames. The code was fine; the timing was not.

For the same reason you cannot put __() into a class constant. A PHP constant expression cannot contain a function call, and even if it could, that expression is evaluated at file load — well before init. Label tables belong in a static method, not a constant array.

// ✗ Evaluated at file load, with no locale yet (and constants cannot call functions)
const LABELS = [ 'speed' => __( 'Speed', 'wper' ) ];

// ✓ Translate when it is called
public static function labels() {
	return [ 'speed' => __( 'Speed', 'wper' ) ];
}

Even inside init there is an order

Because so much registration piles into init, priority within it can genuinely change the outcome. Rewrite rules accumulate in the order their permastructs are registered.

This repository ran into exactly that. A custom post type with the archive slug tools generates an attachment rule that was registered before the taxonomy archive nested beneath it at /tools/type/{slug}/, and it swallowed that address. Both rules were perfectly valid, so flushing rewrites changed nothing — the problem was order, not existence. The fix was to register taxonomies at init priority 9, ahead of post types at 10.

Failures of this shape are slow to find because the page still returns 200 while being quietly wrong. Habits for catching them are collected in the development workflow archive, and a sweep of these points on a live site is part of the diagnostic in our optimization program.

Next part

Sometimes the timing is right and your value still never reaches the page, because somebody after you changes it again. Part three is about priority races and their limits.

More on this topic

All technotes

Development workflow Practical

Turning taste arguments into rule checks

"It feels a bit cramped" can be neither argued with nor fixed. Spacing off the scale, colour off the palette, contrast below threshold, missing states — four rules…

Designers 9 min read

Development workflow Practical

Do not swap everything at once

A full swap makes every problem appear at the same moment — which means none of them can be attributed. So you switch one template at a time.

Designers 6 min read

Development workflow Practical

Adding an SCSS build, and whether to commit the output

WordPress themes are expected to deploy without a build step, which leads to the opposite conclusion from ordinary application code — and to its own costs.

Developers 7 min read

₩270,000 · Join the program