What are Hooks in WordPress? How to use WordPress Hooks?

9 min read
577

Every WordPress developer knows that hooks the main communication mechanism between the core, plugins, and themes. If not, this is a critically important topic that you should dive into to work with WordPress and its ecosystem.

I have participated in a few dozen technical interviews for software engineers with WordPress knowledge and was really amazed that a huge part of them intuitively use hooks but don’t know how it works. Personally, I feel the topic is the most critical topic we should discuss one more time. Anyway, let’s get started.

Hooks are Actions and Filters

Actions allow you to add data or change how WordPress operates. Actions will run at a specific point in the execution of WordPress Core, plugins, and themes. Callback functions for Actions can perform some kind of a task, like echoing output to the user or inserting something into the database. Callback functions for an Action do not return anything back to the calling Action hook.

WordPress Develop Documentation

Filters give you the ability to change data during the execution of WordPress Core, plugins, and themes. Callback functions for Filters will accept a variable, modify it, and return it. They are meant to work in an isolated manner, and should never have side effects such as affecting global variables and output. Filters expect to have something returned back to them.

WordPress Develop Documentation

In a nutshell, the action runs something when a filter allows modifying the variable. Let’s check a few examples:

<?php

add_action( 'save_post', 'notify_administrator', 10, 3 );

function notify_administrator( $post_id, WP_Post $post, $update ) {
	// Really important code here ... 

	wp_mail( $admin_email, $subject, $message );
}
<?php

add_filter( 'post_class', 'modify_post_classes', 10, 3 );

function modify_post_classes( $classes, $class, $post_id ) {
	if ( in_array( get_post_type( $post_id ), [ 'review', 'service' ] ) ) {
		$classes[] = 'dark-theme';
	}

	return $classes;
}

Pay attention that the filter callback must return modified data with the same data type in the same format but a bit changed.

How do Hooks work technically?

An HTTP request came from the user, and the PHP interpreter launches the WordPress files necessary to run the core, including the storage in the form of the WP_Hook class. The PHP interpreter then reads the code line by line and stores the callbacks from each add-hook function in storage. And when the interpreter hits the launch-hook function, it accesses this storage and launches the callbacks for this action or filter one by one.

Such an excellent mechanism gives us crazy flexibility for development. Are you a theme or plugin developer? You can easily change a core/plugin/theme behavior inside your code without any problem (if there are enough hooks :D).

Here is a simplified example:

<?php

// Run code...
// Load 1st plugin
add_action( 'before_magic', 'say_hi' ); // Remember it well.
// Load 2nd plugin
add_action( 'before_magic', 'introduce_yourself' ); // Remember it well.
// Loaded plugins.
// Load active theme
add_action( 'after_magic', 'applause' ); // Remember it well.
// Loaded theme.
// A few miliseconds later...
// Run some code on a page.
function magic() {
	do_action( 'before_magic' ); // Run all callbacks that the storage has to the action.

	$this->take_hat();
	$this->stick_your_hand_in_your_hat();
	$this->get_hare();

	do_action( 'after_magic' ); // Run all callbacks that the storage has to the action.
}
magic();

In such a way, the 1st, 2nd plugins and theme can influence the code located on a page. Isn’t that a miracle?

List of the Hooks Functions

All hooks API based on a few functions which simplify work with the WP_Hook class. We already know that hooks are divided on action and filter, but the hooks’ function also we can clarify on groups.

Calls the callback functions that have been added to a hook (greatest use):

  • do_action
  • apply_filters

Calls the callback functions that have been added to a hook, specifying arguments in an array:

  • do_action_ref_array
  • apply_filters_ref_array

Fires functions attached to a deprecated hook:

  • do_action_deprecated
  • apply_filters_deprecated

Add a callback function from a hook (greatest use):

  • add_action
  • add_filter

Remove a callback function from a hook:

  • remove_action
  • remove_filter

Remove all of the callback functions from a hook:

  • remove_all_actions
  • remove_all_filters

Retrieve the number of times an action has been fired during the current request:

  • did_action

Check if any callback has been registered for a hook:

  • has_action
  • has_filter

