1

I'm trying to create a custom widget to display contributing authors for the current category being viewed on a WordPress site. I have the following PHP code:

<?php
if (is_category()) {
?>
// Grab posts from the current category
<?php
    $current_category = single_cat_title(“”, false);
    $author_array     = array();
    $args             = array(
        ‘numberposts’ => -1,
        ‘category_name’ => $current_category,
        ‘orderby’ => ‘author’,
        ‘order’ => ‘ASC’
    );
    // Get the posts in the array and create an array of the authors
    $cat_posts        = get_posts($args);
    foreach ($cat_posts as $cat_post):
        if (!in_array($cat_post->post_author, $author_array)) {
            $author_array[] = $cat_post->post_author;
        }
    endforeach;
    // Get the author information and build the output
    foreach ($author_array as $author):
        $auth      = get_userdata($author)->display_name;
        $auth_link = get_userdata($author)->user_login;
        echo "<a href='/author/'" . $auth_link . "'>" . $auth . "</a>" . '<br \/>';
    endforeach;

    // Testing to make sure the current category is being pulled correctly  
    echo $current_category;
?>
<?php
}
?>

It's displaying authors from the site, but they're not changing from category to category - the same three are displayed in each section. The test statement at the end of the code block is working as expected - the displayed category in the widget matches the URI category.

Any thoughts on where I went wrong in creating the author array?

2 Answers 2

3

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

2
  • This is, by far, the most helpful answer I've ever gotten on any of the SE network. Thanks. If I want to add a link to the author's posts in the widget, I just need to add that to they query in functions.php, right?
    – Brian
    Jul 6, 2016 at 19:23
  • 1
    My pleasure, glad that you found this helpful. The $user object in your last foreach loop contain about everything you need, all you need to do is to access that object's properties. Inside that foreach loop, do var_dump( $user );, this will dump the complete object which will give you an idea of what is available. For ease, the user's link is avilable via $user->user_url. If you need to display any other info, you can do it inside your foreach loop Jul 7, 2016 at 4:23
0
<?php
if( is_category() ) {
  global $wp_query;
  $term_obj = $wp_query->get_queried_object();
  $author_array     = array();
  $args             = array(
      'posts_per_page'  => -1,
      'category'        => $term_obj->term_id,
      'orderby'         => 'author',
      'order'           => 'ASC'
  ); 

  // Get the posts in the array and create an array of the authors
  $cat_posts  = get_posts( $args );
  foreach ( $cat_posts as $cat_post ):
      if ( ! in_array( $cat_post->post_author, $author_array ) ) {
          $author_array[] = $cat_post->post_author;
      }
  endforeach;

  // Get the author information and build the output
  foreach ( $author_array as $author ):
      $auth      = get_userdata($author)->display_name;
      $auth_link = get_userdata($author)->user_login;
      printf( '<a href="/author/%s">%s</a><br />', $auth_link, $auth );
  endforeach;
}
?>

Explaining the Code here:

  1. Checking category archive page by is_category() conditional tag
  2. Assign global variable global $wp_query;
  3. Now getting the queried object by $wp_query->get_queried_object() function
  4. Creating the args array. I am using category ID ($term_obj->term_id) not category name.
  5. Fetching the all posts of current category by get_posts() function
  6. Now creating the unique authors array.
  7. Lastly displaying the authors list by printf() function.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.