How a Request Boots
The path from index.php through wp-settings.php, and which hook is the earliest safe place for your code.
index.php -> wp-blog-header.php -> wp-load.php -> wp-config.php -> wp-settings.php -> wp() -> template-loader.phpA front-end request only ever enters WordPress through one file. The web server sends the URL to index.php, which defines WP_USE_THEMES as true and requires wp-blog-header.php. That file loads wp-load.php, calls wp() to run the main query, then includes wp-includes/template-loader.php to choose and render a template. Everything else hangs off those three steps.
wp-load to wp-settings
wp-load.php defines ABSPATH and looks for wp-config.php in ABSPATH first, then one directory above it — which is why the config file can live outside the web root. wp-config.php sets the database constants and ends by requiring wp-settings.php, the file that actually assembles WordPress.
The order that matters
- Core files load, including
plugin.php, soadd_action()andadd_filter()exist before any of your code runs. - Must-use plugins are included, then
muplugins_loadedfires. - Active plugins are included in the order held in the
active_pluginsoption, thenpluggable.phpis included, thenplugins_loadedfires. setup_themefires, the child theme'sfunctions.phploads before the parent's, thenafter_setup_themefires.- The current user is resolved, then
init, thenwp_loaded.
The practical consequence: the body of your plugin file runs before the theme exists, before any user is known, and before init. Registering hooks there is correct. Registering post types, taxonomies or shortcodes there, or calling a translation function, is too early — since WordPress 6.7 the latter also raises a _doing_it_wrong() notice.
Example
<?php
/**
* Plugin Name: Acme Catalogue
*/
// This block runs while wp-settings.php is including plugin files: before the
// theme is loaded, before any user is resolved, before init. Register hooks
// here and nothing else.
add_action( 'after_setup_theme', 'acme_theme_support' );
add_action( 'init', 'acme_register_product_type' );
add_action( 'wp_loaded', 'acme_warm_cache' );
function acme_theme_support() {
// The active theme's functions.php has already been included by now, so
// this either adds to or overrides what the theme declared.
add_theme_support( 'post-thumbnails' );
}
function acme_register_product_type() {
// init is the earliest point at which post types may be registered, and
// the earliest at which __() is safe to call.
register_post_type(
'acme_product',
array(
'labels' => array( 'name' => __( 'Products', 'acme' ) ),
'public' => true,
'has_archive' => true,
'supports' => array( 'title', 'editor', 'thumbnail' ),
)
);
}
function acme_warm_cache() {
// wp_loaded: plugins, theme and init are all finished and the current user
// is set, but the main query has not run yet.
if ( ! is_user_logged_in() ) {
return;
}
// ... prime whatever this user is about to need
}When to use it
- Deciding whether new plugin code belongs at file scope, on after_setup_theme, or on init
- Fixing a function that returns nothing when called from a plugin but works from a template
- Moving wp-config.php out of the web root without breaking the boot
- Writing a lightweight standalone script that needs $wpdb but not the whole stack
More examples
A must-use plugin that has to win
Shows the two things mu-plugins are genuinely for: getting a hook registered ahead of every normal plugin, and replacing a pluggable function. Reach for this when a filter must not be outranked, or when a whole environment needs mail, cron or redirects neutralised.
<?php
/**
* Plugin Name: Acme Environment Guard
* File: wp-content/mu-plugins/acme-environment.php
*/
// mu-plugins are included before any regular plugin and cannot be deactivated
// from the admin, so this filter is in place before a plugin can register a
// competing one at the same priority.
add_filter(
'pre_option_blog_public',
static function () {
return 'production' === wp_get_environment_type() ? '1' : '0';
}
);
// Pluggable overrides belong at file scope. Core includes pluggable.php only
// after every plugin file has run, so this declaration gets there first.
if ( ! function_exists( 'wp_mail' ) && 'production' !== wp_get_environment_type() ) {
function wp_mail( $to, $subject, $message, $headers = '', $attachments = array() ) {
error_log( sprintf( 'Suppressed mail: %s', $subject ) );
return true;
}
}Booting only part of WordPress
A standalone script that needs the database credentials and $wpdb but nothing else. Useful for load-balancer health checks and cron scripts where booting every plugin on every poll is wasted work — but remember that with SHORTINIT no hook of yours fires at all.
<?php
// tools/healthcheck.php, in a tools directory inside the WordPress root.
//
// SHORTINIT stops wp-settings.php early: no plugins, no theme, no rewrite
// rules, no WP_Query. $wpdb, the object cache and wp-includes/functions.php
// are still available, and the script boots in a fraction of the time.
define( 'SHORTINIT', true );
require dirname( __DIR__ ) . '/wp-load.php';
global $wpdb;
$published = (int) $wpdb->get_var(
"SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_status = 'publish' AND post_type = 'post'"
);
header( 'Content-Type: application/json' );
echo wp_json_encode( array( 'published' => $published ) );Proving the order to yourself
did_action() returns how many times a hook has fired, which is the reliable way to answer "has this happened yet?" without guessing. Use it when a call works from a template but returns nothing from a plugin file, which is nearly always a timing problem rather than a broken function.
<?php
/**
* Plugin Name: Acme Boot Trace
*/
// Drop this in while debugging an "it works in one place and not the other"
// problem. current_filter() reports which hook is running.
foreach ( array( 'muplugins_loaded', 'plugins_loaded', 'setup_theme', 'after_setup_theme', 'init', 'wp_loaded', 'template_redirect' ) as $hook ) {
add_action(
$hook,
static function () {
error_log(
sprintf(
'%s | user: %d | theme loaded: %s',
current_filter(),
get_current_user_id(), // 0 until the user is resolved
did_action( 'after_setup_theme' ) ? 'yes' : 'no'
)
);
},
0
);
}
Discussion