Retrieve the name of the current hook:

  • current_action
  • current_filter

Return whether or not a hook is currently being processed:

  • doing_action
  • doing_filter

Hooks going to pull on something Big Callable

Why is the callable pseudotype so important for the hooks? Since we do not need to execute our custom code immediately, but at a certain moment, we must provide all the necessary information on how and when this code needs to be run. The add_action( 'save_post', 'notify_administrator' ); line should be read as “on the action of saving any post, call the function to inform the administrator. It is essential that we don’t run or execute the function, we answer the questions “When?” and “What to do?”.

In two words, callable is something that we can call. What can we call in PHP? We can call functions or public object/class methods, but how to pass the function or method as a callable argument? Just pass the function or method name using the special syntax:

<?php

function do_something_awesome() {
	// ...
}

/**
 * 1. Function name.
 */
is_callable( 'do_something_awesome' ); // true

class Awesome {

	public static function do_something() {
		// ...
	}

	public function do_something_more() {
	}

	/**
	 * 4. Methods inside the method.
	 */
	public function hooks() {

		is_callable( [ $this, 'do_something' ] ); // true
		is_callable( [ $this, 'do_something_more' ] ); // true
		is_callable( [ __CLASS__, 'do_something' ] ); // true
		is_callable( 'self::do_something_more' ); // true
	}

	public function __invoke( ...$values ) {
	}
}

$awesome = new Awesome();
$awesome->hooks();

/**
 * 2. Static method.
 */
is_callable( 'Awesome::do_something' ); // true
is_callable( [ 'Awesome', 'do_something' ] ) ; // true
is_callable( [ Awesome::class, 'do_something' ] ); // true
is_callable( [ $awesome, 'do_something' ] ); // true

/**
 * 3. Method.
 */
is_callable( [ $awesome, 'do_something_more' ] ); // true

/**
 * 4. If the class has the __invoke method we can run the object as a function.
 */
is_callable( $awesome ); // true

/**
 * 5. Anonymous functions.
 */
is_callable( function () { // true

} );

I recommend deep dive into callable syntax and read more about it to automatically catch on to the callable syntax because hooks are based on the callbacks, like many other things in WordPress.

Hooks Setup for PhpStorm

Configurate PHP interpreter and included project paths

WordPress hooks are the main communication mechanism between the core, plugins, and themes, which allow influence to any code directly from your theme or plugin - WP Punk
  1. In the Settings/Preferences, go to PHP.
  2. Choose the PHP Interpreter.
  3. In the Include path field, specify the folder where WordPress is installed. This folder should contain the wp-admin and wp-includes subdirectories.

Configurate WordPress as Framework

WordPress hooks are the main communication mechanism between the core, plugins, and themes, which allow influence to any code directly from your theme or plugin - WP Punk
  1. In the Settings/Preferences, go to PHP | Frameworks.
  2. On the Frameworks page that opens, expand the WordPress node and select the Enable WordPress integration checkbox.
  3. In the WordPress installation path field, specify the folder where WordPress is installed. This folder should contain the wp-admin and wp-includes subdirectories.
  4. Click Apply to save it.

Result of PhpStorm Configurations

As a result, we’ve got two awesome features: autocomplete hooks which allow finding needed hook if you know approximately a name.

WordPress hooks are the main communication mechanism between the core, plugins, and themes, which allow influence to any code directly from your theme or plugin - WP Punk

And a quick hooks search. You can click on the h↑ icon and find all places where the hook uses. As a result, after move to the hook usage, you can find more context, hooks nearby, and hook documentation.

WordPress hooks are the main communication mechanism between the core, plugins, and themes, which allow influence to any code directly from your theme or plugin - WP Punk

How to put hooks?

Advising on this topic is quite difficult because each case is unique. But I’ll still try:

  1. Don’t skimp on hooks. The more hooks you leave, the easier it will be for other developers to add their code.
  2. Add a filter for “mystical” numbers, strings or arrays. For example:
    1. storage time of the cache (mystical number).
    2. request settings or CPT list (mystical array)
  3. Give as many additional arguments as possible. Don’t skimp on the data you have. Let the developers who use your hooks have everything they need, don’t be greedy.
  4. Allow changing the main data with apply_filters and mark the main actions in your code with do_action.

