Cómo combinar dos consultas en WordPress

16 ene 2014, 17:12:41
Vistas: 14.4K
Votos: 14

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?

0
Todas las respuestas a la pregunta 4
0

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:

  1. El método accept() permite definir criterios que permiten iterar el elemento o no.
  2. 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.
  3. 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.
  4. 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.

17 ene 2014 00:45:31
0

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.

16 ene 2014 18:01:20
2

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.

16 ene 2014 21:30:38
Comentarios

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.

Jay Neely Jay Neely
7 mar 2016 19:46:29

Esto parece una solución simple y más versátil. +1 a la solución del propio Sr. WordPress.

samjco-com samjco-com
19 ene 2021 08:49:43
0

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.

16 ene 2014 18:00:53