Cómo combinar dos consultas en WordPress
Estoy intentando ordenar los posts de una categoría mostrando primero los posts con imágenes y luego los que no tienen imágenes. Lo he logrado ejecutando dos consultas separadas y ahora quiero combinarlas.
Tengo lo siguiente:
<?php
// Consulta para posts con imagen destacada
$loop = new WP_Query( array('meta_key' => '_thumbnail_id', 'cat' => 1 ) );
// Consulta para posts sin imagen destacada
$loop2 = new WP_Query( array('meta_key' => '', 'cat' => 1 ) );
// Intento de combinar las consultas
$mergedloops = array_merge($loop, $loop2);
while($mergedloops->have_posts()): $mergedloops->the_post(); ?>
Pero cuando intento ver la página obtengo el siguiente error:
Fatal error: Call to a member function have_posts() on a non-object in...
Luego intenté convertir array_merge a un objeto, pero obtuve este error:
Fatal error: Call to undefined method stdClass::have_posts() in...
¿Cómo puedo solucionar este error?
Una única consulta
Pensándolo un poco más, existe la posibilidad de que puedas trabajar con una única consulta principal. En otras palabras: no necesitas dos consultas adicionales cuando puedes trabajar con la predeterminada. Y en caso de que no puedas trabajar con la predeterminada, no necesitarás más de una única consulta sin importar cuántos bucles quieras dividir.
Requisitos previos
Primero necesitas establecer (como se muestra en mi otra respuesta) los valores necesarios dentro de un filtro pre_get_posts
. Allí probablemente configurarás posts_per_page
y cat
. Ejemplo sin el filtro pre_get_posts
:
$catID = 1;
$catQuery = new WP_Query( array(
'posts_per_page' => -1,
'cat' => $catID,
) );
// Agregar un encabezado:
printf( '<h1>%s</h1>', number_format_i18n( $catQuery->found_posts )
.__( " Posts archivados bajo ", 'YourTextdomain' )
.get_cat_name( $catID ) );
Construyendo una base
Lo siguiente que necesitamos es un pequeño plugin personalizado (o simplemente colocarlo en tu archivo functions.php
si no te importa moverlo durante actualizaciones o cambios de tema):
<?php
/**
* Plugin Name: (#130009) Fusionar Dos Consultas
* Description: "Fusiona" dos consultas utilizando un <code>RecursiveFilterIterator</code> para dividir una consulta principal en dos consultas
* Plugin URl: http://wordpress.stackexchange.com/questions/130009/how-to-merge-two-queries-together
*/
class ThumbnailFilter extends FilterIterator implements Countable
{
private $wp_query;
private $allowed;
private $counter = 0;
public function __construct( Iterator $iterator, WP_Query $wp_query )
{
NULL === $this->wp_query AND $this->wp_query = $wp_query;
// Ahorra tiempo de procesamiento guardándolo una vez
NULL === $this->allowed
AND $this->allowed = $this->wp_query->have_posts();
parent::__construct( $iterator );
}
public function accept()
{
if (
! $this->allowed
OR ! $this->current() instanceof WP_Post
)
return FALSE;
// Cambiar índice, configurar datos del post, etc.
$this->wp_query->the_post();
// Último WP_Post alcanzado: Configurar WP_Query para el siguiente bucle
$this->wp_query->current_post === $this->wp_query->query_vars['posts_per_page'] -1
AND $this->wp_query->rewind_posts();
// ¿No cumple los criterios? Abortar.
if ( $this->deny() )
return FALSE;
$this->counter++;
return TRUE;
}
public function deny()
{
return ! has_post_thumbnail( $this->current()->ID );
}
public function count()
{
return $this->counter;
}
}
Este plugin hace una cosa: Utiliza la SPL de PHP (Biblioteca Estándar de PHP) y sus Interfaces e Iteradores. Lo que tenemos ahora es un FilterIterator
que nos permite eliminar convenientemente elementos de nuestro bucle. Extiende el Filter Iterator de PHP SPL, por lo que no tenemos que configurar todo. El código está bien comentado, pero aquí hay algunas notas:
- El método
accept()
permite definir criterios que permiten iterar el elemento o no. - Dentro de ese método usamos
WP_Query::the_post()
, por lo que puedes usar simplemente cualquier etiqueta de plantilla en el bucle de tus archivos de plantilla. - También estamos monitoreando el bucle y rebobinando los posts cuando alcanzamos el último elemento. Esto permite iterar a través de una cantidad infinita de bucles sin reiniciar nuestra consulta.
- Hay un método personalizado que no es parte de las especificaciones del
FilterIterator
:deny()
. Este método es especialmente conveniente ya que contiene solo nuestra declaración "procesar o no" y podemos sobrescribirlo fácilmente en clases posteriores sin necesidad de saber nada aparte de las etiquetas de plantilla de WordPress.
¿Cómo iterar?
Con este nuevo Iterator, ya no necesitamos if ( $customQuery->have_posts() )
y while ( $customQuery->have_posts() )
. Podemos usar una simple declaración foreach
ya que todas las verificaciones necesarias ya están hechas por nosotros. Ejemplo:
global $wp_query;
// Primero necesitamos un ArrayObject hecho de los posts actuales
$arrayObj = new ArrayObject( $wp_query->get_posts() );
// Luego necesitamos pasarlo a nuestro nuevo Filter Iterator personalizado
// Pasamos el objeto $wp_query como segundo argumento para mantener el seguimiento
$primaryQuery = new ThumbnailFilter( $arrayObj->getIterator(), $wp_query );
Finalmente, no necesitamos nada más que un bucle foreach
predeterminado. Incluso podemos omitir the_post()
y seguir usando todas las etiquetas de plantilla. El objeto global $post
siempre permanecerá sincronizado.
foreach ( $primaryQuery as $post )
{
var_dump( get_the_ID() );
}
Bucles subsidiarios
Lo bueno es que cada filtro de consulta posterior es bastante fácil de manejar: simplemente define el método deny()
y estarás listo para tu próximo bucle. $this->current()
siempre apuntará a nuestro post actualmente iterado.
class NoThumbnailFilter extends ThumbnailFilter
{
public function deny()
{
return has_post_thumbnail( $this->current()->ID );
}
}
Como definimos que ahora deny()
iterará cada post que tenga una miniatura, entonces podemos iterar instantáneamente todos los posts sin miniatura:
foreach ( $secondaryQuery as $post )
{
var_dump( get_the_title( get_the_ID() ) );
}
Pruébalo.
El siguiente plugin de prueba está disponible como Gist en GitHub. Simplemente súbelo y actívalo. Muestra/vuelca el ID de cada post iterado como callback en la acción loop_start
. Esto significa que podrías obtener bastante salida dependiendo de tu configuración, número de posts y configuración. Por favor, agrega algunas declaraciones de aborto y modifica los var_dump()
al final para ver lo que quieras y donde quieras verlo. Es solo una prueba de concepto.

