Why WP_Query::is_date() returns false when ‘date_query’ argument is set?

WordPress 3.7 introduced ‘date_query’ argument for WP_Query.

That’s pretty useful and allow very complex date-based queries, however something strange happen when using WP_Query conditional methods on query using that argument.

Read More

Example:

$args = array(

  'tax_query'=> array(
    'relation' => 'AND',
    array(
      'taxonomy' => 'category',
      'field'    => 'slug',
      'terms'    => 'uncategorized'
    ),
    array(
      'taxonomy' => 'post_tag',
      'field'    => 'slug',
      'terms'    => array('foo', 'bar')
    )
  ),

  'date_query' => array(
     array(
       'year'  => $today["year"],
       'month' => $today["mon"],
       'day'   => $today["mday"],
     )
  )

);

$query = new WP_Query( $args );

The query should returns posts posted today and having ‘uncategorized’ category or ‘foo’ and ‘bar’ tags. And it does, so far so good.

Now consider the following conditional methods:

$query->is_archive();  // returns true, as guessable
$query->is_category(); // returns true, as guessable
$query->is_tag();      // returns true, as guessable

but

$query->is_date();  // returns false
$query->is_day();   // returns false
$query->is_month(); // returns false
$query->is_year();  // returns false

I thought that the problem can be caused by the fact I mixed ‘tax_query’ and ‘date_query’, but that’s not the problem, proof:

$args = array(
  'date_query' => array(
     array(
       'year'  => $today["year"],
       'month' => $today["mon"],
       'day'   => $today["mday"],
     )
  )
);

$query = new WP_Query( $args );

$query->is_date();    // returns false
$query->is_day();     // returns false
$query->is_month();   // returns false
$query->is_year();    // returns false
$query->is_archive(); // returns false this time

Is this a bug, or I’m missing something? (I’ve searched WP Trac, but found nothing related).

Related posts

Leave a Reply

