How WordPress Core Loads

When working with themes, plugins, and in general with any WordPress code, including hacks in the popular functions.php theme file. It's good to know in what sequence the engine's php files are loaded, when important hooks are triggered, and what important constants are defined. In this article, let's talk about that loading sequence.

Examples

To make it clearer, let's take a simple task: we need to connect some code in the WordPress admin panel and only there - the code should not be triggered at the front end or in AJAX requests. Someone will say that it is enough to check with is_admin(), but it is not true, because is_admin() will return true also for AJAX request to file admin-ajax.php.

Or for example you can plug some logic on admin_init hook and then wonder why my logic works for AJAX requests too. Or why it interferes with plugins that work with AJAX...

Or here's another example. When some code is executed in the theme's functions.php file that depends on other code plugged in via the init hook. For example, you have created new post type on the init hook (action) and in functions.php you execute some check that relies on this new post type and expect the code to work, but it doesn't, because the direct code in functions.php triggers before the init action...

There are a lot of examples like that. But to avoid making such mistakes, you need to figure out and understand the sequence of loading the WordPress core. That's what we're going to do.

WP Loading Order (theory)

Understanding the loading logic is not as difficult as it might seem at first glance. To do this, let's take a look at this picture, which I like so much:

There are four variants of loading. Actually there are a bit more, but these are the main ones:

  • FrontEnd (theme) load.
  • REST request load.
  • Admin panel load.
  • AJAX request (admin-ajax.php) load.

In all cases, the core of WordPress is loaded - the file wp-load.php. The core loads always and everywhere!

Before we move on to each type of loading, we need to figure out how loads the core itself.

WordPress Core Load Sequence

WP core is loaded at any request: front, ajax, admin, REST ...

wp-load.php
	wp-config.php
		wp-settings.php

			// Load functions: wp_debug_mode(), timer_start(), require_wp_db() ...
			// Constants functions: wp_initial_constants(), wp_cookie_constants() ...
			// Plugin functions (hooks, activation): do_action(), plugin_dir_url(), register_activation_hook().

			// Constants setup: WP_START_TIMESTAMP, WP_MEMORY_LIMIT, WP_MAX_MEMORY_LIMIT, WP_DEBUG, SCRIPT_DEBUG, WP_CONTENT_DIR, WP_CACHE.

			// Standardize server variables: wp_fix_server_vars().

			// maintenance mode check: wp_maintenance().

			// Enables load speed timer: timer_start().

			// Debug mode check: wp_debug_mode().

			// Includes 'wp-content/advanced-cache.php' if it is exists and WP_CACHE is on.

			// Data base. $wpdb. require_wp_db().
			// Loads 'wp-content/db.php' if it is exists. Creates DB connection
			// and setups all DB related variables: prefixes and so on...

			// Object cache: 'wp-content/object-cache.php' if exists or 'wp-include/cache.php'.

			// Base WP hooks (filters): default-filters.php.

			// Multisite is enabled (if need)
			// Loads 'wp-content/sunrise.php' if exists (for multisite only).

			// register_shutdown_function( 'shutdown_action_hook' )

			// SHORTINIT: stopping the download, where there is only the most basic.
			if( SHORTINIT )
				return false;

			// The localization functions are connected.

			// Checks whether WP is installed: wp_not_installed().

			// Connects a bunch of files with the rest of the WordPress functions.

			// Connects Must-use plugins and corresponding action is triggered:
			do_action( 'muplugins_loaded' );

			// cookie, ssl constants: COOKIEPATH, COOKIE_DOMAIN
			// Common global vars: $pagenow, $is_apache, $is_nginx, $is_lynx
			// Client global vars: $is_opera, $is_NS4, $is_safari, $is_chrome, $is_iphone, $is_IE, $is_edge

			// Active plugins are connected
			// pluggable functions are connected: pluggable.php.
			// triggers hook
			do_action( 'plugins_loaded' );

			// Force add slashes for $_POST, $_REQUEST ... values. See. wp_magic_quotes().

			// Global vars:
			// $wp_the_query      — new WP_Query()
			// $wp_query          — $wp_the_query
			// $wp_rewrite        — new WP_Rewrite() — constants, functions, rewrite rules.
			// $wp                — new WP() — base WP query (runs later).
			// $wp_widget_factory — new WP_Widget_Factory()
			// $wp_roles          — new WP_Roles()

			// Current theme
			do_action( 'setup_theme' );
			// functions.php (child) - first connect functions.php of the child theme
			// functions.php (parent) - then connect functions.php of the main theme
			// WP translation file: load_default_textdomain()
			do_action( 'after_setup_theme' ); // first hook allowed in the theme

			// Sets the current user (creates an object).
			// See. wp_get_current_user()
			// The user is often already defined by plugins after the 'plugins_loaded' action.
			$wp->init();

			// init action. The time when WP environment, themes and plugins is already activated,
			// but nothing has been displayed on the screen yet:
			do_action( 'init' );

			// Widget registration: 'widget_init' action

			// Checking site status for multisite.
			// The site may be: deleted, inactive, in the archive. See. ms_site_check()
			// If the site failed the check, the drop file will be called and PHP is aborted via die().

			// Same as `init` only after the status check.
			// This line of code can be not reached. For example on REST request.
			do_action( 'wp_loaded' );

