List authors with posts in a category

This can become a quite an expensive operation which can seriously damage page load time. At this stage, your code is quite expensive. Lets look a better way to tackle this issue.

What we need to do is to minimize the time spend in db, and to do this, we will only get the info we need, that is the post_author property of the post object. As we now, there is no native way to just get the post_author property from the db with WP_Query, we only have the possibility to get the complete object or just the post ID‘s. To alter the behavior (and the generated SQL), we will make use of the posts_fields filter in which we can command the SQL query to just return the post_author field, which will save us a ton in time spend in the db as we only get what we need.

Secondly, to improve performance, we will tell WP_Query to not cache any post data or post term and post meta data. As we will not be needing any meta data or post term data, we can simply just ask WP_Query to not query these and cache them for later use, this also saves us a lot of extra time spend in db, time to add this data in cache, and we will also save on db queries.

Lets put this all in code: (NOTE: All code is untested and require PHP 5.4+)

posts_fields FILTER

add_filter( 'posts_fields', function ( $fields, \WP_Query $q ) use ( &$wpdb )
{
    remove_filter( current_filter(), __FUNCTION__ );

    // Only target a query where the new wpse_post_author parameter is set to true
    if ( true === $q->get( 'wpse_post_author' ) ) {
        // Only get the post_author column
        $fields = "
            $wpdb->posts.post_author
        ";
    }

    return $fields;
}, 10, 2);

You’ll notice that we have a custom trigger called wpse_post_author. Whenever we pass a value of true to that custom trigger, our filter will fire.

get_posts() QUERY

By default, get_posts() passes suppress_filters=true to WP_Query as to avoid filters acting on get_posts(), so in order to get our filter to work, we need to set that back to true.

Just another note, to get the current category reliably, use $GLOBALS['wp_the_query']->get_queried_object() and $GLOBALS['wp_the_query']->get_queried_object_id() for the category ID

if ( is_category() ) {
    $current_category = get_term( $GLOBALS['wp_the_query']->get_queried_object() );

    $args = [
        'wpse_post_author'       => true, // To trigger our filter
        'posts_per_page'         => -1,
        'orderby'                => 'author',
        'order'                  => 'ASC',
        'suppress_filters'       => false, // Allow filters to alter query
        'cache_results'          => false, // Do not cache posts
        'update_post_meta_cache' => false, // Do not cache custom field data
        'update_post_term_cache' => false, // Do not cache post terms
        'tax_query'              => [
            [
                'taxonomy'         => $current_category->taxonomy,
                'terms'            => $current_category->term_id,
                'include_children' => true
            ]
        ]
    ];
    $posts_array = get_posts( $args );

    if ( $posts_array ) {
        // Get all the post authors from the posts
        $post_author_ids = wp_list_pluck( $posts_array, 'post_author' );

        // Get a unique array of ids
        $post_author_ids = array_unique( $post_author_ids );

        // NOW WE CAN DO SOMETHING WITH THE ID'S, SEE BELOW TO INCLUDE HERE
    }
}

As you can see, I used a tax_query, this is personal preference due to the flexibility of it, and also, you can reuse the code on any term page with no need to modify it. In this case, you only need to change the is_category() condition.

In my tax_query I have set include_children to true, which means that WP_Query will get posts from the current category and the posts that belongs to the child categories of the category being viewed. This is default behavior for all hierarchical term pages. If you really just need authors from the category being viewed, set include_children to false

QUERYING THE AUTHORS

If you do a var_dump( $post_author_ids ), you will see that you have an array of post author ids. Now, instead of looping through each and every ID, you can just pass this array of id’s to WP_User_Query, and then loop through the results from that query.

$user_args = [
    'include' => $post_author_ids
];
$user_query = new \WP_User_Query( $user_args );
var_dump( $user_query->results ); // For debugging purposes

if ( $user_query->results ) {
    foreach ( $user_query->results as $user ) {
        echo $user->display_name;
    }
}

