Como mesclar duas consultas juntas

10

Estou tentando fazer o pedido das postagens em uma categoria, mostrando as postagens com imagens primeiro e depois as postagens sem imagens por último. Eu consegui fazer isso executando duas consultas e agora quero mesclar as duas consultas juntas.

Eu tenho o seguinte:

<?php
$loop = new WP_Query( array('meta_key' => '_thumbnail_id', 'cat' => 1 ) );
$loop2 = new WP_Query( array('meta_key' => '', 'cat' => 1 ) );
$mergedloops = array_merge($loop, $loop2);

while($mergedloops->have_posts()): $mergedloops->the_post(); ?>

Mas quando tento ver a página, obtenho o seguinte erro:

 Fatal error: Call to a member function have_posts() on a non-object in...

Eu, então, tentei converter array_merge em um objeto, mas recebi o seguinte erro:

Fatal error: Call to undefined method stdClass::have_posts() in...

Como posso corrigir esse erro?

    
por Howli 16.01.2014 / 16:12

4 respostas

8

Uma única consulta

Pense nisso um pouco mais e há uma chance de você escolher uma única consulta principal. Ou, em outras palavras: não há necessidade de duas consultas adicionais quando você pode trabalhar com o padrão. E, caso você não consiga trabalhar com um padrão, não será necessário mais do que uma única consulta, independentemente de quantos loops quiser dividir a consulta.

Pré-requisitos

Primeiro, você precisa definir (como mostrado na minha outra resposta) os valores necessários dentro de um filtro pre_get_posts . É provável que você defina posts_per_page e cat . Exemplo sem o pre_get_posts -Filter:

$catID = 1;
$catQuery = new WP_Query( array(
    'posts_per_page' => -1,
    'cat'            => $catID,
) );
// Add a headline:
printf( '<h1>%s</h1>', number_format_i18n( $catQuery->found_posts )
    .__( " Posts filed under ", 'YourTextdomain' )
    .get_cat_name( $catID ) );

Construindo uma base

A próxima coisa que precisamos é de um pequeno plugin personalizado (ou apenas colocá-lo no seu arquivo functions.php se você não se importa em movê-lo durante atualizações ou mudanças de tema):

