Una pregunta bastante repetida en WPSE es sobre como hacer un query de posts entre dos fechas cuándo estas fechas son almacenadas como custom meta fields. Motivos para esta situación hay muchos, uno de los más habituales es para posts tipo “eventos” en los que se almacena la fecha de comienzo y la fecha de fin en campos personalizados y luego se quieren obtener los eventos disponibles en un rango de fechas determinado.

Pues bien, en este tutorial vamos a ver un ejemplo básico de esta situación: crearemos un tipo de post llamado “event”, crearemos un meta box con dos custom fields para almacenar las fechas de inicio y fin y luego obtendremos los eventos entre un rango de fechas determinado. Antes de seguir, deberías saber como se registran los custom post types y como se crean meta boxes y custom fields en general. A estos dos apartados le dedicaré muy poco para centrar el post en el query.

1

Registro del post type event

Este paso es el más sencillo de todos. Basta con utilizar register_post_type en el evento init. Por ejemplo:

add_action( 'init', function () {

  $labels = array(
    'name'               => _x( 'Events', 'Nombre general del post type', 'plugin-textdomain' ),
    'singular_name'      => _x( 'Event', 'Nombre singular del post type', 'plugin-textdomain' ),
    'add_new_item'       => __( 'Add New Event', 'plugin-textdomain' ),
    'new_item'           => __( 'New Event', 'plugin-textdomain' ),
    'edit_item'          => __( 'Edit Event', 'plugin-textdomain' ),
    'view_item'          => __( 'View Event', 'plugin-textdomain' ),
    'all_items'          => __( 'All Events', 'plugin-textdomain' ),
    'search_items'       => __( 'Search Events', 'plugin-textdomain' ),
    'parent_item_colon'  => __( 'Parent Events:', 'plugin-textdomain' ),
    'not_found'          => __( 'No events found.', 'plugin-textdomain' ),
    'not_found_in_trash' => __( 'No events found in Trash.', 'plugin-textdomain' )
  );

  $args = array(
    'labels'             => $labels,
    'description'        => __( 'A Event post type.', 'plugin-textdomain' ),
    'public'             => true,
    'has_archive'        => true,
    'supports'           => array( 'title', 'editor', 'author', 'thumbnail', 'excerpt', 'comments' )
  );
 
  register_post_type( 'event', $args );
 
} );
2

Creación de los custom fields para almacenar las fechas

Definimos dos campos personalizados, event_start_date e event_end_date, dónde almacenaremos la fecha de inicio y fin del evento respectivamente (Nota: he utilizado el input tipo date, el propio navegador proporcionará un datepicker; aunque es soportado de forma bastante amplia por los diferentes navegadores, puede serte de interés añadir el datepicker de jQuery u otro que estimes oportuno, incluso ninguno).

add_action( 'add_meta_boxes_event', function() {

  add_meta_box( 'cyb-event-meta-box', __('Event dates', 'my-textdomain'), 'cyb_event_dates_fields', 'event', 'normal', 'high' );
 
} );

function cyb_event_dates_fields( $post ) {

  $current_dates = array(
    'event_start_date' => '',
    'event_end_date'   => ''
  );

  $post_meta = get_post_custom( $post->ID );
 
  foreach( $current_dates as $date => $value ) {
    if( isset( $post_meta[$date] ) ) {
      $current_dates[$date] = $post_meta[$date][0];
    }
  }
 
  //Nonce field. http://codex.wordpress.org/Function_Reference/wp_nonce_field
  wp_nonce_field( 'cyb-event-meta-box', 'cyb_event_meta_box_noncename' );

  ?>
 
  <p>
    <label class="label" for="event_start_date"><?php _e("Enter the start date", 'plugin-textdomain'); ?></label>
    <input name="event_start_date" id="event_start_date" type="date" value="<?php echo esc_attr( $current_dates['event_start_date'] ); ?>">
  </p>
  <p>
    <label class="label" for="event_end_date"><?php _e("Enter the end date", 'plugin-textdomain'); ?></label>
    <input name="event_end_date" id="event_end_date" type="date" value="<?php echo esc_attr( $current_dates['event_end_date'] ); ?>">
  </p>
  <?php
 
}