We can take this even further and save everything in a transient, which will have us ending up only 2 db queries in =/- 0.002s.

THE TRANSIENT

To set a unique transient name, we will use the term object to create a unique name

if ( is_category() ) {
    $current_category = get_term( $GLOBALS['wp_the_query']->get_queried_object() );
    $transient_name="wpse231557_" . md5( json_encode( $current_category ) );

    // Check if transient is set
    if ( false === ( $user_query = get_transient( $transient_name ) ) ) {

        // Our code above

        // Set the transient for 3 days, adjust as needed
        set_transient( $transient_name, $user_query, 72 * HOUR_IN_SECONDS );
    }

    // Run your foreach loop to display users
}

FLUSHING THE TRANSIENT

We can flush the transient whenever a new post is publish or when a post is edited, deleted, undeleted, etc. For this we can use the transition_post_status hook. You can also adjust it to fire only when certain things happens, like only when a new post is published. Anyways, here is the hook which will fire when anything happens to the post

add_action( 'transition_post_status', function () use ( &$wpdb )
{
    $wpdb->query( "DELETE FROM $wpdb->options WHERE `option_name` LIKE ('_transient%_wpse231557_%')" );
    $wpdb->query( "DELETE FROM $wpdb->options WHERE `option_name` LIKE ('_transient_timeout%_wpse231557_%')" );
});

ALL TOGETHER NOW!!!

In a functions type file

add_filter( 'posts_fields', function ( $fields, \WP_Query $q ) use ( &$wpdb )
{
    remove_filter( current_filter(), __FUNCTION__ );

    // Only target a query where the new wpse_post_author parameter is set to true
    if ( true === $q->get( 'wpse_post_author' ) ) {
        // Only get the post_author column
        $fields = "
            $wpdb->posts.post_author
        ";
    }

    return $fields;
}, 10, 2);

add_action( 'transition_post_status', function () use ( &$wpdb )
{
    $wpdb->query( "DELETE FROM $wpdb->options WHERE `option_name` LIKE ('_transient%_wpse231557_%')" );
    $wpdb->query( "DELETE FROM $wpdb->options WHERE `option_name` LIKE ('_transient_timeout%_wpse231557_%')" );
});

In your widget

if ( is_category() ) {
    $current_category = get_term( $GLOBALS['wp_the_query']->get_queried_object() );
    $transient_name="wpse231557_" . md5( json_encode( $current_category ) );

    // Check if transient is set
    if ( false === ( $user_query = get_transient( $transient_name ) ) ) {

        $args = [
            'wpse_post_author'       => true, // To trigger our filter
            'posts_per_page'         => -1,
            'orderby'                => 'author',
            'order'                  => 'ASC',
            'suppress_filters'       => false, // Allow filters to alter query
            'cache_results'          => false, // Do not cache posts
            'update_post_meta_cache' => false, // Do not cache custom field data
            'update_post_term_cache' => false, // Do not cache post terms
            'tax_query'              => [
                [
                    'taxonomy'         => $current_category->taxonomy,
                    'terms'            => $current_category->term_id,
                    'include_children' => true
                ]
            ]
        ];
        $posts_array = get_posts( $args );

        $user_query = false;

        if ( $posts_array ) {
            // Get all the post authors from the posts
            $post_author_ids = wp_list_pluck( $posts_array, 'post_author' );

            // Get a unique array of ids
            $post_author_ids = array_unique( $post_author_ids );

            $user_args = [
                'include' => $post_author_ids
            ];
            $user_query = new \WP_User_Query( $user_args );
        }

        // Set the transient for 3 days, adjust as needed
        set_transient( $transient_name, $user_query, 72 * HOUR_IN_SECONDS );
   }

    if (    false !== $user_query
         && $user_query->results 
    ) {
        foreach ( $user_query->results as $user ) {
            echo $user->display_name;
        }
    }
}

EDIT

All code is now tested and works as expected

Leave a Comment