8 Useful Snippets For Working With WordPress Hooks

Add your hook before or after another hook

Sometimes you need to ensure your function will get executed before or after another function that’s attached to the same hook (e.g. to fix a plugin conflict). The most reliable way to do this is to use has_action() to get the priority of the other hook and make your function’s priority one higher/lower:

function add_filter_after($tag, $callback, $target_callback, $accepted_args = 1) {
	$priority = has_action($tag, $target_callback);
	$priority = ($priority !== false) ? $priority + 1 : 10;
	add_filter($tag, $callback, $priority, $accepted_args);
}

function add_filter_before($tag, $callback, $target_callback, $accepted_args = 1) {
	$priority = has_action($tag, $target_callback);
	$priority = ($priority !== false) ? $priority - 1 : 10;
	return add_filter($tag, $callback, $priority, $accepted_args);
}

//Example: Add a the_content filter that will 
//run before the built-in wpautop filter.
add_filter_before('the_content', 'my_callback', 'wpautop');

Pass additional arguments to filters and action callbacks

By default, WordPress offers no way to pass additional data to hook callbacks. One interesting way to get around this limitation is to create a helper class to hold the name of your callback and the data you want to pass to it. Then add a method that will execute your function (with the extra arguments, of course) and register that as the hook callback. Essentially, implementing partial application in PHP.

class PartialHookCallback {
	private $callback;
	private $origArgs;

	public function __construct($callback, $origArgs) {
		$this->callback = $callback;
		$this->origArgs = $origArgs;
	}

	public static function make() {
		$origArgs = func_get_args();
		$callback = array_shift($origArgs);
		$partialCallback = new self($callback, $origArgs);
		return array($partialCallback, 'invoke');
	}

	public function invoke() {
		$allArgs = array_merge($this->origArgs, func_get_args());
		return call_user_func_array($this->callback, $allArgs);
	}
}

Usage example:

function my_sample_filter($message, $content) {
	return $content . '<p>' . $message . '</p>';
}

$addHelloWorld = PartialHookCallback::make(
	'my_sample_filter', 
	'Hello world!'
);
add_filter('the_content', $addHelloWorld);

$addAnotherMessage = PartialHookCallback::make(
	'my_sample_filter', 
	'Same function, different message.'
);
add_filter('the_content', $addAnotherMessage);

Alternative approaches:

  • Create a class that contains your filter callbacks, then pass the data through instance variables.
  • Make the variables global so that you can access them from anywhere (not recommended).

An easier way to create filters that return a fixed value

WordPress already has a number of built-in __return_* functions for when you need a callback that simply returns a predefined value. However, they only cover the most common cases – returning true, false, null and so on. The following function makes it easy to return anything you want:

function add_filter_return($tag, $return_value, $priority = 10) {
	$handler = new FilterReturnHandler($return_value);
	add_filter($tag, array($handler, 'invoke'), $priority, 0);
}

class FilterReturnHandler {
	private $returnValue;

	public function __construct($returnValue) {
		$this->returnValue = $returnValue;
	}

	public function invoke() {
		return $this->returnValue;
	}
}

As an example, here’s how you could use this function to enable the menu_order hook that lets you re-order admin menus:

//Enable custom admin menu order.
add_filter_return('custom_menu_order', true);

Source: Adapted from the Advanced Hooks API plugin.

Run a filter or action exactly once, or a specific number of times

Some filters and actions can get executed many times per page. If you want to do something just the first time a hook is run, try this:

function sample_callback() {
	static $times_run = 0;
	if ( $times_run++ < 1 ) {
		//The actual hook code goes here.
	}
}
add_action('hook_that_gets_called_often', 'sample_callback');

Or if you need to process the first N calls, just change “1” to a different number.

Alternatively, you could use the did_action() function to determine how many times a particular hook has been executed:

function pippin_sample_function() {
	if(did_action('my_action') === 1) {
		// this code only runs the first time the "my_action" hook is fired
	}
}
add_action('my_action', 'pippin_sample_function');

