Cuándo usar WP_query(), query_posts() y pre_get_posts
Ayer leí @nacin's You don't know Query y me sumergí en un agujero de consultas. Antes de ayer, estaba (equivocadamente) usando query_posts()
para todas mis necesidades de consultas. Ahora estoy un poco más informado sobre el uso de WP_Query()
, pero todavía tengo algunas áreas grises.
Lo que creo saber con seguridad:
Si estoy creando bucles adicionales en cualquier parte de una página—en la barra lateral, en el pie de página, cualquier tipo de "entradas relacionadas", etc—debo usar WP_Query()
. Puedo usarlo repetidamente en una sola página sin ningún problema. (¿correcto?).
Lo que no sé con seguridad
- ¿Cuándo debo usar @nacin's
pre_get_posts
en lugar deWP_Query()
? ¿Debería usarpre_get_posts
para todo ahora? - Cuando quiero modificar el bucle en una plantilla — digamos que quiero modificar una página de archivo de taxonomía — ¿debo eliminar la parte
if have_posts : while have_posts : the_post
y escribir mi propioWP_Query()
? ¿O debo modificar la salida usandopre_get_posts
en mi archivo functions.php?
tl;dr
Las reglas resumidas que me gustaría extraer de esto son:
- No usar nunca más
query_posts
- Cuando se ejecuten múltiples consultas en una sola página, usar
WP_Query()
- Cuando se modifique un bucle, hacer esto __________________.
Gracias por cualquier sabiduría
Terry
PD: He visto y leído: ¿Cuándo deberías usar WP_Query vs query_posts() vs get_posts()? Lo cual añade otra dimensión — get_posts
. Pero no trata en absoluto sobre pre_get_posts
.

Tienes razón al decir:
Nunca uses
query_posts
nunca más
pre_get_posts
pre_get_posts
es un filtro para modificar cualquier consulta. Se usa con más frecuencia para alterar solo la 'consulta principal':
add_action('pre_get_posts','wpse50761_alter_query');
function wpse50761_alter_query($query){
if( $query->is_main_query() ){
//Hacer algo con la consulta principal
}
}
(También verificaría que is_admin()
devuelva false, aunque esto puede ser redundante). La consulta principal aparece en tus plantillas como:
if( have_posts() ):
while( have_posts() ): the_post();
//El loop
endwhile;
endif;
Si alguna vez sientes la necesidad de editar este loop, usa pre_get_posts
. Es decir, si tienes la tentación de usar query_posts()
, usa pre_get_posts
en su lugar.
WP_Query
La consulta principal es una instancia importante de un objeto WP_Query
. WordPress lo usa para decidir qué plantilla utilizar, por ejemplo, y cualquier argumento pasado a la URL (como la paginación) se canaliza en esa instancia del objeto WP_Query
.
Para loops secundarios (por ejemplo, en barras laterales o listas de 'publicaciones relacionadas'), querrás crear tu propia instancia separada del objeto WP_Query
. Ejemplo:
$my_secondary_loop = new WP_Query(...);
if( $my_secondary_loop->have_posts() ):
while( $my_secondary_loop->have_posts() ): $my_secondary_loop->the_post();
//El loop secundario
endwhile;
endif;
wp_reset_postdata();
Observa wp_reset_postdata();
: esto se debe a que el loop secundario sobrescribirá la variable global $post
, que identifica la 'publicación actual'. Esto esencialmente restablece eso al $post
en el que estamos.
get_posts()
Esto es esencialmente un envoltorio para una instancia separada de un objeto WP_Query
. Devuelve un array de objetos de publicación. Los métodos utilizados en el loop anterior ya no están disponibles para ti. Esto no es un 'Loop', simplemente un array de objetos de publicación.
<ul>
<?php
global $post;
$args = array( 'numberposts' => 5, 'offset'=> 1, 'category' => 1 );
$myposts = get_posts( $args );
foreach( $myposts as $post ) : setup_postdata($post); ?>
<li><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></li>
<?php endforeach; wp_reset_postdata(); ?>
</ul>
En respuesta a tus preguntas
- Usa
pre_get_posts
para modificar tu consulta principal. Usa un objetoWP_Query
separado (método 2) para loops secundarios en las páginas de plantilla. - Si deseas modificar la consulta del loop principal, usa
pre_get_posts
.

¿Existe algún escenario en el que uno iría directamente a get_posts() en lugar de WP_Query?

@drtanz - sí. Por ejemplo, si no necesitas paginación o posts fijos en la parte superior - en estos casos get_posts()
es más eficiente.

¿Pero no añadiría eso una consulta adicional cuando podríamos simplemente modificar pre_get_posts para alterar la consulta principal?

@drtanz - no estarías usando get_posts()
para la consulta principal - es para consultas secundarias.