Let’s try to add hooks for a simple API request:

<?php

class API {
	const URL = 'https://random-data-api.com/api/code/random_code';

	public function get_data() {

		$response = wp_remote_request(
			self::URL,
			[
				'method'  => 'GET',
				'timeout' => 2,
			]
		);

		$body = wp_remote_retrieve_body( $response );

		if ( is_wp_error( $body ) ) {
			return [];
		}

		$body = json_decode( $body, true );

		if ( ! is_array( $body ) || empty( $body ) ) {
			return [];
		}

		return $body;
	}
}
<?php

class API {
	const URL = 'https://random-data-api.com/api/code/random_code';

	public function get_data() {

		do_action( 'hooks_api_before_request' );
		$response = wp_remote_request( self::URL, $this->get_request_arguments() );
		$body     = $this->get_response_body( $response );
		do_action( 'hooks_api_after_request', $response );

		return apply_filters( 'hooks_api_get_data', $body );
	}
	
	private function get_response_body( $response ) {
		if ( is_wp_error( $response ) ) {
			return apply_filters( 'hooks_api_fail_request', [], $response );
		}

		$body = json_decode( wp_remote_retrieve_body( $response ), true );

		if ( ! $this->is_valid_body( $body ) ) {
			return apply_filters( 'hooks_api_fail_request', [], $response );
		}
		
		return $body;
	}

	private function is_valid_body( $body ) {
		return apply_filters(
			'hooks_api_is_valid_body',
            is_array( $body ) && ! empty( $body ),
			$body
		);
	}

	private function get_request_arguments() {
		return apply_filters(
			'hooks_api_get_request_arguments',
			[
				'method'  => 'GET',
				'timeout' => 2,
			]
		);
	}
}
  • hooks_api_before_request – Allow doing something before request sending.
  • hooks_api_after_request – Allow doing something after request sending.
  • hooks_api_get_request_arguments – Allow modifying request arguments, e.g. timeout, headers, etc.
  • hooks_api_fail_request – Allow modify failed response, e.g. we allow to put dummy data instead.
  • hooks_api_is_valid_body – Allow validating the body.
  • hooks_api_get_data – Allow modifying the method result.

The next example is a shortcode that prints a table on the frontend:

<?php

add_shortcode( 'dummy_table', 'dummy_table' );
function dummy_table( $attributes ) {
	$api        = new API();
	$table_data = $api->get_data();

	echo '<div class="dummy-table-wrapper">';
	echo '<div class="dummy-table">';
	foreach( $table_data as $row ) {
		echo '<div class="dummy-table-row">';
		printf(
			'<div class="dummy-table-column">%s</div><div class="dummy-table-column">%s</div>',
			! empty( $row['npi'] ) ? esc_html( $row['npi'] ) : '',
			! empty( $row['imei'] ) ? esc_html( $row['imei'] ) : ''
		);
		echo '</div>';
	}
	echo '</div>';
	echo '</div>';
}
<?php

