Nested Hooks with do_action for performance reasons – safe/necessary?

This is a good use case for either Transients or the Object Cache. The choice of which to use depends on whether you need to perform this query/check on every single request, or whether it only needs to be performed once every hour/day/week etc.

For both options would would start by creating a separate function that performs the check and returns the number of results. You would then use this function inside each hook callback to determine the output/action performed (as you put it).

function wpse_344520_count_user_posts() {
    $user_id = get_current_user_id();
    $query   = new WP_Query(
        [
                // etc.
        ]
    );

    $count = $query->found_posts;

    return $count;
}

add_action(
    'manage_users_columns',
    function() {
        $count = wpse_344520_count_user_posts();

        if ( $count > 0 ) {
            // Do something.
        }
    }
);

add_action(
    'admin_head',
    function() {
        $count = wpse_344520_count_user_posts();

        if ( $count > 0 ) {
            // Do something.
        }
    }
);

// etc.

The trick is that inside this function you should check if the query has already been performed, and return the existing result if it exists. You would use transients or the object cache as the storage mechanism, depending on your requirements.

The object cache verson would look like this:

function wpse_344520_count_user_posts() {
    // Check if we've cached a result.
    $count = wp_cache_get( 'wpse_344520_count', $user_id );

    // If we have...
    if ( false !== $count ) { // Must be strict comparison so we can store 0 if necessary.
        // ...return it.
        return $count;
    } 

    // Otherwise perform the query...
    $user_id = get_current_user_id();

    $query = new WP_Query(
        [
            // etc.
        ]
    );

    $count = $query->found_posts;

    // ..cache the result... 
    wp_cache_set( 'wpse_344520_count', $count, $user_id );

    // ...and return it.
    return $count;
}

Now the query will only run once, and subsequent calls to wpse_344520_count_user_posts() will use the cached result.

The transient method is similar, but saves its result in the database, which lets you cache the result over multiple separate requests. Use this method if the result being accurate up to the second isn’t necessary:

function wpse_344520_count_user_posts() {
    $user_id = get_current_user_id();

    // Check if we've cached a result.
    $count = get_transient( 'wpse_344520_count_' . $user_id );

    // If we have...
    if ( $count !== false ) { // Must be strict comparison so we can store 0 if necessary.
        // ...return it.
        return $count;
    } 

    // Otherwise perform the query...
    $query = new WP_Query(
        [
            // etc.
        ]
    );

    $count = $query->found_posts;

    // ..and cache the result... 
    set_transient( 'wpse_344520_count_' . $user_id, $count, HOUR_IN_SECONDS );

    // ...then return it.
    return $count;
}

As you can see, it’s very similar. The only differences are:

  • We use get_transient() and set_transient() instead of wp_cache_get() and wp_cache_set().
  • We use the user ID as part of the transient name to make sure that the value is stored separately for each user. This is necessary because transients do not support “groups” the way the object cache does.
  • We pass a number of seconds that the value should be cached to set_transient(). In my example I’ve used one of the built in constants to set the time to one hour.