Source: Detect when an action has run with did_action from PippinsPlugins.com

The downside of this approach is that did_action() returns the total number of times a hook has been executed, not the number of times your callback function has run. So if something triggers ‘my_action’ before you register your callback, did_action() will return a value above 1 and your code will never get executed.

Prevent hook recursion

Sometimes your hook callback can inadvertently trigger the same hook again, leading to infinite recursion. For example, if you create or update a post inside a ‘save_post’ action callback, it will trigger another ‘save_post’ action, which will run your function again, which will in turn trigger yet another ‘save_post’ action, which will run your function… and so on, until PHP runs out of stack space and your plugin crashes.

Thankfully, this is easy to avoid. Simply create a static flag variable that indicates whether your hook is running and bail out early if it’s true. Example:

function non_recursive_action($post_id, $post) {
	static $running = false;
	if ( !$running ) {
		$running = true;
		//...Code that can trigger more save_post actions...
		$running = false;
	}
}
add_action('save_post', 'non_recursive_action', 10, 2);

Beware: Due to a known bug in WordPress, calling a hook recursively can cause WP to skip executing some of the callbacks attached to that hook.

List page-specific admin hooks

WordPress has a number of useful contextual actions like admin_print_scripts-$page, load-$page and so on. You can use them to create code that only runs on specific admin pages. However, figuring out the $page part of the action name can be difficult since it’s usually not documented anywhere. So here’s a great snippet that will list the current page’s contextual hooks and screen variables in the “Help” panel:

add_action( 'contextual_help', 'wptuts_screen_help', 10, 3 );
function wptuts_screen_help( $contextual_help, $screen_id, $screen ) {
    // The add_help_tab function for screen was introduced in WordPress 3.3.
    if ( ! method_exists( $screen, 'add_help_tab' ) )
        return $contextual_help;
    global $hook_suffix;
    // List screen properties
    $variables = '<ul style="width:50%;float:left;"> <strong>Screen variables </strong>'
        . sprintf( '<li> Screen id : %s</li>', $screen_id )
        . sprintf( '<li> Screen base : %s</li>', $screen->base )
        . sprintf( '<li>Parent base : %s</li>', $screen->parent_base )
        . sprintf( '<li> Parent file : %s</li>', $screen->parent_file )
        . sprintf( '<li> Hook suffix : %s</li>', $hook_suffix )
        . '</ul>';
    // Append global $hook_suffix to the hook stems
    $hooks = array(
        "load-$hook_suffix",
        "admin_print_styles-$hook_suffix",
        "admin_print_scripts-$hook_suffix",
        "admin_head-$hook_suffix",
        "admin_footer-$hook_suffix"
    );
    // If add_meta_boxes or add_meta_boxes_{screen_id} is used, list these too
    if ( did_action( 'add_meta_boxes_' . $screen_id ) )
        $hooks[] = 'add_meta_boxes_' . $screen_id;
    if ( did_action( 'add_meta_boxes' ) )
        $hooks[] = 'add_meta_boxes';
    // Get List HTML for the hooks
    $hooks = '<ul style="width:50%;float:left;"> <strong>Hooks </strong> <li>' . implode( '</li><li>', $hooks ) . '</li></ul>';
    // Combine $variables list with $hooks list.
    $help_content = $variables . $hooks;
    // Add help panel
    $screen->add_help_tab( array(
        'id'      => 'wptuts-screen-help',
        'title'   => 'Screen Information',
        'content' => $help_content,
    ));
    return $contextual_help;
}

Source: Quick Tip: Get the Current Screen’s Hooks by Stephen Harris

If you found this snippet useful, check out the Current Admin Info plugin by Franz Kaiser as well. It’s based on the same source and displays not only hooks and screen info, but also page-specific WP globals like $hooks_suffix, $parent_file and others.

Display all hooks executed on a page

And last but not least, here’s a great little plugin that’s useful both for debugging hook problems and general WordPress development. Just add “?instrument=hooks” to the URL of any WP page and it will output a list of all hooks executed on that page in the footer.

