Crear un formulario de búsqueda avanzada en WordPress para tu post-type

1. Introducción

En este artículo vamos a crear un formulario de búsqueda avanzada para un «post-type» de WordPress.

Utilizaremos el plugin Pods para crear nuestros «custom post types». Este plugin lo he descubierto gracias a David Viña, en una Meetup local de WordPress, en la ciudad de A Coruña. También puedes utilizar el famoso Custom Post Type UI.

Algo bueno sobre el plugin Pods, es que puedes crear campos meta asociados al nuevo tipo de post sin tener que utilizar otro plugin como Advanced Custom Fields.

Lo que haremos inicialmente será crear un tipo de post, utilizaremos de ejemplo un proyecto de un astillero. El tipo de post será barco.

Una vez creado el tipo de post, agregaremos campos meta o «meta fields» directamente desde el plugin Pods. Para nuestro ejemplo agregaremos 2 campos:

  • Potencia
  • Velocidad

Algo importante será marcar en las opciones avanzadas, la opción: Habilitar la página de archivo.

También crearemos una taxonomía clasificacion y la asociaremos con el tipo de post que hemos generado recientemente. Puedes crear algunos términos, por ejemplo:

  • Velero
  • Lancha

Nuestro objetivo será crear un formulario de búsqueda avanzado que pueda pasar por URL los parámetros y dejar que WordPress resuelva la consulta de búsqueda automáticamente.

Para recapitular, tenemos nuestro tipo de post barco, hemos creado una taxonomía clasificacion (con sus términos) y hemos creado dos campos meta.

Te recomendaría crear algunos posts del tipo barco, agregar a cada uno una clasificacion y llenar sus campos meta.

2. Agregar variables a WP Query Vars

Como sabrás, WordPress tiene definidas una serie de variables que utiliza en sus consultas. Algunas son públicas, otras privadas. Voy a darte un ejemplo para que pruebes.

Si ya has creado el tipo de post y has creado barcos prueba esta URL en tu navegador tudominio.com/?post_type=barco

WordPress resolverá automáticamente esa consulta mostrando los barcos en una página de archivo. ¿No es fantástico? Esto sucede porque post_type es una variable de consulta pública.

Aquí tienes la documentación del Codex con la lista de variables públicas y privadas.

Ahora vamos a registrar nuestras variables para que WordPress pueda interpretar cuando le pasemos un dato por parámetro en la URL. Para ello lo haremos mediante el filtro query_vars.

El código lo puedes insertar en el archivo functions.php de tu plantilla hija o crear un plugin. Para la facilidad del artículo lo haremos directamente en el archivo functions.php.

/**
 * Register custom query vars
 *
 * @link https://codex.wordpress.org/Plugin_API/Filter_Reference/query_vars
 */
function charrua_register_query_vars( $vars ) {
    $vars[] = 'pot';     //potencia
    $vars[] = 'vel';     //velocidad
    $vars[] = 'cla';     //clasificación
    return $vars;
} 
add_filter( 'query_vars', 'charrua_register_query_vars' );

3. La consulta que vamos a generar

Vamos a realizar una pequeña prueba de la consulta que se va a generar automáticamente para interiorizarnos con lo que sucederá.

Para que tengas una idea nuestro formulario de búsqueda buscará entre los dos campos que hemos definido antes y una taxonomía.

En este ejemplo vamos a suponer que definimos todos los campos de búsqueda de nuestro formulario, por ejemplo:

  • Potencia: 450
  • Velocidad: 40
  • Clasificación: Lancha

Para ello pegaremos el siguiente código en una plantilla y asignaremos la plantilla a una nueva página.

<?php
/**
 * Template Name: Barcos
 */

// Exit if accessed directly.
defined( 'ABSPATH' ) || exit;

get_header();

$args = array (
    'post_type'             => array( 'barco' ),
    'tax_query' => array(
        array (
            'taxonomy'      => 'clasificacion',
            'field'         => 'slug',
            'terms'         => 'lancha',
        )
    ),
    'meta_query' => array(
        'relation'          => 'AND',
        array(
            'key'           => 'potencia',
            'value'         => 450,
            'compare'	    => '=',
            'type'	    => 'NUMERIC'
        ), 
        array(
            'key'           => 'velocidad',
            'value'         => 40,
            'compare'	    => '=',
            'type'	    => 'NUMERIC'
        )
    )
);