add_shortcode( 'dummy_table', 'dummy_table' );
function dummy_table( $attributes ) {

	$api             = new API();
	$table_data      = $api->get_data();
	$attributes      = apply_filters( 'dummy_table_attributes', $attributes );
	$table_data      = apply_filters( 'dummy_table_data', $table_data, $attributes );
	$wrapper_classes = apply_filters( 'dummy_table_wrapper_classes', [ 'dummy-table-wrapper' ], $table_data, $attributes );
	$table_classes   = apply_filters( 'dummy_table_classes', [ 'dummy-table' ], $table_data, $attributes );
	$row_classes     = [ 'dummy-table-row' ];

	do_action( 'dummy_table_before', $table_data, $attributes );

	printf(
		'<div class="%s">',
		implode( ' ', array_map( 'sanitize_html_class', $wrapper_classes ) )
	);
	do_action( 'dummy_table_after_wrapper_open', $table_data, $attributes );

	printf(
		'<div class="%s">',
		implode( ' ', array_map( 'sanitize_html_class', $table_classes ) )
	);

	foreach ( $table_data as $row ) {
		$row         = apply_filters( 'dummy_table_row', $row, $table_data, $attributes );
		$row_classes = apply_filters( 'dummy_table_row_classes', $row_classes, $row, $table_data, $attributes );
		printf(
			'<div class="%s">',
			implode( ' ', array_map( 'sanitize_html_class', $row_classes ) )
		);
		do_action( 'dummy_table_row_start', $row, $table_data, $attributes );
		printf(
			'<div class="dummy-table-column">%s</div><div class="dummy-table-column">%s</div>',
			! empty( $row['npi'] ) ? esc_html( $row['npi'] ) : '',
			! empty( $row['imei'] ) ? esc_html( $row['imei'] ) : ''
		);
		do_action( 'dummy_table_row_end', $row, $table_data, $attributes );
		echo '</div>';
	}

	echo '</div>';

	do_action( 'dummy_table_before_wrapper_close', $table_data, $attributes );
	echo '</div>';
	do_action( 'dummy_table_after', $table_data, $attributes );
}
  • dummy_table_attributes, dummy_table_data, dummy_table_row – Allow modifying main data.
  • dummy_table_wrapper_classes, dummy_table_classes, dummy_table_row_classes – Allow adding any custom classes.
  • dummy_table_before, dummy_table_after_wrapper_open, dummy_table_row_start, dummy_table_row_end, dummy_table_before_wrapper_close, dummy_table_after – Allow modifying content.

The Hooks naming Convention

The WordPress documentation doesn’t have recommendations about hooks naming, but it’s significant for WordPress products. I slightly prefer that a linter also checked the hooks’ names as with all coding standards. It doesn’t matter how your team will check hooks’ names, but I’m sure that you should do it.

For example, you can use Fully Qualified Class Name (FQCN) + method name + extra detail(optional). Let’s update our API class due to the hooks’ names convention:

<?php

namespace WPPunk\Hooks;

class API {
	const URL = 'https://random-data-api.com/api/code/random_code';

	public function get_data() {
		$body = $this->make_request();

		return apply_filters( 'wp_punk_hooks_api_get_data', $body );
	}

	private function make_request() {
		do_action( 'wp_punk_hooks_api_make_request_before' );
		$response = wp_remote_request( self::URL, $this->get_request_arguments() );
		$body     = $this->get_response_body( $response );
		do_action( 'wp_punk_hooks_api_make_request_after', $response );

		return $body;
	}

	private function get_response_body( $response ) {
		if ( is_wp_error( $response ) ) {
			return $this->fail_request( $response );
		}

		$body = json_decode( wp_remote_retrieve_body( $response ), true );

		if ( ! $this->is_valid_body( $body ) ) {
			return $this->fail_request( $response );
		}

		return $body;
	}

	private function fail_request( $response ) {
		return apply_filters( 'wp_punk_hooks_api_fail_request', [], $response );
	}

	private function is_valid_body( $body ) {
		return apply_filters(
			'wp_punk_hooks_api_is_valid_body',
			is_array( $body ) && ! empty( $body ),
			$body
		);
	}

	private function get_request_arguments() {
		return apply_filters(
			'wp_punk_hooks_api_get_request_arguments',
			[
				'method'  => 'GET',
				'timeout' => 2,
			]
		);
	}
}

As a result, we always know how to name a hook and won’t have any conflicts with other codebase parts. Sometimes the names have incorrect word-order or non-obviously names, but we have a clear logic for all hooks in your project, which you can automatize.

The WordPress documentation has one more interesting piece of information about naming dynamic hooks. Use curly brackets:

do_action( "{$new_status}_{$post->post_type}", $post->ID, $post );

instead of:

do_action( $new_status . '_' . $post->post_type, $post->ID, $post );

The reason is simplifying work for code analysis tools, such as quick search in PhpStorm, hooks generator for documentation. etc.

That’s all. Happy hooking!

Leave a Reply

Your email address will not be published. Required fields are marked *

Subscribe to news. I promise not to spam :)
Follow me, don't be shy