When loading the core, the functions.php theme file is always connected, even in the admin panel.I.e. it does not matter whether we need a theme or not, functions.php works and therefore it can be put on a par with plugins... It is done so as a convenience, so that any code can be "stick" in our, and therefore a favorite, file functions.php. This behavior is not logical from a programming point of view, but very convenient from a development point of view.

The core, as we see, is in the wp-settings.php file, but before it wp-load.php is called. We need this in order to find the wp-config.php file. The fact is that wp-config.php may be in the same folder as all the wordPress files, or it may be moved to the parent folder. The task of wp-load.php is to find the configuration file and connect it . If it is nowhere to be found, WP will prompt you to create it. In fact, this is exactly what happens when you install WordPress.

After the configuration file is found, it connects. It specifies all the important constants, database connection parameters, etc. And then the core - the wp-settings.php file - is connected.

Front-End Load (Theme)

Any Frontend requests are sent to the index.php file in the root folder of the domain. This means that WordPress is loaded with a theme. To state this fact, the WP_USE_THEMES constant is defined in index.php. So, for example template_redirect hook from the file template-loader.php will work only if this constant is defined.

Front loading process looks like this:

index.php
	// Sets the WP_USE_THEMES constant

	wp-blog-header.php
		// CORE (described above)
		require_once dirname(__FILE__) . '/wp-load.php';

		// Setup WP main query
		// Run main query and determine the current page type
		// See. https://wp-kama.com/function/wp
		wp(); // See. WP::main()

		// connection of the proper template file
		require_once ABSPATH . WPINC . '/template-loader.php';
  • For more details on the setup of the main query and the WP environment, read the code of the wp() function.

  • For the logic of the template-loader.php file (connecting the theme file), see: Template Hierarchy.

Infographic of how a WordPress Fornd-End Loads:

Infographic of how a WordPress query works:

REST Request Load

A 90% REST request is handled the same way as a frontend request. The process starts with index.php file as well. The request parameter rest_route=route is set via rewrite rules (user-friendly URLs).

^wp-json/?$    => index.php?rest_route=/
^wp-json/(.*)? => index.php?rest_route=/$matches[1]

The process of loading a REST request looks like this:

index.php
	// sets WP_USE_THEMES constant

	wp-blog-header.php
		// WP CORE See https://wp-kama.com/handbook/wordpress/loading#wpcore-load
		require_once dirname(__FILE__) . '/wp-load.php';

		// In the process of loading the core, a hook `parse_request` is created
		// and it is triggered in the `wp()` function
		add_action( 'parse_request', 'rest_api_loaded' );

		// Sets the main WordPress query.
		// Makes a request and determines which page is loading.
		// When the `parse_request` hook kicks in, control is passed
		// to the rest_api_loaded() function, which aborts PHP via die.
		// See https://wp-kama.com/function/wp
		wp(); // См. WP::main()

Explanation of wp() and parse_request hook