En tu ejemplo de WP_Query, si cambias $my_secondary_loop->the_post(); por $my_post = $my_secondary_loop->next_post(); puedes evitar tener que recordar usar wp_reset_postdata() siempre que uses $my_post para hacer lo que necesites hacer.

@Privateer No es así, WP_Query::get_posts()
establece global $post;

@StephenHarris Acabo de revisar la clase de consulta y no la veo. Lo verifiqué principalmente porque nunca uso wp_reset_postdata porque siempre hago las consultas de esta manera. Estás creando un nuevo objeto y todos los resultados están contenidos dentro de él.

@Privateer - perdón, error tipográfico, WP_Query::the_post()
, ver: https://github.com/WordPress/WordPress/blob/759f3d894ce7d364cf8bfc755e483ac2a6d85653/wp-includes/query.php#L3732

@StephenHarris Correcto =) Si usas next_post() en el objeto en lugar de usar the_post, no afectas la consulta global y no necesitas recordar usar wp_reset_postdata después.

@Privateer Ah, mis disculpas, parece que me confundí. Tienes razón (pero no podrías usar funciones que hagan referencia al global $post
como the_title()
, the_content()
).

@urok93 A veces uso get_posts()
cuando necesito obtener posts relacionados de ACF, especialmente si solo hay uno. Aunque para estandarizar mis plantillas estoy considerando reescribirlas como instancias de WP_Query.

Existen dos contextos diferentes para los bucles (loops):
- El bucle principal que ocurre basado en la solicitud URL y se procesa antes de cargar las plantillas
- Los bucles secundarios que ocurren de cualquier otra manera, llamados desde archivos de plantilla o de otro modo
El problema con query_posts()
es que es un bucle secundario que intenta ser el principal y falla miserablemente. Por lo tanto, olvida que existe.
Para modificar el bucle principal
- no uses
query_posts()
- usa el filtro
pre_get_posts
con la verificación$query->is_main_query()
- alternativamente usa el filtro
request
(un poco demasiado brusco, por lo que lo anterior es mejor)
Para ejecutar un bucle secundario
Usa new WP_Query
o get_posts()
que son prácticamente intercambiables (el último es un envoltorio ligero del primero).
Para limpiar
Usa wp_reset_query()
si utilizaste query_posts()
o manipulaste directamente la variable global $wp_query
- por lo que casi nunca lo necesitarás.
Usa wp_reset_postdata()
si utilizaste the_post()
o setup_postdata()
o manipulaste la variable global $post
y necesitas restaurar el estado inicial de las cosas relacionadas con el post.

Existen escenarios legítimos para usar query_posts($query)
, por ejemplo:
Quieres mostrar una lista de entradas o entradas de tipos de contenido personalizados en una página (usando una plantilla de página)
Quieres que la paginación de esas entradas funcione
¿Por qué querrías mostrarlo en una página en lugar de usar una plantilla de archivo?
Es más intuitivo para un administrador (¿tu cliente?) - pueden ver la página en 'Páginas'
Es mejor para agregarlo a los menús (sin la página, tendrían que agregar la URL directamente)
Si deseas mostrar contenido adicional (texto, imagen destacada o cualquier meta contenido personalizado) en la plantilla, puedes obtenerlo fácilmente de la página (y también tiene más sentido para el cliente). Si usaras una plantilla de archivo, necesitarías codificar el contenido adicional o usar, por ejemplo, opciones del tema/plugin (lo que lo hace menos intuitivo para el cliente)
Aquí tienes un ejemplo de código simplificado (que estaría en tu plantilla de página - por ejemplo, page-page-of-posts.php):
/**
* Nombre de la plantilla: Página de entradas
*/
while(have_posts()) { // bucle principal original - contenido de la página
the_post();
the_title(); // título de la página
the_content(); // contenido de la página
// etc...
}
// ahora mostramos la lista de nuestras entradas de tipo de contenido personalizado
// primero obtenemos los parámetros de paginación
$paged = 1;
if(get_query_var('paged')) {
$paged = get_query_var('paged');
} elseif(get_query_var('page')) {
$paged = get_query_var('page');
}
// consultamos las entradas y reemplazamos la consulta principal (página) con esta (para que la paginación funcione)
query_posts(array('post_type' => 'my_post_type', 'post_status' => 'publish', 'paged' => $paged));
// paginación
next_posts_link();
previous_posts_link();
// bucle
while(have_posts()) {
the_post();
the_title(); // título de tu entrada de tipo de contenido personalizado
the_content(); // contenido de tu entrada de tipo de contenido personalizado
}
wp_reset_query(); // establece la consulta principal (global $wp_query) a la consulta original de la página (la obtiene de la variable global $wp_the_query) y restablece los datos de la entrada
// Entonces, ahora podemos mostrar el contenido relacionado con la página nuevamente (si lo deseamos)
while(have_posts()) { // bucle principal original - contenido de la página
the_post();
the_title(); // título de la página
the_content(); // contenido de la página
// etc...
}
Ahora, para ser completamente claros, también podríamos evitar usar query_posts()
aquí y usar WP_Query
en su lugar - así:
// ...
global $wp_query;
$wp_query = new WP_Query(array('tus parámetros de consulta aquí')); // establece la nueva consulta personalizada como consulta principal
// tu bucle de tipo de contenido personalizado aquí
wp_reset_query();
// ...
Pero, ¿por qué haríamos eso cuando tenemos una función tan pequeña y agradable disponible para ello?