En este bloque sólo he creado el meta box y los custom fields, ahora tenemos que guardar estos datos. Aquí me voy a parar un poco. Cómo decía en la introducción, esta pregunta es bastante frecuente en WPSE y cada usuario que pregunta suele venir con un formato de fecha diferente que en ocasiones ni siquiera conocen o no le han prestado atención y simplemente tienen problemas al intentar comparar formatos incomparables. Así que hay que prestarle atención.

Puedes elegir entre varios formatos de fecha, los más recomendados para la base de datos son UNIX timestamp y MySQL date. Yo me decanto por el segundo, que es Y-m-d (Y-m-d H:i:s si fuera fecha y hora. Vea formatos de fecha en PHP). Ambos formatos se pueden ordenar y se pueden comparar valores fácilmente pero UNIX timestamp es bastante difícil de descifrar a simple vista.

add_action('save_post', function( $post_id, $post ){

  // Primero comprobamos el tipo de post y que el usuario tenga permiso para editarlo
  if ( 'event' == $post->post_type ) {
    if ( !current_user_can( 'edit_post', $post_id ) ) {
      return;
    }
  }

  // Segundo, comprobamos el nonce como medida de seguridad
  if ( !isset( $_POST['cyb_event_meta_box_noncename'] ) || ! wp_verify_nonce( $_POST['cyb_event_meta_box_noncename'], 'cyb-event-meta-box' ) ) {
    return;
  }
 
  //Tercero, validamos y almacenamos el valor del custom field o lo borramos si es necesario

  $date_fields = array(
    'event_start_date',
    'event_end_date'
  );
 
  //Para cada field, comprobar si está en la solicitud y almacenarlo en la base de datos
  foreach( $date_fields as $field ) {
    //El text input
    if( isset( $_POST[$field] ) ) {
      //Se supone que la fecha ya está en formato Y-d-m en el datepicker, pero por si acaso
      $value = date('Y-m-d', strtotime( sanitize_text_field( $_POST[$field] ) ) );
      update_post_meta( $post_id, $field, $value );
    } else {
      if ( isset( $post_id ) ) {
        delete_post_meta( $post_id, $field );
      }
    }
  }

}, 10, 2);
3

Obtener posts entre fechas determinadas en meta fields

Ahora que ya tenemos el tipo de post y los meta fields donde guardamos las fechas de inicio y de fin en el formato adecuado, podemos pasar al tema central de este artículo: obtener los posts (“event” en este ejemplo) cuyos fechas de inicio y/o de fin estén entre un rango determinado. Vamos a ver varias situaciones y ejemplos, pero primero, algunos conceptos.

En WordPress el query de posts se puede realizar fácilmente con la clase WP Query. Cómo las fechas las hemos almacenado en campos meta, utilizaremos los parámetros relativos a estos fields para especificar las condiciones que han de cumplir los posts, concretamente vamos a utilizar el parámetro meta_query. Este parámetro es muy flexible y se pueden definir relaciones entre meta fields desde muy sencillas hasta muy complejas, incluso anidadas unas dentro de otras. Comencemos con algunos ejemplos con nuestros “eventos” :

Obtener eventos “actuales”

Con “actuales” quiero decir eventos cuya fecha de inicio sea menor o igual que “hoy” y cuya fecha de fin sea mayor o igual a “hoy”. En otras palabras, que “hoy” esté entre los custom fields event_start_date e event_end_date, ambos incluidos.

//Obtener la fecha de hoy en formato Y-m-d
$today = date('Y-m-d');

$args = array(
  'post_type'   => 'event',
  'meta_query'  => array(
    //Relación entre los dos custom fields
    'relation'    => 'AND',
      array(
        'key'     => 'event_start_date',
        'value'   => $today,
        'compare' => '<=',
        'type'    => 'DATE'
      ),
      array(
        'key'     => 'event_end_date',
        'value'   => $today,
        'compare' => '>=',
        'type'    => 'DATE'
      ),
  ),
);

$query = new WP_Query( $args );

Con el código anterior ya pordríamos iniciar el loop para mostrar los eventos. También podríamos, por ejemplo, añadir los parámetros order y orderby para que aparezcan en un orden determinado. Ejemplo: construir una lista de eventos actuales de menor a mayor fecha de inicio:

$today = date('Y-m-d');