The wp() function calls the WP::main() method, it does the following in order:

  1. Calls WP::init().

    This method sets the current user for normal (non-REST) requests. See wp_get_current_user().

    Later (if it's a REST request), on rest_authentication_errors authentication hook the authorized user will be nulled if the nonce code is wrong. See also:

  2. Calls WP::parse_request().

    This method sets the parameters of the current query $wp->query_vars.

  3. REST Running - rest_api_loaded().

    At the end of WP::parse_request(), the parse_request hook is triggered and it runs the rest_api_loaded() function:

    ``php
    add_action('parse_request', 'rest_api_loaded' );

    
    ``rest_api_loaded()`` checks the query parameter ``$GLOBALS['wp']->query_vars['rest_route']``. If it's a REST request, it starts the REST logic (query logic for normal pages ends here).
    1. Sets the define('REST_REQUEST', true ) constant.

    2. Starts the REST server rest_get_server().

      The rest_api_init hook is triggered on startup. It adds routes to the existing server.

    3. the REST request WP_REST_Server::serve_request( $route ) is processed.

      When the request is processed, the current route is checked for availability, then the route is processed, and then the REST server response is returned as a WP_REST_Response object.

      When the request is processed, the following useful hooks are triggered (in that order):

    4. PHP is interrupted by die().

So

rest_api_loaded() function is fully responsible for processing REST request.

The REST request looks exactly like a normal frontend request. Until setting the main WordPress request by wp() function. It sets the current user as in a normal frontend request. But:

  • The headers WP::send_headers() are NOT set.
  • The main query WP::query_posts() does NOT happen.
  • The WP::handle_404() does NOT handle the 404 page.
  • The template-loader.php file is NOT calling (it defines the page template).
  • The redirect template_redirect hook is NOT triggering.

The REST server is setups response headers; it also makes a corresponding request to the database or do any other stuff.

Admin Load

The WordPress admin does not have anything to do with the "root" file index.php. All of its loading begins with the file /wp-admin/admin.php.

The first and foremost thing that happens there - it is defined WP_ADMIN constant, which tells all the code that is executed then, that we are in the admin area.

wp-admin/admin.php
	// sets WP_ADMIN constant
	define( 'WP_ADMIN', true );

	// CORE (described above)
	require_once dirname(dirname(__FILE__)) . '/wp-load.php';

	// Checks the need to Upgrade WordPress

	// Connect files (additional functions) of the admin panel
	require_once ABSPATH . 'wp-admin/includes/admin.php';

	// Checks if the user is logged in before allowing him to access any page
	// If the user's permissions are insufficient, the user will "fly away" to the login page
	auth_redirect();

	// admin hook
	do_action( 'admin_init' );

	// the current page data is set
	set_current_screen();

	// header
	require_once ABSPATH . 'wp-admin/admin-header.php';

	// content - here the logic branches off and the content is loaded
	// either from a plugin
	// or none (if it is a data import request)
	// or native admin files, such as wp-admin/options-general.php

	// footer
	include ABSPATH . 'wp-admin/admin-footer.php';

AJAX Requests Load

It's common to send AJAX requests to the /wp-admin/admin-ajax.php file, which is where WordPress starts loading AJAX requests.

Let's break down the loading order and then some explanation.

wp-admin/admin.php
	// sets WP_ADMIN constant
	define( 'WP_ADMIN', true );

	// CORE (described above)
	require_once dirname(dirname(__FILE__)) . '/wp-load.php';

	// The 'action' request parameter must be defined for any AJAX request.
	if( empty( $_REQUEST['action'] ) )
		die( '0' );

	// Connect files (additional functions) of the admin panel
	require_once ABSPATH . 'wp-admin/includes/admin.php';

	// connect the Ajax request handlers of the WordPress core
	require_once ABSPATH . 'wp-admin/includes/ajax-actions.php';

	// admin hook
	do_action( 'admin_init' );

	// Check and fix `action` parameter name related to the WordPress core and adding the right hook
	add_action( 'wp_ajax_' . $_GET['action'], 'wp_ajax_' . str_replace( '-', '_', $_GET['action'] ), 1 );

	// check the user permissions and run the necessary hook
	do_action( 'wp_ajax_' . $_REQUEST['action'] );
	// or
	do_action( 'wp_ajax_nopriv_' . $_REQUEST['action'] );

	// Default AJAX response
	die( '0' );

When making an AJAX request, WP_ADMIN constant is also set. This means that any AJAX request looks to the core as a request to the admin area, even if this request is made from the front... It's not always necessary, but it's the way WordPress does it for standardization and usability reasons.

Next, the WP CORE is loaded, in which triggered all plugin hooks. That is, all plugins are plugged in and run, the functions.php file code is executed, and the init action is triggered. That is, the core of WordPress is fully loaded and processed.

Now, when the core is processed, all PHP functions of the admin panel are additionally connected (although they are not needed sometimes) and one of the AJAX hooks is started (which one depends on authorization): wp_ajax_(action) or wp_ajax_nopriv_(action).

Running the AJAX hook is the last step - the AJAX operation is done and it should interrupt PHP and display some data - usually just a string or a string in json format.

By the way, for convenient json response for AJAX requests WP has a special function - wp_json_encode() and two functions derived from it:

WP CRON Loading

WP sends a request from any page to the /wp-cron.php file.

It, in turn:

  1. "Activate" the DOING_CRON constant.
  2. Loads the WordPress environment: /wp-load.php file.
  3. Performs crown tasks.

Detailed see here.