3 comments

  1. The values retrieved by the is_date(), is_day(), etc. methods are set inside the parse_query method and they really seem to be intended for the main query, whether that is explicitly stated (intended) or not. The code parses the public query variables to determine those settings.

    My interpretation is that these values aren’t really meant for secondary queries, and really don’t make much sense for secondary queries either. You’ve just run the query, you know that it is a date/tax/whatever query. That part doesn’t need to be determined for you by parsing the query variables.

    To did a bit deeper, I’d say the problem stems from WP_Query being a bit schizophrenic. It parsed the request to determine which page ought to load, rather than just being a tool to query the database for posts. It does two jobs, rather than one, which is poor design in my opinion. I think distinct code should parse the request and then pass arguments to WP_Query. Maybe that is just me.

    Is it a bug? I’d call it that, at least “weird design”. If WP_Query is going to do both jobs, it should at least set the class variables consistently, or split the class into two each with a single purpose. Of course, I don’t get to canonically declare something a “bug” or not 🙂

  2. Note: This is just an addition to @s_ha_dum answer (which by the upvotes probably be north of this one).

    All the “Conditional Tags”/Query methods that determine state, for e.g.

    • is_date
    • is_day
    • is_hour
    • is_nanosecond

    …are all old, so people tend to forget about their existance. And let’s face it: They never have been very popular. How many hourly archives have you encountered in your digital journeys across the interwebs?

    But what is interesting and often needed are date ranges, which can be interpreted as completely unrelated to date ARCHIVES – and that is what the is_ conditionals are. So I could imagine, that those who wrote the date_query parts saw exactly no connection between date or time based archives and the according queries.

    Imagine the following situation:

    WTF! Why the heck does my hourly archive template kick in, when all I wanted was posts, paginated by week on my front page?

    Even if a lot of core often seems to be “design by accident”, this one actually isn’t. Take a look at the tax_query parts of WP_Query::get_posts():

    $this->parse_tax_query( $qv );
    
    foreach ( $this->tax_query->queries as $tax_query ) {
        if ( 'NOT IN' != $tax_query['operator'] ) {
            switch ( $tax_query['taxonomy'] ) {
                case 'category':
                    $this->is_category = true;
                    break;
                case 'post_tag':
                    $this->is_tag = true;
                    break;
                default:
                    $this->is_tax = true;
            }
        }
    }
    unset( $tax_query );
    

    Let me leave you with two questions:

    1. What does above snippet imply?
    2. Do you really want that?
  3. I’ll try to make some order, and focus all my thoughts in a answer instead of commenting the 2 good answers by @s_ha_dum and @kaiser (I upvoted both).

    First we need to specify that in WordPress there are 2 types of queries, main and secondaries.

    Both are instances of same class, WP_Query but first difference is that main query object is instanciated by WordPress and saved in the global $wp_query variable, all the secondary queries must be instantiated by custom code.

    But there is another big difference: to get posts, WP_Query need to receive some arguments: in main query those arguments are taken from url in secondary queries arguments must explicitly passed to constructor or to get_posts method.

    A direct consequence of previous is that, for main query, the query arguments -taken from url- can be only scalar variables: strings, integers and booleans: query arguments whose values are arrays can’t be set via urls, infact, any non scalar variable is strip out from query arguments set via urls.

    So we have also 2 types of query arguments, “scalar arguments” and “array arguments”.

    WordPress uses scalar arguments to setup template tags, infact WordPress workflow is:

    1. Setup query arguments using url arguments
    2. Setup conditional template properties (all the is_*) in global $wp_query object
    3. Get the posts based on query arguments
    4. Using those conditional template properties load a specific template

    Worth noting that even if using url is not possible set non-scalar arguments, is possible to use 'pre_get_posts' action hook to make the main query use non-scalar variables like tax_query, meta_query, date_query and so on.

    But 'pre_get_posts' runs between #3 ad #4 in workflow described above, so even it make possible to use array-based query arguments, the conditional template properties are already setup, so simply using a tax_query or any other non-scalar query argument inside pre_get_posts don’t affect the template conditional tags (and so the template loaded) unless one explicitly set is_* properties:

    add_action( 'pre_get_posts', function( $query ) {
    
      if ( $query->is_main_query() && ! is_admin() ) {
          
        $tax_query = array ( array(
          'taxonomy' => 'category', 'field' => 'slug', 'terms' => 'foo')
        );
    
        // this will affect query, but will NOT affect the template loaded
        $query->query_vars['tax_query'] = $tax_query; 
        // this will affect the template loaded, instead
        $query->is_category = TRUE; 
      }
    
    } );
    

    That said, seems logic and legit that non-scalar query arguments like 'date_query' are not used to set template tags, so the question becomes another: why WordPress parse tax_query arguments even if they can’t affect templates?

    Even if this question should be asked to core devs, I have an idea.

    Notice: next 2 code snippets are bad, here just for proof of concept: don’t do at home.

    add_action( 'template_redirect', function() {
    
      $args = array( 'tax_query' => array(
        array( 'taxonomy' => 'category', 'terms' => array( 16, 17 )  )
      ) );
    
      $GLOBALS['wp_query'] = new WP_Query( $args );
    
    } );
    

    In previous code I replaced the global wp_query with another query that makes use of a tax query. It can appear strange, but it works as expected, and the template loaded is category.php because WordPress set template tags for 'tax_query' (in facts and $GLOBALS['wp_query']->is_category() will be true).

    If I do same thing using a 'date_query':

    add_action( 'template_redirect', function() {
    
      $args = array( 'date_query' => array(
        array( 'month' => 3, 'year' => 2014  )
      ) );
    
      $GLOBALS['wp_query'] = new WP_Query( $args );
    
    } );
    

    posts will be retrieved as expected (from March 2014), but template loaded will be not date.php and $GLOBALS['wp_query']->is_date() will be false.

    So my guess – and is only a guess- is that in older code, core devs thought about the possibility to override the global $wp_query object with one using non-scalar arguments and so they concerned to make template hierarchy working in that case. But in more modern code (and ‘date_query’ appear only on WP 3.7) core devs don’t worried about that because they probably thought there is no reason to be worried about bad practises…

    Essentially, for how core works, isn’t strange that date_query doesn’t set template conditional properties, but is strange that tax_query does.

    Just two additional notes.

    Looking at template hierarchy, some conditional template tags has no effect on it, e.g. is_time and much others have no effect on template choosing.

    So, which is the usefulness of conditional template tags that doesn’t affect template choosing?

    If we answer: “none”, than we have to admit that they are there just for some historical or misterious reason, otherwise if a conditional tag is useful even if doesn’t affect template choosing, then we have to admit that will be useful if WP would set conditional properties for all the non-scalar argumets and not only for 'tax_query'.

    Moreover, sometimes in my code I use to create my own class that extends WP_Query (and I’m not the only one). Even if most cases the usage of custom WP_Query classes is for secondary queries, is possible that one want to use it also for the main query (far from impossible because is just matter of overwrite a global variable).

    Sure is a very bad practice overwrite $wp_query after it already queried posts, but if one uses an early hook, like 'setup_theme', there, global $wp_query is just an empty object and can be override without any issue.
    But customizing main WP_Query object, one can make possible to use non-scalar arguments for main query and allow to set them before the conditional template properties are set.
    In that case wouldn’t be useful that WP_Query would parse all the non-scalar arguments and properly set conditional template properties accordingly?

    In my opinion, yes, it would be useful, but untill the core will be strongly based on core WP_Query class (and thats is closely forever) it probably makes no sense having that in core.


    Bonus

    If someone, for any reason, want to parse 'date_query' and set conditional template tags, then following code can be used:

    add_action( 'parse_query', function ( WP_Query $q ) {
      $cond = array(
        'hour' => 'is_time', 'minute' => 'is_time', 'second' => 'is_time',
        'year' => 'is_year', 'month' => 'is_month', 'day' => 'is_day',
        'dayofweek' => 'is_day',
        'week' => 'is_date', 'before' => 'is_date', 'after' => 'is_date'
      );
      $date_query = isset( $q->query_vars['date_query'] )
        ? $q->query_vars['date_query']
        : FALSE;
      if ( empty( $date_query ) ) return;
      $found = 0;
      foreach ( $date_query as $i => $query ) {
        if ( ! is_array( $query ) ) continue;
        foreach ( $query as $key => $val) {
          if ( is_numeric($key) || ! isset( $cond[$key] ) ) continue;
          if ( in_array( $key, array( 'before', 'after' ) ) ) {
            $q->is_time = $q->is_year = $q->is_month = $q->is_day = FALSE;
            $q->is_date = $q->is_archive = TRUE;
            return;
          }
          $found++;
          $q->$cond[$key] = TRUE;
        }
      }
      if ( $found <= 0 ) return;
      // a query can't be is_time, is_year, is_month and is_day in same time
      $q->is_year = $q->is_year && ! $q->is_time && ! $q->is_month && ! $q->is_day;
      $q->is_month = $q->is_month && ! $q->is_time && ! $q->is_day;
      $q->is_day = $q->is_day && ! $q->is_time;
      $q->is_date = $q->is_archive = TRUE;
      $q->is_home = FALSE;
    });