How to get posts from two categories with WP_Query?

I’m using WP_Query to create ‘latest posts’ & ‘popular posts’ sections on my home page. I’m trying to just pull 5 posts from 2 categories (9 & 11) but it will only show posts from cat 9.

Here’s the php I’m using for ‘latest posts’-

Read More
<ul>
        <?php 

        $cat = array(9,11);
        $showposts = 5;
        $paged = (get_query_var('paged')) ? get_query_var('paged') : 1;
        $args=array(
            'category__in' => $cat, 
            'showposts' => $showposts,
            'paged' => $paged,
            'orderby' => 'post_date',
            'order' => 'DESC',
            'post_status' => 'publish',
           );

        $the_query = new WP_Query ( $args ); //the query


        $i = 0;while ($the_query->have_posts() ) : $the_query->the_post(); //start the loop
        ?>

        <?php
        if($i==0){ //Sets the output for the top post
        ?>  
            <li class="first-news">
                <div class="post-thumbnail"><a href="<?php the_permalink(); ?>"><?php the_post_thumbnail(350,187); ?></a></div>
                <h2 class="post-box-title"><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h2>
                <p class="post-meta"><span class="tie-date"><?php the_time('F jS, Y') ?></span></p>
                <div class="entry"><?php the_excerpt(); ?></div>
                <div><a class="more-link" href="<?php the_permalink(); ?>">Read More »</a></div>

            </li>


        <?php
            $i++;
             } else { ?>

                <li class="other-news rar">
                <div class="post-thumbnail"><a href="<?php the_permalink(); ?>"><?php the_post_thumbnail(145,93); ?></a></div>
                <h3 class="post-box-title"><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h3>
                <p class="post-meta"><span class="tie-date"><?php the_time('F jS, Y') ?></span></p>
           </li>

        <?php } endwhile; //end of the loop ?>
        <?php wp_reset_postdata(); // reset the query ?>
        </ul> 

Any suggestions?

Related posts

