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.

  • Israel Ixon

    Hola Juan, para rizar el rizo, ¿seria posible paginar los resultados? Por más información que busco no doy con la solución o, mejor dicho, me cuesta mucho integrar lo que encuentro con este ejemplo. Valdría como está, pero seria perfecto si se pudiera paginar porque puede darse el caso de que haya muchísimos post. ¿no?

    • Claro que es posible, pero paginar loops secundarios es algo que no haría salvo casos muy excepcionales. Un listado de posts paginado es generalmente el contenido principal de la página. Y si es el contenido principal, ¿como va a ir en un loop secundario?

      Yo lo haría, sin duda, modificando el query principal con el action pre_get_posts. Te ahorras solicitudes a la base de datos innecesarias y además, como se trabaja con el query/loop principal, la paginación te funcionará sin que tebgas que hacer nada.

    • Israel Ixon

      Gracias Juan por tu rápida respuesta. Esta vez sí que la he visto.

    • Tengo un rato libre y estoy escribiendo sobre la paginación de loops secundarios. Atento, espero publicarlo esta mañana.

  • Israel Ixon

    Hola, lo primero de todo, muchas gracias por compartir estos conocimientos. Si no fuera por gente como tú, a muchos como yo, nos costaría muchísimo aprender todas estas cosas.

    Ahora tengo una duda:

    Por qué el artículo crear metaboxes y campos… el metabox lo formas así:

    add_action('add_meta_boxes', 'cyb_meta_boxes');
    function cyb_meta_boxes() {
    add_meta_box( 'cyb-meta-box', __('Titulo del Metaboxes'), 'cyb_meta_box_callback', 'post' );
    }

    y en este artículo, metes la función dentro del add_action? así:

    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' );
    } );

    ¿Sirve igual o tiene que ser así? ¿Hay alguna razón?

    Una recomendación, para gente que intentamos aprender, cuando nos cambian un criterio sin saber por qué, NOS PERDEMOS, nos perdemos mucho. Es mejor seguir, si no hay una necesidad, un mismo metodo ¿no?

    Muchas gracias

    • Buenos días Israel.

      Ambos códigos son correctos, te explicaré las diferencias, que son dos principalmente.

      En el primer código se utiliza el action add_meta_boxes, que es genérico para los tipos de posts, y en el segundo se utiliza la forma add_meta_boxes_{post-type}, que es específico para el post type establecido, en este caso el tipo de post event con el que estamos trabajando. Ambos se explican en Cómo crear metaboxes y custom fields.

      Otra diferencia es el uso de funciones anónimas.

      El segundo parámtro de la función add_action() es la función de llamada de retorno, callback en inglés. Esto es, la función que se ejecutará en el evento o action definido como primer parámetro; cuándo ocurre ese evento WordPress ejecuta la función que le hemos dado.

      Cómo callback puedes especificar el nombre de una función o de un método, que el primer código que pones, o una función anónima (closure), que el segundo código que pones. Ambos son completamente váidos y serían equivalente en cuánto a resultados:

      add_action( 'nombre_del_action', 'mi_callback' );
      function mi_callback() {
      // Has lo que necesites aquí
      }

      add_action( 'nombre_del_action', function() {
      // Has lo que necesites aquí
      } );

      Sin embargo, y en términos generales, cuándo se trabaja con WordPress se tiende a evitar las funciones anónimas ya que los actions y filters que utilizan funciones anónimas no se pueden eliminar de forma selectiva si es necesario.

      Espero que te haya quedado claro. Si sigues con dudas estaré encantado de resolverlas.

    • Israel Ixon

      Hola Juan, ya se que hace mucho de este comentario, pero nunca es tarde si la dicha es buena, en este caso es agradecerte la respuesta. Muhcas gracias.
      No había visto que me habias contestado. Creo que no me llegó ningún mail, y ahora volviendo a consultarlo, he visto la respuesta. Gracias otra vez.