Registering hooks before the WordPress hooks system starts

Usually hooks in WordPress can be used only after the core initialization, when the global object $wp_filter is already created. However sometimes there is a need to create a hook for an action earlier — even before the hooks system starts working. For this, WordPress allows you to define the global variable $wp_filter in advance.

How it works

In WP there is the function WP_Hook::build_preinitialized_hooks(), which normalizes the array $wp_filter predefined in advance, and turns it into fully-fledged WP_Hook objects.

Thus, you can prepare in advance $GLOBALS['wp_filter'] as if the hooks were already registered. And WP will then in its own time turn your array into a WP_Hook object and when the hook is executed, your callback will run.

  • Hooks are usually registered after WordPress starts, but you can do this earlier by manually defining $GLOBALS['wp_filter'].
  • For this, the method WP_Hook::build_preinitialized_hooks() is used.
  • Such code works only in wp-config.php or earlier.
  • Suitable for temporary solutions and low-level fixes.

Format of the $wp_filter array

In the array key there should be the name of the hook, and in the value you need to specify an array of callbacks grouped by priorities:

$filters = [
	'wp_fatal_error_handler_enabled' => [
		10 => [
			[
				'accepted_args' => 0,
				'function'      => function() {
					return false;
				},
			],
		],
	],
];

The hook wp_fatal_error_handler_enabled is called even before the hooks system starts. Because of this, it can be overridden already in wp-config.php.

Example: redirect from the home page to a subdomain

Consider a real case. There is a multisite: / (English version) and /ru/ (Russian version). During development of the English version, it was temporarily required to redirect the home page //ru/.

To make such non-standard behavior obvious, and the code easy to find and remove, it was moved to wp-config.php. But since the hooks system is not yet working there, the hook was registered via $GLOBALS['wp_filter'].

/// Temp fix for prod site while EN version is under development.
defined( 'WP_CLI' ) || defined( 'DOING_CRON' ) || defined( 'DOING_AJAX' ) || temp_fix_for_multisite_migration_for_prod();

function temp_fix_for_multisite_migration_for_prod() {
	$host = 'example.com';
	if( $host !== $_SERVER['HTTP_HOST'] ){
		return;
	}

	// ! before REQUEST_URI change
	if( preg_match( '~^/ru/?$~', $_SERVER['REQUEST_URI'] ) ){
		header( "Location: https://$host", true, 307 );
		exit;
	}

	// request substitution
	if( '/' === $_SERVER['REQUEST_URI'] ){
		$_SERVER['REQUEST_URI'] = '/ru/';
	}

	$temp_ru_home_url_fix = static function( $url, $path, $orig_scheme, $blog_id ){
		if( ! $path || '/' === $path ){
			$url = preg_replace( '~/ru/?~', '/', $url );
		}

		return $url;
	};

	// add a hook for the wp action
	$GLOBALS['wp_filter'] = [
		'wp' => [
			0 => [
				[
					'function' => static fn() => add_filter( 'home_url', $temp_ru_home_url_fix, 99, 4 ),
					'accepted_args' => 0,
				],
			],
		],
	];
}

Example: disabling the recovery screen

$GLOBALS['wp_filter'] = [
	'wp_fatal_error_handler_enabled' => [
		10 => [[
			'function'      => static fn() => false,
			'accepted_args' => 0,
		]],
	],
];