<?php
/**
 * Plugin Name: (#130009) Merge Two Queries
 * Description: "Merges" two queries by using a <code>RecursiveFilterIterator</code> to divide one main query into two queries
 * 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;

        // Save some processing time by saving it once
        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;

        // Switch index, Setup post data, etc.
        $this->wp_query->the_post();

        // Last WP_Post reached: Setup WP_Query for next loop
        $this->wp_query->current_post === $this->wp_query->query_vars['posts_per_page'] -1
            AND $this->wp_query->rewind_posts();

        // Doesn't meet criteria? Abort.
        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 faz uma coisa: Ele utiliza o PHP SPL (Biblioteca PHP Padrão) e suas Interfaces e Iteradores. O que temos agora é um FilterIterator que nos permite remover itens do nosso loop. Ele estende o Iterador de Filtro PHP SPL para que não tenhamos que definir tudo. O código é bem comentado, mas aqui estão algumas notas:

  1. O método accept() permite definir critérios que permitem o loop do item - ou não.
  2. Dentro desse método, usamos WP_Query::the_post() , portanto, você pode simplesmente usar todas as tags de modelo em seu loop de arquivos de modelo.
  3. E também estamos monitorando o loop e rebobinando as postagens quando chegamos ao último item. Isso permite fazer loop por uma quantidade infinita de loops sem redefinir nossa consulta.
  4. Existe um método personalizado que não faz parte das especificações FilterIterator : deny() . Este método é especialmente conveniente, pois contém apenas a nossa declaração "processo ou não" e podemos facilmente sobrescrevê-lo em classes posteriores sem precisar saber nada além das tags de template do WordPress.

Como fazer um loop?

Com esse novo Iterador, não precisamos mais de if ( $customQuery->have_posts() ) e while ( $customQuery->have_posts() ) . Podemos usar uma instrução foreach simples, pois todas as verificações necessárias já foram feitas para nós. Exemplo:

global $wp_query;
// First we need an ArrayObject made out of the actual posts
$arrayObj = new ArrayObject( $wp_query->get_posts() );
// Then we need to throw it into our new custom Filter Iterator
// We pass the $wp_query object in as second argument to keep track with it
$primaryQuery = new ThumbnailFilter( $arrayObj->getIterator(), $wp_query );

Finalmente, não precisamos de nada além de um loop foreach padrão. Podemos até mesmo descartar the_post() e ainda usar todas as tags de modelo. O objeto global $post sempre permanecerá em sincronia.

foreach ( $primaryQuery as $post )
{
    var_dump( get_the_ID() );
}

Loops subsidiários

Agora, o bom é que cada filtro de consulta posterior é muito fácil de manusear: Simplesmente defina o método deny() e você está pronto para o próximo ciclo. $this->current() sempre apontará para nossa postagem atualmente em loop.

class NoThumbnailFilter extends ThumbnailFilter
{
    public function deny()
    {
        return has_post_thumbnail( $this->current()->ID );
    }
}

Como definimos que agora deny() faz um loop em todas as postagens que têm uma miniatura, podemos fazer um loop instantâneo de todas as postagens sem uma miniatura:

foreach ( $secondaryQuery as $post )
{
    var_dump( get_the_title( get_the_ID() ) );
}

Teste.

O seguinte plug-in de teste está disponível como Gist no GitHub. Basta fazer o upload e ativá-lo. Ele envia / copia o ID de todas as postagens em loop como retorno de chamada na ação loop_start . Isso significa que pode ter uma saída bastante dependendo da configuração, do número de postagens e da configuração. Por favor, adicione algumas declarações de abortar e altere o var_dump() s no final para o que você deseja ver e onde deseja vê-lo. É apenas uma prova de conceito.

    
por kaiser 16.01.2014 / 23:45
6

Embora esta não seja a melhor maneira de resolver este problema (a resposta do kaiser é), para responder à pergunta diretamente, os resultados reais da consulta estarão em $loop->posts e $loop2->posts , então ...

$mergedloops = array_merge($loop->posts, $loop2->posts);

... deve funcionar, mas você precisaria usar um loop foreach e não a estrutura de loop padrão baseada em WP_Query , pois a mesclagem de consultas como essa quebraria os dados "meta" do objeto WP_Query sobre o loop .

Você também pode fazer isso:

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

Naturalmente, essas soluções representam várias consultas, e é por isso que o @ Kaiser é a melhor abordagem para casos como este, em que WP_Query pode lidar com a lógica necessária.

    
por s_ha_dum 16.01.2014 / 17:01
3

Na verdade, há meta_query (ou WP_Meta_Query ) - que usa uma matriz de matrizes - onde você pode pesquisar para as linhas _thumbnail_id . Se você verificar por EXISTS , poderá obter apenas aqueles que possuem esse campo. Combinando isso com o argumento cat , você só conseguirá postagens que são atribuídas à categoria com o ID de 1 e que tenham uma miniatura anexada. Se você, então, encomendá-los pelo meta_value_num , você irá encomendá-los pelo ID de miniatura menor a maior (conforme indicado em order e ASC ). Você não precisa especificar o value quando usar o valor EXISTS as compare .

$thumbsUp = new WP_Query( array( 
    'cat'        => 1,
    'meta_query' => array( 
        array(
            'key'     => '_thumbnail_id',
            'compare' => 'EXISTS',
        ),
    ),
    'orderby'    => 'meta_value_num',
    'order'      => 'ASC',
) );

Agora, quando fizer um loop por eles, você poderá coletar todos os IDs e usá-los em uma instrução exclusiva para a consulta subsidiária:

$postsWithThumbnails = array();
if ( $thumbsUp->have_posts() )
{
    while ( $thumbsUp->have_posts() )
    {
        $thumbsUp->the_post();

        // collect them
        $postsWithThumbnails[] = get_the_ID();

        // do display/rendering stuff here
    }
}

Agora você pode adicionar sua segunda consulta. Não há necessidade de wp_reset_postdata() aqui - tudo está na variável e não na consulta principal.

$noThumbnails = new WP_Query( array(
    'cat'          => 1,
    'post__not_in' => $postsWithThumbnails
) );
// Loop through this posts

Claro que você pode ser muito mais esperto e simplesmente alterar a instrução SQL dentro de pre_get_posts para não desperdiçar a consulta principal. Você poderia simplesmente fazer a primeira consulta ( $thumbsUp acima) dentro de um retorno de chamada do 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;

    // Only needed if this query is for the category archive for 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' );

    // In case we're not on the cat = 1 category archive page, we need the following:
    $query->set( 'category__in', 1 );

    return $query;
}

Isso alterou a consulta principal. Por isso, só conseguiremos postagens com uma miniatura anexada. Agora podemos (como mostrado na primeira consulta acima) coletar os IDs durante o loop principal e, em seguida, adicionar uma segunda consulta que exibe o restante dos posts (sem uma miniatura).

Além disso, você pode ficar ainda mais inteligente e alterar posts_clauses e modificar a consulta diretamente pelo valor meta. Dê uma olhada em esta resposta , pois a atual é apenas um ponto de partida.

    
por kaiser 16.01.2014 / 17:00
3

O que você precisa é, na verdade, uma terceira consulta para obter todas as postagens de uma só vez. Em seguida, você altera suas duas primeiras consultas para não retornar as postagens, mas apenas as IDs de postagem em um formato com o qual você pode trabalhar.

O parâmetro 'fields'=>'ids' fará com que uma consulta realmente retorne uma matriz de números de ID de postagem correspondentes. Mas nós não queremos o objeto de consulta inteiro, então usamos get_posts para estes em vez disso.

Primeiro, pegue os códigos de postagem de que precisamos:

$imageposts = get_posts( array('fields'=>'ids', 'meta_key' => '_thumbnail_id', 'cat' => 1 ) );
$nonimageposts = get_posts( array('fields'=>'ids', 'meta_key' => '', 'cat' => 1 ) );

$ imageposts e $ nonimageposts agora serão ambos uma matriz de números de ID de postagem, então os mesclamos

$mypostids = array_merge( $imageposts, $nonimageposts );

Elimine os números de ID duplicados ...

$mypostids = array_unique( $mypostids );

Agora, faça uma consulta para obter as postagens reais na ordem especificada:

$loop = new WP_Query( array('post__in' => $mypostids, 'ignore_sticky_posts' => true, 'orderby' => 'post__in' ) );

A variável $ loop agora é um objeto WP_Query com suas postagens nela.

    
por Otto 16.01.2014 / 20:30