Como mesclar duas consultas

10

Estou tentando ordenar as 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.

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 visualizar a página, recebo o seguinte erro:

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

Tentei converter array_merge para um objeto, mas recebi o seguinte erro:

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

Como posso corrigir este erro?

Howli
fonte

Respostas:

8

Uma única consulta

Pensei um pouco mais sobre isso e há uma chance de você ir com uma única consulta / a principal. Ou, em outras palavras: Não há necessidade de duas consultas adicionais quando você pode trabalhar com a padrão. E, caso você não consiga trabalhar com uma padrão, não precisará de mais do que uma única consulta, independentemente de quantos ciclos desejar dividir.

Pré-requisitos

Primeiro, você precisa definir (como mostrado na minha outra resposta) os valores necessários dentro de um pre_get_postsfiltro. Lá você provavelmente definirá posts_per_pagee 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 plug-in personalizado (ou apenas o coloque no seu functions.phparquivo, se você não se importar em movê-lo durante atualizações ou alterações 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: utiliza o PHP SPL (Standard PHP Library) e suas interfaces e iteradores. O que temos agora é um FilterIteratorque nos permite remover convenientemente itens do nosso loop. Ele estende o Iterador de Filtro PHP SPL para que não tenhamos que definir tudo. O código está bem comentado, mas aqui estão algumas notas:

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

Como fazer um loop?

Com esta nova Iterator, nós não precisamos if ( $customQuery->have_posts() )e while ( $customQuery->have_posts() )mais. Podemos usar uma foreachdeclaração 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 foreachloop padrão . Podemos até soltar the_post()e ainda usar todas as tags de modelo. O $postobjeto global sempre permanecerá sincronizado.

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

Loops subsidiários

Agora, o mais interessante é que todo filtro de consulta posterior é bastante fácil de manipular: basta definir o deny()método e você estará pronto para o próximo ciclo. $this->current()sempre apontará para nossa postagem em loop no momento.

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

Como definimos que agora fazemos o deny()loop de 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.

O seguinte plugin de teste está disponível como Gist no GitHub. Basta fazer o upload e ativá-lo. Ele gera / despeja o ID de cada postagem em loop como retorno de chamada na loop_startação. Isso significa que você pode obter um pouco de saída, dependendo da sua instalação, número de posts e configuração. Por favor, adicione algumas instruções de cancelamento e altere os var_dump()s no final do que você deseja ver e onde deseja vê-lo. É apenas uma prova de conceito.

kaiser
fonte
6

Embora essa não seja a melhor maneira de resolver esse problema (a resposta do @ kaiser é), para responder diretamente à pergunta, os resultados reais da consulta estarão dentro $loop->postse $loop2->posts, portanto ...

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

... deve funcionar, mas você precisará usar um foreachloop, e não a WP_Queryestrutura de loop padrão, pois a mesclagem de consultas como essa quebrará os WP_Querydados "meta" do objeto 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'));

Obviamente, essas soluções representam várias consultas, e é por isso que o @ Kaiser's é a melhor abordagem para casos como este, onde WP_Querypode lidar com a lógica necessária.

s_ha_dum
fonte
3

Na verdade, há meta_query(ou WP_Meta_Query) - que leva uma matriz de matrizes - onde você pode procurar as _thumbnail_idlinhas. Se você procurar EXISTS, poderá obter apenas aqueles que possuem esse campo. Combinando isso com o catargumento, você receberá apenas postagens atribuídas à categoria com o ID 1e que tenham uma miniatura anexada. Se você ordená-los por meta_value_num,, na verdade, os ordenará pelo ID da miniatura, do menor para o maior (conforme declarado com ordere ASC). Você não precisa especificar valuequando usar EXISTScomo comparevalor.

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

Agora, ao fazer um loop entre eles, você pode 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 é necessário 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 inteligente e simplesmente alterar a instrução SQL pre_get_postspara não desperdiçar a consulta principal. Você também pode simplesmente fazer a primeira consulta ( $thumbsUpacima) dentro de um pre_get_postsretorno de chamada de filtro.

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;
}

Como a consulta principal foi alterada, teremos apenas postagens com uma miniatura anexada. Agora podemos (como mostrado na 1ª consulta acima) coletar os IDs durante o loop principal e adicionar uma segunda consulta que exibe o restante das postagens (sem uma miniatura).

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

kaiser
fonte
3

O que você precisa é realmente 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 os IDs das postagens em um formato com o qual você possa trabalhar.

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

Primeiro, obtenha os IDs 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, por isso os mesclamos

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

Eliminar os números de identificação 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.

Otto
fonte
Obrigado por isso. Considerou que esta é a solução menos complicada para manter uma estrutura de loop e cálculos de paginação descomplicados.
Jay Neely