Brian, gracias por eso. He estado luchando para que pre_get_posts funcione en una página en EXACTAMENTE el escenario que describes: el cliente necesita agregar campos personalizados/contenido a lo que de otro modo sería una página de archivo, por lo que se necesita crear una "página"; el cliente necesita ver algo para agregar al menú de navegación, ya que agregar un enlace personalizado les resulta difícil; etc. ¡+1 de mi parte!

Eso también se puede hacer usando "pre_get_posts". Lo hice para tener una "página de inicio estática" que muestre mis tipos de publicaciones personalizadas en un orden personalizado y con un filtro personalizado. Esta página también está paginada. Echa un vistazo a esta pregunta para ver cómo funciona: http://wordpress.stackexchange.com/questions/30851/how-to-use-a-custom-post-type-archive-as-front-page/30854
En resumen, todavía no hay un escenario más legítimo para usar query_posts ;)

Porque "Cabe señalar que usar esto para reemplazar la consulta principal en una página puede aumentar los tiempos de carga de la página, en los peores escenarios más del doble de la cantidad de trabajo necesario o más. Aunque es fácil de usar, la función también es propensa a confusiones y problemas posteriores." Fuente http://codex.wordpress.org/Function_Reference/query_posts

Esta respuesta está completamente equivocada. Puedes crear una "Página" en WP con la misma URL que el tipo de contenido personalizado. Por ejemplo, si tu CPT es Bananas, puedes tener una página llamada Bananas con la misma URL. Entonces terminarías con siteurl.com/bananas. Siempre que tengas archive-bananas.php en la carpeta de tu tema, usará la plantilla y "anulará" esa página en su lugar. Como se menciona en uno de los otros comentarios, usar este "método" crea el doble de trabajo para WP, por lo que NUNCA debería usarse.

Modifico la consulta de WordPress desde functions.php:
//desafortunadamente, la condición "IS_PAGE" no funciona en pre_get_posts (es un comportamiento de WORDPRESS)
//por lo que puedes usar `add_filter('posts_where', ....);` O modificar la consulta de "PAGE" directamente en el archivo de plantilla
add_action( 'pre_get_posts', 'myFunction' );
function myFunction($query) {
if ( ! is_admin() && $query->is_main_query() ) {
if ( $query->is_category ) {
$query->set( 'post_type', array( 'post', 'page', 'my_postType' ) );
add_filter( 'posts_where' , 'MyFilterFunction_1' ) && $GLOBALS['call_ok']=1;
}
}
}
function MyFilterFunction_1($where) {
return (empty($GLOBALS['call_ok']) || !($GLOBALS['call_ok']=false) ? $where : $where . " AND ({$GLOBALS['wpdb']->posts}.post_name NOT LIKE 'Journal%')";
}

Solo para esbozar algunas mejoras a la respuesta aceptada ya que WordPress ha evolucionado con el tiempo y algunas cosas son diferentes ahora (cinco años después):
pre_get_posts
es un filtro, para alterar cualquier consulta. Se usa más comúnmente para alterar solo la 'consulta principal':
En realidad es un action hook. No es un filtro, y afectará a cualquier consulta.
La consulta principal aparece en tus plantillas como:
if( have_posts() ):
while( have_posts() ): the_post();
//El loop
endwhile;
endif;
En realidad, esto tampoco es cierto. La función have_posts
itera el objeto global $wp_query
que no está relacionado únicamente con la consulta principal. global $wp_query;
puede ser alterado también con consultas secundarias.
function have_posts() {
global $wp_query;
return $wp_query->have_posts();
}
get_posts()
Esto es esencialmente un envoltorio para una instancia separada de un objeto WP_Query.
En realidad, actualmente WP_Query
es una clase, por lo que tenemos una instancia de una clase.
Para concluir: En el momento en que @StephenHarris escribió esto, muy probablemente todo era cierto, pero con el tiempo las cosas en WordPress han cambiado.

Técnicamente, todo son filtros bajo el capó, las acciones son simplemente un filtro básico. Pero tienes razón aquí, es una acción que pasa un argumento por referencia, que es cómo difiere de acciones más simples.

get_posts
devuelve un array de objetos de publicación, no un objeto WP_Query
, así que eso sigue siendo correcto. Y WP_Query
siempre ha sido una clase, instancia de una clase = objeto.
