Come unire due query insieme
Sto cercando di ordinare i post in una categoria mostrando prima i post con immagini e poi quelli senza immagini. Sono riuscito a farlo eseguendo due query separate e ora vorrei unire queste due query insieme.
Ho il seguente codice:
<?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(); ?>
Ma quando provo a visualizzare la pagina ottengo il seguente errore:
Errore fatale: Chiamata a una funzione membro have_posts() su un oggetto non valido in...
Ho quindi provato a convertire array_merge in un oggetto, ma ho ottenuto questo errore:
Errore fatale: Chiamata a metodo non definito stdClass::have_posts() in...
Come posso risolvere questo errore?
Una singola query
Ho riflettuto un po' di più su questo e c'è la possibilità che tu possa procedere con una singola/la query principale. In altre parole: non c'è bisogno di due query aggiuntive quando puoi lavorare con quella predefinita. E nel caso in cui non puoi lavorare con una query predefinita, non avrai bisogno di più di una singola query indipendentemente da quante loop vuoi dividere la query.
Prerequisiti
Innanzitutto devi impostare (come mostrato nella mia altra risposta) i valori necessari all'interno di un filtro pre_get_posts
. Lì probabilmente imposterai posts_per_page
e cat
. Esempio senza il filtro pre_get_posts
:
$catID = 1;
$catQuery = new WP_Query( array(
'posts_per_page' => -1,
'cat' => $catID,
) );
// Aggiungi un titolo:
printf( '<h1>%s</h1>', number_format_i18n( $catQuery->found_posts )
.__( " Articoli archiviati sotto ", 'YourTextdomain' )
.get_cat_name( $catID ) );
Costruire una base
La prossima cosa di cui abbiamo bisogno è un piccolo plugin personalizzato (o semplicemente inseriscilo nel tuo file functions.php
se non ti dispiace spostarlo durante aggiornamenti o cambi di tema):
<?php
/**
* Plugin Name: (#130009) Unisci Due Query
* Description: "Unisce" due query utilizzando un <code>RecursiveFilterIterator</code> per dividere una query principale in due query
* 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;
// Risparmia tempo di elaborazione salvandolo una volta
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;
// Cambia indice, imposta dati post, ecc.
$this->wp_query->the_post();
// Ultimo WP_Post raggiunto: Imposta WP_Query per il prossimo loop
$this->wp_query->current_post === $this->wp_query->query_vars['posts_per_page'] -1
AND $this->wp_query->rewind_posts();
// Non soddisfa i criteri? Interrompi.
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;
}
}
Questo plugin fa una cosa: utilizza la PHP SPL (Standard PHP Library) e le sue interfacce e iteratori. Quello che abbiamo ora è un FilterIterator
che ci consente di rimuovere comodamente elementi dal nostro loop. Estende il PHP SPL Filter Iterator quindi non dobbiamo impostare tutto. Il codice è ben commentato, ma ecco alcune note:
- Il metodo
accept()
consente di definire criteri che consentono di iterare l'elemento - oppure no. - All'interno di quel metodo usiamo
WP_Query::the_post()
, quindi puoi semplicemente usare ogni template tag nel loop dei tuoi file template. - E inoltre monitoriamo il loop e riavvolgiamo i post quando raggiungiamo l'ultimo elemento. Questo consente di iterare attraverso un numero infinito di loop senza resettare la nostra query.
- C'è un metodo personalizzato che non fa parte delle specifiche di
FilterIterator
:deny()
. Questo metodo è particolarmente comodo poiché contiene solo la nostra dichiarazione "elabora o non elabora" e possiamo facilmente sovrascriverlo in classi successive senza bisogno di sapere altro oltre ai template tag di WordPress.
Come iterare?
Con questo nuovo Iterator, non abbiamo più bisogno di if ( $customQuery->have_posts() )
e while ( $customQuery->have_posts() )
. Possiamo usare una semplice istruzione foreach
poiché tutti i controlli necessari sono già stati fatti per noi. Esempio:
global $wp_query;
// Prima abbiamo bisogno di un ArrayObject fatto dai post attuali
$arrayObj = new ArrayObject( $wp_query->get_posts() );
// Poi dobbiamo passarlo nel nostro nuovo custom Filter Iterator
// Passiamo l'oggetto $wp_query come secondo argomento per tenerne traccia
$primaryQuery = new ThumbnailFilter( $arrayObj->getIterator(), $wp_query );
Infine non ci serve altro che un normale loop foreach
. Possiamo anche eliminare the_post()
e usare comunque tutti i template tag. L'oggetto globale $post
rimarrà sempre sincronizzato.
foreach ( $primaryQuery as $post )
{
var_dump( get_the_ID() );
}
Loop secondari
Ora la cosa bella è che ogni filtro di query successivo è abbastanza facile da gestire: basta definire il metodo deny()
e sei pronto per il tuo prossimo loop. $this->current()
punterà sempre al nostro post attualmente iterato.
class NoThumbnailFilter extends ThumbnailFilter
{
public function deny()
{
return has_post_thumbnail( $this->current()->ID );
}
}
Poiché abbiamo definito che ora deny()
impedisce di iterare ogni post che ha una miniatura, possiamo quindi iterare immediatamente tutti i post senza miniatura:
foreach ( $secondaryQuery as $post )
{
var_dump( get_the_title( get_the_ID() ) );
}
Provalo.
Il seguente plugin di test è disponibile come Gist su GitHub. Basta caricarlo e attivarlo. Mostra/dumpa l'ID di ogni post iterato come callback sull'azione loop_start
. Questo significa che potresti ottenere un po' di output a seconda della tua configurazione, numero di post e impostazioni. Per favore aggiungi alcune istruzioni di interruzione e modifica gli var_dump()
alla fine con ciò che vuoi vedere e dove vuoi vederlo. È solo una prova di concetto.

Anche se questa non è la soluzione migliore per risolvere il problema (la risposta di @kaiser lo è), per rispondere direttamente alla domanda, i risultati effettivi della query saranno in $loop->posts
e $loop2->posts
, quindi ...
$mergedloops = array_merge($loop->posts, $loop2->posts);
... dovrebbe funzionare, ma sarebbe necessario utilizzare un ciclo foreach
e non la struttura standard del loop basata su WP_Query
, poiché unire le query in questo modo romperà i metadati dell'oggetto "meta" WP_Query
relativi al loop.
Puoi anche fare questo:
$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'));
Ovviamente, queste soluzioni rappresentano query multiple, motivo per cui l'approccio di @Kaiser è migliore per casi come questo in cui WP_Query
può gestire la logica necessaria.

Ciò di cui hai effettivamente bisogno è una terza query per ottenere tutti i post in una sola volta. Poi modifichi le prime due query in modo che non restituiscano i post, ma solo gli ID dei post in un formato con cui puoi lavorare.
Il parametro 'fields'=>'ids'
farà sì che una query restituisca un array di numeri ID dei post corrispondenti. Ma non vogliamo l'intero oggetto query, quindi usiamo get_posts per questi.
Per prima cosa, ottieni gli ID dei post di cui abbiamo bisogno:
$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 saranno ora entrambi un array di numeri ID dei post, quindi li uniamo
$mypostids = array_merge( $imageposts, $nonimageposts );
Elimina gli ID duplicati...
$mypostids = array_unique( $mypostids );
Ora, crea una query per ottenere i post effettivi nell'ordine specificato:
$loop = new WP_Query( array('post__in' => $mypostids, 'ignore_sticky_posts' => true, 'orderby' => 'post__in' ) );
La variabile $loop è ora un oggetto WP_Query con i tuoi post all'interno.

Grazie per questo. Ho trovato questa la soluzione meno complicata per mantenere una singola struttura di loop e calcoli di paginazione non complessi.

In realtà esiste meta_query
(o WP_Meta_Query
) - che accetta un array di array - dove puoi cercare le righe _thumbnail_id
. Se poi controlli per EXISTS
, sarai in grado di ottenere solo quelli che hanno questo campo. Combinando questo con l'argomento cat
, otterrai solo i post assegnati alla categoria con ID 1
e che hanno una miniatura allegata. Se poi li ordini per meta_value_num
, li ordinerai effettivamente per ID miniatura dal più basso al più alto (come specificato con order
e ASC
). Non è necessario specificare il value
quando si utilizza EXISTS
come valore di compare
.
$thumbsUp = new WP_Query( array(
'cat' => 1,
'meta_query' => array(
array(
'key' => '_thumbnail_id',
'compare' => 'EXISTS',
),
),
'orderby' => 'meta_value_num',
'order' => 'ASC',
) );
Ora, durante il loop, puoi raccogliere tutti gli ID e utilizzarli in un'istruzione esclusiva per la query secondaria:
$postsWithThumbnails = array();
if ( $thumbsUp->have_posts() )
{
while ( $thumbsUp->have_posts() )
{
$thumbsUp->the_post();
// raccoglili
$postsWithThumbnails[] = get_the_ID();
// fai qui le operazioni di visualizzazione/rendering
}
}
Ora puoi aggiungere la tua seconda query. Non c'è bisogno di wp_reset_postdata()
qui - tutto è nella variabile e non nella query principale.
$noThumbnails = new WP_Query( array(
'cat' => 1,
'post__not_in' => $postsWithThumbnails
) );
// Esegui il loop attraverso questi post
Ovviamente puoi essere molto più furbo e modificare direttamente l'istruzione SQL all'interno di pre_get_posts
per non sprecare la query principale. Potresti anche semplicemente fare la prima query ($thumbsUp
sopra) all'interno di 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;
// Necessario solo se questa query è per l'archivio della categoria con ID 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' );
// Nel caso non siamo nella pagina archivio della categoria con ID 1, abbiamo bisogno di questo:
$query->set( 'category__in', 1 );
return $query;
}
Questo modifica la query principale, quindi otterremo solo i post che hanno una miniatura allegata. Ora possiamo (come mostrato nella prima query sopra) raccogliere gli ID durante il loop principale e poi aggiungere una seconda query che mostra i post rimanenti (senza miniatura).
Oltre a questo, puoi essere ancora più furbo e modificare posts_clauses
per alterare direttamente la query ordinando per il valore meta. Dai un'occhiata a questa risposta poiché quella attuale è solo un punto di partenza.