// The Query
$the_query = new WP_Query( $args );

// The Loop
if ( $the_query->have_posts() ) {

    while ( $the_query->have_posts() ) : $the_query->the_post(); 
        // Your code here
    endwhile;

} else {
        // no posts found
}

// Restore original Post Data
wp_reset_postdata();

?>

Debemos destacar varias cosas, la primera es recordar que esta es una consulta estática, la estamos escribiendo para entender cómo consultar algo personalizado a la base de datos de WordPress.

Vamos a comentar lo que estamos pasando como argumentos:

'post_type' => array( 'barco' )

Lo más obvio, el tipo de post, no hay mucho que explicar aquí.

'tax_query' => array(
    array (
        'taxonomy' => 'clasificacion',
        'field'    => 'slug',
        'terms'    => 'lancha',
    )
),

La taxonomía, aquí vamos a destacar algunas cosas, el campo taxonomy define el nombre de la taxonomía, en nuestro caso clasificacion. El campo field, indica como buscaremos ese término, en nuestro caso lo buscaremos por el slug, que lo aclaramos en terms con el valor lancha.

'meta_query' => array(
        'relation'          => 'AND',
        array(
            'key'           => 'potencia',
            'value'         => 450,
            'compare'	    => '=',
            'type'	    => 'NUMERIC'
        ), 
        array(
            'key'           => 'velocidad',
            'value'         => 40,
            'compare'	    => '=',
            'type'	    => 'NUMERIC'
        )
    )

Viene lo interesante, la consulta con meta campos. Esta parte de la consulta utiliza la clase WP Meta Query y es la encargada de generar las consultas con los campos meta. Los argumentos que podemos utilizar en cada consulta son:

  • key
  • value
  • compare
  • type

Los posibles valores son:

ArgumentoTipoDescripción
keystringIdentifica el campo meta.
valuestring|arrayPuede ser un array cuando el valor de compare es ‘IN’‘NOT IN’‘BETWEEN’, o ‘NOT BEETWEEN’.
comparestringComparador de operación. Los posibles valores son ’=’’!=’‘>‘‘>=’‘<‘‘<=’‘LIKE’‘NOT LIKE’‘IN’‘NOT IN’‘BETWEEN’‘NOT BETWEEN’‘EXISTS’‘NOT EXISTS’‘REGEXP’‘NOT REGEXP’ and ‘RLIKE’. El valor por defecto es ’=’.
typestringTipo de dato. Los posibles valores son ‘NUMERIC’‘BINARY’‘CHAR’‘DATE’‘DATETIME’‘DECIMAL’‘SIGNED’‘TIME’‘UNSIGNED’. El valor por defecto es ‘CHAR’.

Puedes jugar con la consulta estática que hemos creado modificando valores a ver si te ofrece algún resultado. Recuerda crear algún post del tipo barco con los valores que ingreses en la consulta, de lo contrario no se mostrará ningún resultado.

Después de experimentar con la plantilla, puedes borrarla. No la necesitaremos en el futuro.

4. El filtro pre_get_posts

Una vez que se crea el objeto $query se dispara el action hook pre_get_posts justo antes de que la consulta se vaya a ejecutar. Esto significa que podemos interceptar la consulta y modificarla antes de su ejecución.

Vamos a ver un ejemplo para simplificar. Supongamos que tenemos una simple consulta:

$query = new WP_Query( 
    array( 
        'author_name'   => 'marcos', 
        'category_name' => 'desarrollo' 
    ) 
);

Podríamos lograr lo mismo si creamos una función y utilizamos el filtro pre_get_posts.

function charrua_pre_get_posts( $query ) {
    // check if the user is requesting an admin page 
    // or current query is not the main query
    if ( is_admin() || ! $query->is_main_query() ){
        return;
    }
    $query->set( 'author_name', 'marcos' );
    $query->set( 'category_name', 'desarrollo' );
}
add_action( 'pre_get_posts', 'charrua_pre_get_posts', 1 );

El objeto $query es pasado por referencia a la función, lo que significa que cualquier cambio que hagamos en ese objeto, afectará directamente a la consulta.