3 comments

  1. In my experience using 'posts_*' filters ('posts_request', 'posts_where'…) in combination with str_replace / preg_replace is unreliable and unflexible:

    Unreliable because if another filter modify uses one of that filters, in better case one gets unexpected results, in worst cases one gets SQL errors.

    Unflexible because changing an argument, e.g. ‘include_children’ for categories, or reuse the code for e.g. 3 terms instead of 2 need a lot of works.

    Moreover, adapt code to be musltisite compatibe need to edit SQL manually.

    So, sometimes, evevn if not the best solution regarding performance, a more canonical approach is the best and more flexible one.

    And performance can be improced with some caching tricks…

    My propose:

    1. write a function taht make use of usort to order posts coming from different queries (e.g. one per term)
    2. write a function that on first run run separate queries, merge results, order them, cache them and return them. On subsequent request simply return the cached results
    3. handle caching invalidation when needed

    Code

    First the function that order posts:

    function my_date_terms_posts_sort( Array $posts, $order = 'DESC' ) {
      if ( ! empty( $posts ) ) {
        usort( $posts, function( WP_Post $a, WP_Post $b ) use ( $order ) {
          $at = (int) mysql2date( 'U', $a->post_date );
          $bt = (int) mysql2date( 'U', $b->post_date );
          $orders = strtoupper($order) === 'ASC' ? array( 1, -1 ) : array( -1, 1 );
          return $at === $bt ? 0 : ( $at > $bt ) ? $orders[0] : $orders[1];
        } );
      }
      return $posts;
    }
    

    Then the function that get non cached results:

    function my_fresh_terms_get_posts( $args, $terms, $tax_query_args = NULL ) {
      $posts = array();
      // we need to know at least the taxonomy
      if ( ! is_array( $tax_query_args ) || ! isset( $tax_query_args['taxonomy'] ) ) return;
      // handle base tax_query
      $base_tax_query = isset( $args['tax_query'] ) ? $args['tax_query'] : array();
      // run a query for each term
      foreach ( $terms as $term ) {
        $term_tax_query = wp_parse_args( array(
          'terms' => array( $term ),
          'field' => is_numeric( $term ) ? 'term_id' : 'slug'
        ), $tax_query_args );
        $args['tax_query'] = array_merge( $base_tax_query, array($term_tax_query) );
        $q = new WP_Query( $args ); 
        if ( $q->have_posts() ) {
          // merging retrieved posts in $posts array
          // preventing duplicates using ID as array keys
          $ids = wp_list_pluck( $q->posts, 'ID' );
          $keyed = array_combine( $ids, array_values( $q->posts ) );
          $posts += $keyed;
        }
      }
      return $posts;
    }
    

    Now the function that check cache and return it if available or return non cached results

    function my_terms_get_posts( $args, $terms, $tax_query_args = NULL, $order = 'DESC' ) {
      // we need to know at least the taxonomy
      if ( ! is_array( $tax_query_args ) || ! isset( $tax_query_args['taxonomy'] ) ) return;
      $tax = $tax_query_args['taxonomy'];
      // get cached  results
      $cached = get_transient( "my_terms_get_posts_{$tax}" );
      if ( ! empty( $cached ) ) return $cached;
      // no cached  results, get 'fresh' posts
      $posts = my_fresh_terms_get_posts( $args, $terms, $tax_query_args );
      if ( ! empty($posts) ) {
        // order posts and cache them
        $posts = my_date_terms_posts_sort( $posts, $order );
        set_transient( "my_terms_get_posts_{$tax}",  $posts, DAY_IN_SECONDS );
      }
      return $posts;
    }
    

    Cache is auto-cleaned daily, however, is possible to invalidate it everytime a new post in on a specific taxonomy is added or updated. That can be done adding a cleaning cache function on 'set_object_terms'

    add_action( 'set_object_terms', function( $object_id, $terms, $tt_ids, $taxonomy ) {
      $taxonomies = get_taxonomies( array( 'object_type' => array('post') ), 'names' );
      if ( in_array( $taxonomy, (array) $taxonomies ) ) {
        delete_transient( "my_terms_get_posts_{$taxonomy}" );
      }
    }, 10, 4 );
    

    Usage

    // the number of post to retrieve for each term
    // total of posts retrieved will be equat to this number x the number of terms passed
    // to my_terms_get_posts function
    $perterm = 5;
    
    // first define general args:
    $paged = ( get_query_var('paged') ) ? get_query_var('paged') : 1;
    $args = array(
      'posts_per_page' => $perterm,
      'paged' => $paged,
    );
    
    // taxonomy args
    $base_tax_args = array(
      'taxonomy' => 'category'
    );
    $terms = array( 9, 11 ); // is also possible to use slugs
    
    // get posts
    $posts = my_terms_get_posts( $args, $terms, $base_tax_args );
    
    // loop
    if ( ! empty( $posts ) ) {
      foreach ( $posts as $_post ) {
        global $post;
        setup_postdata( $_post );
        //-------------------------> loop code goes here    
      }
      wp_reset_postdata();
    }
    

    Functions are flexible enough to use complex queries, even queries for additional taxonomies:

    E.G.

    $args = array(
      'posts_per_page' => $perterm,
      'paged' => $paged,
      'post_status' => 'publish',
      'tax_query' => array(
         'relation' => 'AND',
         array(
           'taxonomy' => 'another_taxonomy',
           'terms' => array( 'foo', 'bar' ),
           'field' => 'id'
         )
       )
    );
    
    $base_tax_args = array(
      'taxonomy' => 'category',
      'include_children' => FALSE
    );
    
    $terms = array( 'a-category', 'another-one' );
    
    $posts = my_terms_get_posts( $args, $terms, $base_tax_args );
    

    In this way the ‘tax_query’ set in $args array will be merged dinamically with the tax query argument in $base_tax_args, for each of the terms in the $terms array.

    Is also possible order posts in ascending order:

    $posts = my_terms_get_posts( $args, $terms, $base_tax_args, 'ASC' );
    

    Please note:

    1. IMPORTANT: If some posts belong to more than one category among the ones passed to function (e.g. in OP case some posts have both category 9 and 11) the number of post retrieved will be not the one expected, because function will return that posts once
    2. Code needs PHP 5.3+
  2. According to the Codex:

    Show Posts From Several Categories (Display posts that have these categories, using category id) would be:

    $query = new WP_Query( 'cat=9,11' );

    And, the Codex’s Pagination Parameters says that, 'showposts' is deprecated and replaced with 'posts_per_page'.

  3. What you are asking is a near duplicate of this question: How do I create my own nested meta_query using posts_where / posts_join?

    The problem, as @fischi suggests, is almost certainly that results are coming from one category or the other and hitting the post limit before both are equally represented. To make this work, you need a UNION. WP_Query is not capable of that logic, but with hooks you can make it work.

    $cat = 9; // your first category
    $showposts = 5; // actual results are twice this value
    $paged = (get_query_var('paged')) ? get_query_var('paged') : 1;
    $args=array(
      'cat' => $cat, 
      'posts_per_page' => $showposts,
      'paged' => $paged,
      'orderby' => 'post_date',
      'order' => 'DESC',
      'post_status' => 'publish',
    );
    
    function create_cat_union($clauses) {
      remove_filter('posts_request','create_cat_union',1);
      $clauses = str_replace('SQL_CALC_FOUND_ROWS','',$clauses,$scfr);
    
      $scfr = (0 < $scfr) ? 'SQL_CALC_FOUND_ROWS' : '';
    
      $pattern = 'wp_term_relationships.term_taxonomy_id IN (([0-9,]+))';
      $clause2 = preg_replace('|'.$pattern.'|','wp_term_relationships.term_taxonomy_id IN (11)',$clauses); // manipulate this
    
      return "SELECT {$scfr} u.* FROM (({$clauses}) UNION ({$clause2})) as u";
    }
    add_filter('posts_request','create_cat_union',1,2);
    
    $the_query = new WP_Query ( $args ); //the query
    var_dump($the_query->posts);
    

    A couple of notes: WP_Query will parse the category argument and find category children so the first UNION will include all of the children of category 9. I did not duplicate that logic for category 11. If you need that functionality, you can refer to the WP_Query source and reproduce the effect.

Comments are closed.