add_filter()

Attaches a callback that receives a value and must return one — the signature, the return contract, and how often filters really run.

Syntaxadd_filter( string $hook_name, callable $callback, int $priority = 10, int $accepted_args = 1 ): true

add_filter() registers a callback that receives a value and returns a replacement. The return value goes to the next callback in line, and after the last one it goes back to whatever called apply_filters(). The signature is identical to add_action(), which is unsurprising since add_action() is a wrapper around this function.

Always return something

A filter callback that returns nothing returns null, and null is what core receives. This is the most common filter bug by a wide margin, usually caused by an early bare return; guard or by a callback written as if it were an action. Every exit path must return a value, including the "not interested" path, which returns the value it was given, unchanged.

Return the right type

Core rarely coerces what you hand back. excerpt_length happens to be cast with (int), but most filters are not defended, so returning a string where an array was expected produces a fatal error far away from your code. Match the type documented on the apply_filters() call in the Code Reference.

Filters run more often than you think

the_content runs for every post in every loop on the page, plus feeds, REST responses and block-based widgets — not once per page view. Anything expensive inside multiplies, and side effects such as writing an option or sending mail will happen repeatedly.

Example

Example · php
<?php
// Priority 999 so this runs after a theme's own excerpt_length filter (usually 10).
add_filter( 'excerpt_length', 'acme_excerpt_length', 999 );
function acme_excerpt_length( $length ) {
	if ( is_search() ) {
		return $length; // Not interested — return the value, never a bare `return;`.
	}

	return 25; // int: core passes this to wp_trim_words() as a word count.
}

// the_title passes two arguments: apply_filters( 'the_title', $post_title, $post_id ).
// accepted_args must be 2 to receive the second one.
add_filter( 'the_title', 'acme_flag_drafts', 10, 2 );
function acme_flag_drafts( $title, $post_id ) {
	if ( ! is_admin() || 'draft' !== get_post_status( $post_id ) ) {
		return $title; // Every path returns the value.
	}

	return $title . ' (draft)';
}

When to use it

  • Shorten or lengthen excerpts site-wide without touching a template.
  • Append a disclaimer to the content of posts in one category.
  • Change the number of posts a REST endpoint returns per page.
  • Rewrite an upload's filename before WordPress stores the attachment.

More examples

The null-return bug

The failure mode to recognise on sight: titles or content disappearing on some templates but not others, with no PHP error. If a filter callback contains a bare `return;` or an `echo`, that is your bug. Actions echo, filters return.

Example · php
// BROKEN: on a page, this returns null and the title vanishes site-wide.
add_filter( 'the_title', 'acme_prefix_title' );
function acme_prefix_title( $title ) {
	if ( ! is_single() ) {
		return; // returns null — core now has null where a string was.
	}

	echo 'Post: ' . $title; // echoing from a filter also prints in the wrong place
}

// FIXED.
add_filter( 'the_title', 'acme_prefix_title' );
function acme_prefix_title( $title ) {
	if ( ! is_single() ) {
		return $title;
	}

	return 'Post: ' . $title;
}

__return_true and the closure it replaces

Reach for these whenever a filter just needs a fixed value and the arguments are irrelevant. Beyond being shorter, the named helpers are removable by anyone, whereas a closure that returns false is stuck on that hook for the life of the request.

Example · php
// These two lines do exactly the same thing. The first is the shorthand.
add_filter( 'wp_is_application_passwords_available', '__return_false' );
add_filter( 'wp_is_application_passwords_available', function () {
	return false;
} );

// WordPress ships one helper per constant return value:
// __return_true, __return_false, __return_zero, __return_null,
// __return_empty_array, __return_empty_string

// Prefer the named function: a closure cannot be removed later, but a site owner
// (or a later plugin) can always undo this:
remove_filter( 'wp_is_application_passwords_available', '__return_false' );

Guarding an expensive filter

The pattern for any the_content filter that queries the database or calls an API. The context checks stop it firing in feeds and widget loops; the transient stops the work repeating; the static flag covers themes that render the content more than once on a page.

Example · php
add_filter( 'the_content', 'acme_append_related', 20 ); // after wpautop (10) and do_shortcode (11)
function acme_append_related( $content ) {
	// the_content runs per post, per loop, and in feeds and REST responses.
	if ( ! is_singular( 'post' ) || ! in_the_loop() || ! is_main_query() || is_feed() ) {
		return $content;
	}

	static $rendered = false;
	if ( $rendered ) {          // belt and braces against a template calling the_content() twice
		return $content;
	}
	$rendered = true;

	$related = get_transient( 'acme_related_' . get_the_ID() );
	if ( false === $related ) {
		$related = acme_build_related_html( get_the_ID() ); // the expensive part
		set_transient( 'acme_related_' . get_the_ID(), $related, HOUR_IN_SECONDS );
	}

	return $content . $related;
}

Discussion

  • Be the first to comment on this lesson.