<?php
/*
Plugin Name: Instrument Hooks for WordPress
Description: Instruments Hooks for a Page. Outputs during the Shutdown Hook.
Version: 0.1
Author: Mike Schinkel
Author URI: http://mikeschinkel.com
*/

if (isset($_GET['instrument']) && $_GET['instrument']=='hooks') {

	add_action('shutdown','instrument_hooks');
	function instrument_hooks() {
		global $wpdb;
		$hooks = $wpdb->get_results("SELECT * FROM wp_hook_list ORDER BY first_call");
		$html = array();
		$html[] = '<style>#instrumented-hook-list table,#instrumented-hook-list th,#instrumented-hook-list td {border:1px solid gray;padding:2px 5px;}</style>
<div align="center" id="instrumented-hook-list">
	<table>
		<tr>
		<th>First Call</th>
		<th>Hook Name</th>
		<th>Hook Type</th>
		<th>Arg Count</th>
		<th>Called By</th>
		<th>Line #</th>
		<th>File Name</th>
		</tr>';
		foreach($hooks as $hook) {
			$html[] = "<tr>
			<td>{$hook->first_call}</td>
			<td>{$hook->hook_name}</td>
			<td>{$hook->hook_type}</td>
			<td>{$hook->arg_count}</td>
			<td>{$hook->called_by}</td>
			<td>{$hook->line_num}</td>
			<td>{$hook->file_name}</td>
			</tr>";
		}
		$html[] = '</table></div>';
		echo implode("\n",$html);
	}

	add_action('all','record_hook_usage');
	function record_hook_usage($hook){
		global $wpdb;
		static $in_hook = false;
		static $first_call = 1;
		static $doc_root;
		$callstack = debug_backtrace();
		if (!$in_hook) {
			$in_hook = true;
			if ($first_call==1) {
				$doc_root = $_SERVER['DOCUMENT_ROOT'];
				$results = $wpdb->get_results("SHOW TABLE STATUS LIKE 'wp_hook_list'");
				if (count($results)==1) {
					$wpdb->query("TRUNCATE TABLE wp_hook_list");
				} else {
					$wpdb->query("CREATE TABLE wp_hook_list (
					called_by varchar(96) NOT NULL,
					hook_name varchar(96) NOT NULL,
					hook_type varchar(15) NOT NULL,
					first_call int(11) NOT NULL,
					arg_count tinyint(4) NOT NULL,
					file_name varchar(128) NOT NULL,
					line_num smallint NOT NULL,
					PRIMARY KEY (first_call,hook_name))"
					);
				}
			}
			$args = func_get_args();
			$arg_count = count($args)-1;
			$hook_type = str_replace('do_','',
				str_replace('apply_filters','filter',
					str_replace('_ref_array','[]',
						$callstack[3]['function'])));
			$file_name = addslashes(str_replace($doc_root,'',$callstack[3]['file']));
			$line_num = $callstack[3]['line'];
			$called_by = $callstack[4]['function'];
			$wpdb->query("INSERT wp_hook_list
				(first_call,called_by,hook_name,hook_type,arg_count,file_name,line_num)
				VALUES ($first_call,'$called_by()','$hook','$hook_type',$arg_count,'$file_name',$line_num)");
			$first_call++;
			$in_hook = false;
		}
	}
}

Source: Instrument Hooks for WordPress by Mike Schinkel. See also the related WPSE answer.

Related posts :

2 Responses to “8 Useful Snippets For Working With WordPress Hooks”

  1. David says:

    Another simple way to avoid hook-recursion or run the callback only once is to remove the callback by itself:

    function call_me_once( $arg ) {
        remove_filter( 'gettext', __FUNCTION__ );
        //handle $arg
    
        return $arg;
    }
    add_filter( 'gettext', 'call_me_once' );
    
  2. Jānis Elsts says:

    Yep, that also works.

    (I added syntax highlighting to your comment.)

Leave a Reply