Aunque esta no es la mejor manera de resolver este problema (la respuesta de @kaiser lo es), para responder directamente a la pregunta, los resultados reales de la consulta estarán en $loop->posts
y $loop2->posts
, así que...
$mergedloops = array_merge($loop->posts, $loop2->posts);
... debería funcionar, pero necesitarías usar un bucle foreach
y no la estructura estándar del bucle basado en WP_Query
, ya que fusionar consultas de esta manera romperá los metadatos del objeto WP_Query
sobre el bucle.
También puedes hacer esto:
$loop = new WP_Query( array('fields' => 'ids','meta_key' => '_thumbnail_id', 'cat' => 1 ) );
$loop2 = new WP_Query( array('fields' => 'ids','meta_key' => '', 'cat' => 1 ) );
$ids = array_merge($loop->posts, $loop2->posts);
$merged = new WP_Query(array('post__in' => $ids,'orderby' => 'post__in'));
Por supuesto, esas soluciones representan múltiples consultas, por lo que el enfoque de @Kaiser es mejor para casos como este donde WP_Query
puede manejar la lógica necesaria.

Lo que realmente necesitas es una tercera consulta para obtener todas las publicaciones a la vez. Luego modificas tus dos primeras consultas para que no devuelvan las publicaciones, sino solo los IDs de las publicaciones en un formato con el que puedas trabajar.
El parámetro 'fields'=>'ids'
hará que una consulta devuelva un array de números de ID de publicaciones coincidentes. Pero no queremos todo el objeto de consulta, así que usamos get_posts para estos casos.
Primero, obtenemos los IDs de publicaciones que necesitamos:
$imageposts = get_posts( array('fields'=>'ids', 'meta_key' => '_thumbnail_id', 'cat' => 1 ) );
$nonimageposts = get_posts( array('fields'=>'ids', 'meta_key' => '', 'cat' => 1 ) );
$imageposts y $nonimageposts ahora serán ambos un array de números de ID de publicaciones, así que los combinamos
$mypostids = array_merge( $imageposts, $nonimageposts );
Eliminamos los IDs duplicados...
$mypostids = array_unique( $mypostids );
Ahora, hacemos una consulta para obtener las publicaciones reales en el orden especificado:
$loop = new WP_Query( array('post__in' => $mypostids, 'ignore_sticky_posts' => true, 'orderby' => 'post__in' ) );
La variable $loop es ahora un objeto WP_Query con tus publicaciones dentro.