Como vamos a manipular directamente la consulta, tenemos que saber perfectamente a qué consulta nos referimos. El método is_main_query chequea si estamos en la consulta principal. El Codex también nos informa que utilizar pre_get_posts puede afectar a nuestro panel de administración. Por ello, utilizamos un condicional para no modificar la consulta principal ni el panel de administración.

Puedes ver la documentación de pre_get_posts para informarte en profundidad y obtener ejemplos de uso.

5. Manipular la consulta con datos reales

Una vez que hemos creado nuestro tipo de post barco, agregado barcos con su correspondiente taxonomía, campos meta, y hemos registrado las variables con query_vars, tendría sentido el generar una URL del tipo:

tudominio.com/?pot=333&vel=444&cla=555

/**
 * Build a custom query based on several conditions
 * The pre_get_posts action gives developers access to the $query object by reference
 * any changes you make to $query are made directly to the original object - no return value is requested
 *
 * @link https://codex.wordpress.org/Plugin_API/Action_Reference/pre_get_posts
 *
 */
function charrua_barco_archive($query)
{
    // check if the user is requesting an admin page
    // or current query is not the main query
    if (is_admin() || !$query->is_main_query()) {
        return;
    }

    // edit the query only when post type is 'barco'
    // if it isn't, return
    if (!is_post_type_archive('barco')) {
        return;
    }

    $meta_query = array();
    $taxonomy_query = array();

    // add meta_query elements
    if (!empty(get_query_var('pot'))) {
        $meta_query[] = array(
            'key' => 'potencia',
            'value' => get_query_var('pot'),
            'compare' => '=',
            'type' => 'NUMERIC',
        );
    }

    if (!empty(get_query_var('vel'))) {
        $meta_query[] = array(
            'key' => 'velocidad',
            'value' => get_query_var('vel'),
            'compare' => '=',
            'type' => 'NUMERIC',
        );
    }

    //if we have more than 1 meta query fields add relation AND
    if (count($meta_query) > 1) {
        $meta_query['relation'] = 'AND';
    }

    //if we have any meta query fields add them to the query
    if (count($meta_query) > 0) {
        $query->set('meta_query', $meta_query);
    }

    //add taxonomy fields
    if (!empty(get_query_var('cla'))) {
        $taxonomy_query[] = array(
            'taxonomy' => 'clasificacion',
            'field' => 'slug',
            'terms' => get_query_var('cla'),
        );
    }

    //if we have any taxonomy fields add them to the query
    if (count($taxonomy_query) > 0) {
        $query->set('tax_query', $taxonomy_query);
    }

}
add_action('pre_get_posts', 'charrua_barco_archive', 1);

Pasemos a explicar rápidamente lo que hemos escrito:

Antes que nada indicamos que si estamos en la consulta principal, en el panel de administración, o en un tipo de post distinto a barco, no aplicaremos esta consulta.

Luego, agregaremos los campos de búsqueda a la consulta, sólo si están definidos en los parámetros que se pasan por URL. Estos campos los recuperamos con get_query_var(). También hemos puesto un condicional que indica, si se define más de un meta campo, agrega la relación AND. Esto significa que si definimos los dos campos que tenemos, el barco que estamos buscando debe cumplir con ambas condiciones de búsqueda. Si queremos ampliar la búsqueda podemos cambiar la relación AND por OR. El resultado será recuperar resultados de la base de datos que cumpla sólo una de las condiciones.

Si seguimos el código, nos encontraremos con otro condicional, en este caso la consulta de la taxonomía clasificacion. Lo mismo que antes, si hemos definido la variable en la URL, el condicional agregará los argumentos a la consulta.

Para recapitular, solo se agregarán a la consulta, aquellos parámetros que hemos pasado por la URL. Por ejemplo si pasamos la siguiente URL

tudominio.com/?pot=350&vel=40&cla=lancha&post_type=barco

WordPress consultaría por un barco, con potencia de 350, velocidad 40 y que sea una lancha.

Si en la relación de los campos meta usamos OR, el resultado sería que WordPress consultaría por un barco, con potencia de 350 o velocidad 40 y que sea una lancha.