$args = array(
  'post_type'   => 'event',
  //Declaración del meta_key necesaria para los parámetros de order y orderby
  'meta_key'    => 'event_start_date',
  'order'       => 'ASC',
  'orderby'     => 'meta_value',
  'meta_query'  => array(
    'relation'    => 'AND',
      array(
        'key'     => 'event_start_date',
        'value'   => $today,
        'compare' => '<=',
        'type'    => 'DATE'
      ),
      array(
        'key'     => 'event_end_date',
        'value'   => $today,
        'compare' => '>=',
        'type'    => 'DATE'
      ),
    ),
);

$query = new WP_Query( $args );

//Comenzamos el loop
if( $query->have_posts() ) {

    echo '<h1>' . __('Current events', 'plugin-textdomain') . '</h1>';
    echo '<ul>';

    while( $query->have_posts() ) {
        
        $query->the_post();
        
        echo '<li>';

        the_title();

        echo '</li>';

    }

    echo '</ul>';

    wp_reset_postdata();

}

Obtener eventos que comiencen en el mes actual

Se podría hacer algo parecido al ejemplo anterior pero utilizando la fecha de inicio en las dos condiciones: que la fecha de inicio sea menor al último día del mes y que a la vez sea mayor al primer día del mes. Esta comparación se puede hacer más fácilmente con el comparador BETWEEN frente a un array que determina el rango primer – último día del mes (se podría utilizar de forma similar para cualquier otro rango de fechas):

//Obtener el primer y último día del mes actual
$month_first_day = date("Y-m-1");
$month_last_day = date("Y-m-t");
$args = array(
  'post_type'  => 'event',
  'meta_key'   => 'event_start_date',
  'order'      => 'ASC',
  'orderby'    => 'meta_value',
  'meta_query' => array(
     array(
       'key'      => 'event_start_date',
       'value'    => array( $month_first_day, $month_last_day ),
       'compare'  => 'BETWEEN',
       'type'     => 'DATE'
     )
   )
 
);

$query = new WP_Query( $args );

Obtener eventos activos para un mes determinado

Aquí la lógica es un poco más compleja. Un evento sería válido para mostrarlo en un mes si algún período entre la fecha de inicio y de fin se sitúa en el primer y último del día del mes, lo que se cumpliría si se dan estas dos condiciones:

  • La fecha de inicio es menor al primer día del mes y la fecha de fin es igual o mayor al primer día del mes: el evento comenzó antes de que empezara el mes y termina después. Da igual cuándo termine, la clave es que comience antes y termine después de que comience el mes.
  • La fecha de inicio se sitúa entre el primer y último día del mes, ambos incluidos: el evento comienza dentro el mes, da igual cuándo termine.

Por lo tanto, vamos a necesitar la comparación de dos meta_query anidados. Por ejemplo, obtener los eventos activos durante el mes de Febrero de 2016:

$month = "2016-02";
//Obtener el primer y último día del mes definido en $month
$month_last_day = date("Y-m-t", strtotime($month));
$month_first_day = date("Y-m-1", strtotime($month));
$args = array(
            'post_type'  => 'event',
            'meta_key'   => 'event_start_date',
            'order'      => 'ASC',
            'orderby'    => 'meta_value',
            'meta_query' => array(
                'relation' => 'OR',
                 array(
                     'relation' => 'AND',
                      array(
                          'key'     => 'event_start_date',
                          'value'   => $month_first_day,
                          'compare' => '<',
                          'type'    => 'DATE'
                      ),
                      array(
                          'key'     => 'event_end_date',
                          'value'   => $month_first_day,
                          'compare' => '>=',
                          'type'    => 'DATE'
                      )
                 ),
                 array(
                      'key'     => 'event_start_date',
                      'value'   => array( $month_first_day, $month_last_day ),
                      'compare' => 'BETWEEN',
                      'type'    => 'DATE'
                 )
            )
);

$query = new WP_Query( $args );

Podría seguir con más y más ejemplos pero creo que ya queda claro como se obtienen posts entre fechas que han sido almacenadas en custom fields. Los ejemplos propuestos pretenden ser eso, ejemplos; se podría elaborar mucho más el código dependiendo de las necesidades. Por ejemplo, se podría incluir también la hora o se podría hacer compatible con diferentes configuraciones de zona horaria. Por ahora lo dejo aquí.

Cómo siempre, espero que te sea útil. Y si tienes algún comentario, soy todo oídos.