Gracias por esto. Encontré que esta es la solución menos complicada para mantener una estructura de bucle única y cálculos de paginación sin complicaciones.

En realidad existe meta_query
(o WP_Meta_Query
) - que recibe un array de arrays - donde puedes buscar las filas de _thumbnail_id
. Si luego verificas con EXISTS
, podrás obtener solo aquellos que tienen este campo. Combinando esto con el argumento cat
, solo obtendrás publicaciones asignadas a la categoría con el ID 1
y que tengan una miniatura adjunta. Si luego las ordenas por meta_value_num
, en realidad las estarás ordenando por el ID de la miniatura de menor a mayor (como se indica con order
y ASC
). No es necesario especificar el value
cuando usas EXISTS
como valor de compare
.
$thumbsUp = new WP_Query( array(
'cat' => 1,
'meta_query' => array(
array(
'key' => '_thumbnail_id',
'compare' => 'EXISTS',
),
),
'orderby' => 'meta_value_num',
'order' => 'ASC',
) );
Ahora, al recorrerlas, puedes recolectar todos los IDs y usarlos en una declaración exclusiva para la consulta secundaria:
$postsWithThumbnails = array();
if ( $thumbsUp->have_posts() )
{
while ( $thumbsUp->have_posts() )
{
$thumbsUp->the_post();
// recopilarlos
$postsWithThumbnails[] = get_the_ID();
// hacer cosas de visualización/renderizado aquí
}
}
Ahora puedes agregar tu segunda consulta. No es necesario usar wp_reset_postdata()
aquí - todo está en la variable y no en la consulta principal.
$noThumbnails = new WP_Query( array(
'cat' => 1,
'post__not_in' => $postsWithThumbnails
) );
// Recorrer estas publicaciones
Por supuesto, puedes ser mucho más inteligente y simplemente modificar la sentencia SQL dentro de pre_get_posts
para no desperdiciar la consulta principal. También podrías simplemente hacer la primera consulta ($thumbsUp
anterior) dentro de un callback del filtro pre_get_posts
.
add_filter( 'pre_get_posts', 'wpse130009excludeThumbsPosts' );
function wpse130009excludeThumbsPosts( $query )
{
if ( $query->is_admin() )
return $query;
if ( ! $query->is_main_query() )
return $query;
if ( 'post' !== $query->get( 'post_type' ) )
return $query;
// Solo necesario si esta consulta es para el archivo de categoría para cat 1
if (
$query->is_archive()
AND ! $query->is_category( 1 )
)
return $query;
$query->set( 'meta_query', array(
array(
'key' => '_thumbnail_id',
'compare' => 'EXISTS',
),
) );
$query->set( 'orderby', 'meta_value_num' );
// En caso de que no estemos en la página de archivo de categoría cat = 1, necesitamos lo siguiente:
$query->set( 'category__in', 1 );
return $query;
}
Esto modifica la consulta principal, por lo que solo obtendremos publicaciones que tengan una miniatura adjunta. Ahora podemos (como se muestra en la primera consulta anterior) recolectar los IDs durante el bucle principal y luego agregar una segunda consulta que muestre el resto de las publicaciones (sin miniatura).
Además, puedes ser aún más inteligente y modificar posts_clauses
para alterar la consulta directamente y ordenar por el valor meta. Echa un vistazo a esta respuesta ya que la actual es solo un punto de partida.