Recuerda también que estamos usando el comparador de igualdad=, podríamos utilizar otro comparador como BETWEEN y pasar dos valores, de esta manera podríamos buscar un barco con velocidad entre 20 y 40 nudos.

Nos quedaría por agregar un detalle, y es que debemos indicar a WordPress el tipo de post en la URL:

tudominio.com/?pot=350&vel=40&cla=lancha&post_type=barco

6. Crear el formulario de búsqueda

Vamos a crear un formulario muy simple mediante un shortcode.

/**
 * Register custom shortcode
 *
 * @link https://codex.wordpress.org/Shortcode_API
 */
function charrua_setup() {
    add_shortcode( 'charrua_search_form', 'charrua_search_form' );
}
add_action( 'init', 'charrua_setup' );

Luego vamos a definir la función encargada del contenido de nuestro shortcode.

/**
 * Callback function for shortcode charrua_search_form
 *
 * @param array  $atts
 */
function charrua_search_form( $atts ){

    $args = array( 'hide_empty' => true );
    $clasificacion_terms = get_terms( 'clasificacion', $args );

    if( is_array( $clasificacion_terms ) ){
		$select_clasificacion = '<label for="clasificacion">Clasificación</label>';
        $select_clasificacion .= '<select name="cla" id="clasificacion" style="width: 100%">';
        $select_clasificacion .= '<option value="" selected="selected">' . __( 'Seleccionar clasificacion', 'charrua_plugin' ) . '</option>';
        foreach ( $clasificacion_terms as $term ) {
            $select_clasificacion .= '<option value="' . $term->slug . '">' . $term->name . '</option>';
        }
        $select_clasificacion .= '</select>' . "\n";
    }

	$output = '<form action="' . esc_url( home_url() ) . '" method="GET" role="search">';
	$output .= '<p><label> Buscar<input type="text" name="s" value="' . get_search_query() . '" /></p>';
    $output .= '<p><label>Potencia <input type="text" id="potencia" name="pot" value="' . get_search_query('pot') . '" /></label></p>';
    $output .= '<p><label>Velocidad <input type="text" id="velocidad" name="vel" value="' . get_search_query('vel') . '"/></label></p>';
    $output .= '<p>' . $select_clasificacion . '</p>';
    $output .= '<input type="hidden" name="post_type" value="barco" />';
    $output .= '<p><input type="submit" value="Buscar!" class="button" /></p>';
    $output .= '</form>';

    return $output;
}

Lo que hace el código anterior es crear un formulario con dos campos de texto y un campo del tipo select. En los campos de texto podemos escribir el valor de la velocidad y la potencia. En el campo select, podemos seleccionar la clasificación. Recuerda que el formulario debe enviar los datos utilizando el método GET.

La idea de hacerlo en un shortcode como te estarás imaginado es que puedas insertar este formulario en cualquier página o entrada de tu página web. Incluso hasta puedes agregarlo en una plantilla.

Una vez hagas click en buscar, WordPress interpretará tu consulta y te enviará a la pagina archive.php.

Puedes duplicar el archivo archive.php y renombrarlo archive-barco.php. De esta manera puedes personalizar la página de búsqueda. Recuerda que esta página tambien se utiliza para mostrar el propio archivo del tipo de post barco.

EXTRA: Campo de texto para búsqueda.

Hay algo que debo comentarte antes de finalizar, podríamos incluir un campo de texto extra en el shortcode anterior con el nombre "s". Esto activara la página de búsqueda de WordPress, por lo que los resultados de búsqueda ahora se mostrarán en la plantilla search.php.

Aparte tenemos otro beneficio, este campo nos permitirá buscar dentro del contenido de cada barco.

7. Código completo

El código lo puedes insertar en el archivo functions.php de tu plantilla hija o crear un plugin. Para la facilidad del artículo lo haremos directamente en el archivo functions.php.

/**
 * Register custom query vars
 *
 * @link https://codex.wordpress.org/Plugin_API/Filter_Reference/query_vars
 */
function charrua_register_query_vars( $vars ) {
    $vars[] = 'pot';     //potencia
    $vars[] = 'vel';     //velocidad
    $vars[] = 'cla';     //clasificación
    return $vars;
} 
add_filter( 'query_vars', 'charrua_register_query_vars' );

