Customize WordPress’ get_posts with a filter
Published: – Leave a comment
For a specific condition I needed to customize the result set of WordPress’ get_posts
function. Since I struggled a bit with it, I want to make sure that never happens again with this post.
What I want to achieve
Since I needed to find all posts with certain post content (here, posts with a specific reusable block), instead of looping through all posts via PHP a custom query would be much more efficient and performant.
The custom WHERE clause
Adjusting the WHERE
clause in the first place was a bit of a pain, since I wasn’t familiar with escaping it correctly as a LIKE
condition. After a steep learning curve I came up with the following method:
/**
* Check the post content for a reusable block with a specific ID.
*
* @param string $where The current WHERE string
* @return string The updated WHERE string
*/
public static function check_post_content_for_reusable_block( string $where ): string {
global $wpdb;
$extra = $wpdb->esc_like( '<!-- wp:block {"ref":' . (int) self::$reusable_post_id );
return $where . ' AND ' . $wpdb->posts . '.post_content LIKE \'%' . $extra . '%\'';
}
Code language: PHP (php)
In short: I check for the string <!-- wp:block {"ref":
inside the post content, followed by the actual ID if the reusable block.
Where the struggle began
Since I already knew the posts_where
filter should allow me to exactly customize the query of my get_posts
if I add a filter exactly before it and remove it directly afterwards. This looked like this:
add_filter( 'posts_where', [ self::class, 'check_post_content_for_reusable_block' ] );
self::$reusable_post_id = $post_id;
// get up to 5 post IDs, which contain this reusable block
$matching_posts_attributes = [
'post_type' => [
'page',
'post',
],
];
$matching_posts = get_posts( $matching_posts_attributes );
remove_filter( 'posts_where', [ self::class, 'check_post_content_for_reusable_block' ] );
Code language: PHP (php)
Basically, this should be it, but it didn’t work. I got more results that I wanted and not only the ones with the desired reusable block. Debugging it further it appeared to ignore my filter entirely.
Lesson learned
After roughly an hour of debugging, changing things here and there, I finally found the solution – of course in the documentation of get_posts
. By default, it uses the attribute suppress_filters
equal true
, which means that it ignores query filters, including my used posts_where
. So adding this to the attributes, it finally worked fine:
add_filter( 'posts_where', [ self::class, 'check_post_content_for_reusable_block' ] );
self::$reusable_post_id = $post_id;
// get up to 5 post IDs, which contain this reusable block
$matching_posts_attributes = [
'post_type' => [
'page',
'post',
],
'suppress_filters' => false,
];
$matching_posts = get_posts( $matching_posts_attributes );
remove_filter( 'posts_where', [ self::class, 'check_post_content_for_reusable_block' ] );
Code language: PHP (php)
Sometimes, reading more carefully will safe you much time.
Bonus: more performance
Depending on what you’re trying to achieve, you can highly improve performance. In my case, I just needed the post IDs (since I changed the post meta afterwards of the results). So returning only 'fields' => 'ids'
doesn’t generate a whole WP_Post
object for every result but just returns a list of IDs.
So especially if you’re working with a potential large result set, make sure to gather as less data as possible.