/**
 * Build a custom query based on several conditions
 * The pre_get_posts action gives developers access to the $query object by reference
 * any changes you make to $query are made directly to the original object - no return value is requested
 *
 * @link https://codex.wordpress.org/Plugin_API/Action_Reference/pre_get_posts
 *
 */
function charrua_barco_archive($query)
{
    // check if the user is requesting an admin page
    // or current query is not the main query
    if (is_admin() || !$query->is_main_query()) {
        return;
    }

    // edit the query only when post type is 'barco'
    // if it isn't, return
    if (!is_post_type_archive('barco')) {
        return;
    }

    $meta_query = array();
    $taxonomy_query = array();

    // add meta_query elements
    if (!empty(get_query_var('pot'))) {
        $meta_query[] = array(
            'key' => 'potencia',
            'value' => get_query_var('pot'),
            'compare' => '=',
            'type' => 'NUMERIC',
        );
    }

    if (!empty(get_query_var('vel'))) {
        $meta_query[] = array(
            'key' => 'velocidad',
            'value' => get_query_var('vel'),
            'compare' => '=',
            'type' => 'NUMERIC',
        );
    }

    //if we have more than 1 meta query fields add relation AND
    if (count($meta_query) > 1) {
        $meta_query['relation'] = 'AND';
    }

    //if we have any meta query fields add them to the query
    if (count($meta_query) > 0) {
        $query->set('meta_query', $meta_query);
    }

    //add taxonomy fields
    if (!empty(get_query_var('cla'))) {
        $taxonomy_query[] = array(
            'taxonomy' => 'clasificacion',
            'field' => 'slug',
            'terms' => get_query_var('cla'),
        );
    }

    //if we have any taxonomy fields add them to the query
    if (count($taxonomy_query) > 0) {
        $query->set('tax_query', $taxonomy_query);
    }

}
add_action('pre_get_posts', 'charrua_barco_archive', 1);

/**
 * Register custom shortcode
 *
 * @link https://codex.wordpress.org/Shortcode_API
 */
function charrua_setup() {
    add_shortcode( 'charrua_search_form', 'charrua_search_form' );
}
add_action( 'init', 'charrua_setup' );

/**
 * Callback function for shortcode charrua_search_form
 *
 * @param array  $atts
 */
function charrua_search_form( $atts ){

    $args = array( 'hide_empty' => true );
    $clasificacion_terms = get_terms( 'clasificacion', $args );

    if( is_array( $clasificacion_terms ) ){
		$select_clasificacion = '<label for="clasificacion">Clasificación</label>';
        $select_clasificacion .= '<select name="cla" id="clasificacion" style="width: 100%">';
        $select_clasificacion .= '<option value="" selected="selected">' . __( 'Seleccionar clasificacion', 'charrua_plugin' ) . '</option>';
        foreach ( $clasificacion_terms as $term ) {
            $select_clasificacion .= '<option value="' . $term->slug . '">' . $term->name . '</option>';
        }
        $select_clasificacion .= '</select>' . "\n";
    }

	$output = '<form action="' . esc_url( home_url() ) . '" method="GET" role="search">';
	$output .= '<p><label> Buscar<input type="text" name="s" value="' . get_search_query() . '" /></p>';
    $output .= '<p><label>Potencia <input type="text" id="potencia" name="pot" value="' . get_search_query('pot') . '" /></label></p>';
    $output .= '<p><label>Velocidad <input type="text" id="velocidad" name="vel" value="' . get_search_query('vel') . '"/></label></p>';
    $output .= '<p>' . $select_clasificacion . '</p>';
    $output .= '<input type="hidden" name="post_type" value="barco" />';
    $output .= '<p><input type="submit" value="Buscar!" class="button" /></p>';
    $output .= '</form>';

    return $output;
}

8. Conclusión

Hemos hecho una primera aproximación a la creación de un sistema avanzado de búsqueda en WordPress, utilizando su propio motor para las consultas.

Claro que podemos hacer consultas mucho más complejas y combinadas, incluso como te comentaba antes, en vez de buscar un valor específico, por ejemplo, buscar un resultado dentro de un rango, o quizá un resultado fuera de un rango… Hay muchas posibilidades.

Ahora solo queda una tarea: ¡picar código!