Pobieranie wpisów bez używania query_posts

Tym razem wpis wyjątkowy, bo nie będący naszego autorstwa. To tłumaczenie tekstu, jaki opublikował John James Jacoby z Automattic, który pierwotnie ukazał się na blogu developerów z tej firmy. Problem w nim poruszony uznałem za tak ważny (i zarazem uważam podobnie jak autor, że query_posts() jest nadużywane w niewłaściwy sposób), że postanowiłem zapytać Jamesa czy możemy tutaj, na dev.wpzlecenia umieścić jego tłumaczenie. „Spread the words” – brzmiała odpowiedź.
Nie przedłużając – oto tekst.

Tutaj na WordPress.com mamy ponad 200 motywów (i jeszcze więcej wtyczek) działających na największej na świecie instalacji WordPressa (tak przynajmniej nam się wydaje!). Wśród tego całego kodu, rozsianego po całej planecie na ponad dwóch tysiącach serwerów, jest pewna wordpressowa funkcja, którą ze wstydem chcielibyśmy ukryć: query_posts().

Jeżeli wydaje ci się, że musisz jej użyć, wiedz, że z pewnością jest lepsze rozwiązanie. Bo query_posts() wcale nie robi tego, co większości z nas się wydaje.

Myślimy, że query posts():

  • resetuje główną pętlę z wpisami
  • resetuje główną, globalną zmienną post

A tak naprawdę:

  • tworzy ona nowy, kolejny obiekt WP_Query z parametrami jakie mu przekażesz
  • zastępuje główną pętle z wpisami tą nową (i tym samym pętla ta przestaje być pętlą główną)

Czujesz się zmieszany? Jeśli tak, to nie przejmuj się: tysiące innych osób czuje w tej chwili dokładnie to samo.

Oto jak query_posts faktycznie wygląda:

/**
 * Set up The Loop with query parameters.
 *
 * This will override the current WordPress Loop and shouldn't be used more than
 * once. This must not be used within the WordPress Loop.
 *
 * @since 1.5.0
 * @uses $wp_query
 *
 * @param string $query
 * @return array List of posts
 */
function &query_posts($query) {
	unset($GLOBALS['wp_query']);
	$GLOBALS['wp_query'] = new WP_Query();
	return $GLOBALS['wp_query']->query($query);
}

Rzadko, jeśli w ogóle, ktokolwiek potrzebuje tego co dzieje się w kodzie powyżej.

Najczęstszym przypadkiem jest ten, gdy tworzymy motyw graficzny, który ma wyświetlać promowane wpisy w osobnym bloku znajdującym się nad obszarem na główną treść. Poniżej jest zrzut ekranu z motywu iTheme2 jako przykład.

Rzecz o której należy pamiętać: WordPress od startu do momentu, gdy zaczyna wyświetlać promowane wpisy zdążył już:

  • spojrzeć na URL strony…
  • sparsować ten URL w poszukiwaniu wpisów, które pasują do jego wzorca…
  • pobrać te wpisy z bazy danych (lub pamięci podręcznej)…
  • wypełnić pobranymi danymi zmienne globalne $wp_query oraz $post

Pomyśl o tym jak czymś mniej więcej takim:

“Główna pętla z wpisami” (ang. Main Loop) składa się z trzech zmiennych globalnych, z których dwie mają faktycznie jakieś znaczenie:

  • $wp_the_query (bez znaczenia)
  • $wp_query (istotna)
  • $post (istotna)

Powód dla którego $wp_the_query nie ma tutaj żadnego znaczenia jest taki, że *nigdy* nie będziesz jej bezpośrednio dotykał, ani też nigdy nie powinieneś próbować tego robić. Została stworzona by zawierać główną pętlę z wpisami, nieważne jak bardzo staną się zatrute $wp_query i $post.

Wróćmy do Promowanych Wpisów

Gdy chcesz wykonać zapytanie do bazy danych by pobrać promowane wpisy, to jest właśnie dobry moment aby stworzyć nowy obiekt WP_Query i wykonać na nim mniej więcej taką pętlę…

$featured_args = array(
	'post__in' => get_option( 'sticky_posts' ),
	'post_status' => 'publish',
	'no_found_rows' => true
);

// The Featured Posts query.
$featured = new WP_Query( $featured_args );

// Proceed only if published posts with thumbnails exist
if ( $featured->have_posts() ) {
	while ( $featured->have_posts() ) {
		$featured->the_post();
		if ( has_post_thumbnail( $featured->post->ID ) ) {
			/// do stuff here
		}
	}

	// Reset the post data
	wp_reset_postdata()
}

Super! Mamy teraz dwa zapytania, żadnych konfliktów – na świecie zapanował pokój. Oczywiście pamiętasz o użyciu wp_reset_postadata(), prawda? ;) Jeśli nie, powód dlaczego należy używać tej funkcji to fakt, że każde nowe WP_Query zastępuje zmienną globalną $post efektem aktualnej iteracji aktualnie przetwarzanej pętli. Jeśli nie użyjesz po nowym WP_Query funkcji wp_reset_postdata(), skończysz z umieszczonymi w $post danymi z pętli promowanych wpisów. Kiepsko.

Pamietasz query_posts()? Spójrz na nią jeszcze raz; zastępuje $wp_query i nie zagląda do $wp_the_query by zrobić z nią to samo. Prościzna, prawda? Po prostu pobiera parametry jakie do niej przekażesz i zakłada, że to jest dokładnie to, czego chcesz. Pomęczę cię tym jeszcze za chwilę, na razie jednak kontynuujmy.

Co jeśli twoje promowane wpisy zostały już wyświetlone i nie chcesz aby wyświetlały się ponownie pod spodem w głównej pętli?

Pomyśl o tym…

Sensownie byłoby użyć query_posts() i za jej pomocą zastąpić główną pętlę $wp_query, prawda? Jednak skąd mamy wiedzieć które wpisy mamy tym razem wykluczyć i czy na pewno jet to przypadek gdy wpisyte faktycznie chcemy wykluczyć? Przecież możemy na przykład spróbować wykluczyć promowane wpisy z podstrony wyświetlającej jeden z nich.

DOKŁADNIE!

Paradoksalnie, i WordPress, i Wp_Query zostały stworzone tak aby obsłużyć to niezwykle łatwo za pomocą akcji nazywającej się ‘pre_get_posts’.

Pomyśl o tym jak o sposobie by przekonać WordPressa, że to co chce zrobić może nie zawsze jest właśnie tym co chce zrobić. W naszym wypadku zamiast pobierać wpisy TRZECI raz (najpierw główna pętla, potem pętla na promowane wpisy, w końcu trzecia pętla z query_posts() aby wykluczyć wpisy wyświetlone już w pętli drugiej) możemy z góry zmodyfikować główną pętlę i wykluczyć z niej pewne wpisy i dopiero wtedy wyświetlić te wykluczone wpisy w pętli na wpisy promowane. Genialne!

Oto jak to robimy w skórce iTheme2:

/**
 * Filter the home page posts, and remove any featured post ID's from it. Hooked
 * onto the 'pre_get_posts' action, this changes the parameters of the query
 * before it gets any posts.
 * 
 * @global array $featured_post_id
 * @param WP_Query $query
 * @return WP_Query Possibly modified WP_query
 */
function itheme2_home_posts( $query = false ) {

	// Bail if not home, not a query, not main query, or no featured posts
	if ( ! is_home() || ! is_a( $query, 'WP_Query' ) || ! $query->is_main_query() || ! itheme2_featuring_posts() )
		return;

	// Exclude featured posts from the main query
	$query->set( 'post__not_in', itheme2_featuring_posts() );

	// Note the we aren't returning anything.
	// 'pre_get_posts' is a byref action; we're modifying the query directly.
}
add_action( 'pre_get_posts', 'itheme2_home_posts' );

/**
 * Test to see if any posts meet our conditions for featuring posts.
 * Current conditions are:
 *
 * - sticky posts
 * - with featured thumbnails
 *
 * We store the results of the loop in a transient, to prevent running this
 * extra query on every page load. The results are an array of post ID's that
 * match the result above. This gives us a quick way to loop through featured
 * posts again later without needing to query additional times later.
 */
function itheme2_featuring_posts() {
	if ( false === ( $featured_post_ids = get_transient( 'featured_post_ids' ) ) ) {

		// Proceed only if sticky posts exist.
		if ( get_option( 'sticky_posts' ) ) {

			$featured_args = array(
				'post__in'      => get_option( 'sticky_posts' ),
				'post_status'   => 'publish',
				'no_found_rows' => true
			);

			// The Featured Posts query.
			$featured = new WP_Query( $featured_args );

			// Proceed only if published posts with thumbnails exist
			if ( $featured->have_posts() ) {
				while ( $featured->have_posts() ) {
					$featured->the_post();
					if ( has_post_thumbnail( $featured->post->ID ) ) {
						$featured_post_ids[] = $featured->post->ID;
					}
				}

				set_transient( 'featured_post_ids', $featured_post_ids );
			}
		}
	}

	return $featured_post_ids;
}

Odczytujemy to następująco:

  • Przefiltruj główną pętlę
  • Kontynuuj tylko jeśli jesteśmy na stronie głównej
  • Kontynuuj tylko jeśli nasze zapytanie nie jest popsute w jakiś sposób
  • Kontynuuj tylko jeśli to jest główne zapytanie
  • Kontynuuj tylko jeśli faktycznie mamy jakieś promowane wpisy
  • Promowane wpisy? Sprawdźmy czy są wpisy przyklejone
  • Pobierz wpisy jeśli istnieją
  • (W tym momencie ponownie jest uruchamiane WP_Query, a wraz z nim nasz filt ‘pre_get_posts’. Dzięki naszym testom opisanym w podpunktach wyżej nasze zapytanie o promowane wpisy nie zostanie zanieczyszczone naszą potrzebą wykluczenia promowanych wpisów)
  • Pobierz ID każdego wpisu i trzymaj go w tablicy
  • Zwróć zmodyfikowaną zmienną zawierającą główne zapytanie
  • Pozwól WordPressowi zająć się resztą

Przy odrobinie przewidywania tego, co chcemy zrobić i co się może stać udało nam się napisać kod, który pozwolił uniknąć potencjalnie kosztownego trzeciego obiektu WP_Query.

Inny, prostszy przykład

Szablon Depo Masthead ogranicza liczbę wpisów wyświetlanych na stronie głównej do trzech. Już nauczyliśmy się, że nie chcemy uruchamiać funkcji query_posts(), która stworzyła by nam kolejny, niepotrzebny obiekt WP_Query. Więc co robimy?

/**
 * Modify home query to only show 3 posts
 *
 * @param WP_Query $query
 * @return WP_Query
 */
function depo_limit_home_posts_per_page( $query = '' ) {

	// Bail if not home, not a query, not main query, or no featured posts
	if ( ! is_home() || ! is_a( $query, 'WP_Query' ) || ! $query->is_main_query() )
		return;

	// Home only gets 3 posts
	$query->set( 'posts_per_page', 3 );
}
add_action( 'pre_get_posts', 'depo_limit_home_posts_per_page' );

Powstrzymaj mnie, jeśli już o tym słyszałeś. Zrobiliśmy haka na ‘pre_get_posts’ i zwróciliśmy zmodyfikowane główne zapytanie! Ta-dam!

Szablony to główne zastosowanie, ale nie tylko one. Często zapominamy posprzątać po sobie, zresetować wpisy i zapytania o nie itp… Unikając query_posts() możemy być bardziej pewni, że nasz kod będzie zachowywał się tak, jak to założyliśmy – dotyczy to tak samo szablonów jak i wtyczek.


Opublikowano

w

przez

Komentarze

9 odpowiedzi na „Pobieranie wpisów bez używania query_posts”

  1. Awatar Aga

    Artykuł tak dobry, że wybaczam Ci te naleciałości z angielskiego typu „Czujesz się zmieszany? Powstrzymaj mnie. Prawda?” :-) Nie miałam pojęcia o możliwości modyfikacji query przez akcję pre_get_post. Zatem wielkie dzięki Konrad!

    1. Awatar Konrad Karpieszuk

      mi tez te nalecialosci przeszkadzaja :) powaznie. o ile po angielsku fajnie sie to czyta, to po polsku wychodzi takie za bardzo 'hop do przodu’. ale nie wiedzialem jak to obejsc

      1. Awatar Aga

        Konrad, najważniejsze, że artykuł merytorycznie jest świetny. Programiści i tak skupią się na technikaliach, a lingwiści będą tu raczej w mniejszości (nawet nie widzę Magdy w grawatarach).

        Ale jak już odbiłeś piłkę… Ja bym odeszła od dosłownego przekazu i przykładowo zdanie „Confused yet? It’s okay if you are, thousands of others are, too.” przetłumaczyła „Nadal masz mętlik? Nie szkodzi, nie tylko ty.”, a zdanie „Stop me if you’ve heard this one.” – „Idę o zakład, że tego nie wiedziałeś.” I resztę w ten deseń. Inaczej niż w oryginale, ale po polsku. ;-)

  2. Awatar bogdan

    Ciekawy wpis.
    Czy zatem lepiej budowe zapytania oprzec o powyzsze zasady jesli chcemy miec dwa rozne zapytania typu: promowane wpisy etc + glowne wpisy czy tez soe to tyczu glownych zapytan?

    Pozdrawiam

  3. Awatar bogdan

    Fajny temat.
    Czy można by to jeszcze bardziej wyjaśnić na innych przykładach.
    Czy to ma zastosowanie jeśli mamy main loop topowe zapytanie WP.
    W footer mamy zapytanie wyświetlające wpisy, kategorie etc. czy mam stosować się do tej reguły wyżej?

    1. Awatar Konrad Karpieszuk

      jak zwykle „wszystko zalezy”, ale generalnie tak powinno sie wybierac wpisy w kazdym przypadku.

      jednak jesli juz masz i wszystko dziala jak nalezy, imo powinienes zostawic. refaktoryzuje sie kod jesli czemus to sluzy, a nie temu ze ktos napisal na dev.wpzlecenia ze tak jest lepiej niz u Ciebie ;)

  4. Awatar bogdan

    Dziękuje za odpowiedź.

    Jestem na etapie tworzenia strony opartej właśnie o WP.
    Kiedyś zaczynałem od PHP,MySQL potem był AS i teraz znów powrót do PHP :)
    Byłem przeciwny wszelakim opensoource cms etc teraz jednak doceniam WP. jest to bardzo przyjemny system.

    ale do rzeczy.

    Rozumiem że lepszym rozwiazaniem jest zastąć wszelakie WP loopy własnym zapytaniem .
    Mam pare pytan:

    Stworzyłem pare własnych typów postów (custom post type) bo chciałem miec przy wpisach dodatkowe pola tekstowe opisujace dany artykuł.
    Jak wybrac posty ze wszystkich tych typów?
    Tworzyć oddzielne zapytania dla każdego z typów? czy da sie wypisac typy postów do tablicy poczym umiescic w zapytaniu i je wyczytać?

    Czy mogę używać za każdym razem jako globalna zmienne np $post skoro funkcja wp_reset_postdata() resetuje zmienna globalna etc.

    Powiedzmy że do postów/wpisów dodaje tagi jak i również własnych typów postów i main_loop nie widzi postów z własnych typów tylko z postów typu -> post. Rozumiem ze muszę stworzyć własne zapytanie które odwoła się do innych typów. Tylko się dziwie czemu po tagu i nie czyta całej zawartości tabeli. Moze coś źle rozumiem?

    1. Awatar Konrad Karpieszuk

      uh, dlugie pytanie, ale jako, ze jestem juz w rosole przed wyjazdem na wordcamp, nie uda mi sie na nie odpowiedziec. moze ktos inny z czytajacych?

      ewentualnie polecam abys zadal to pytanie na goldenline.pl na forum 'fani wordpressa’. na pewno znajdzie sie odpowiedz.

      ewentualnie odpowiem po powrocie, w poniedzialek, ale jak znam zycie poniedzialek po tak dlugim wyjezdzie to bedzie nawalnica rzeczy zaleglych do zrobienia

    2. Awatar marsjaninzmarsa

      Kodeks mi podpowiada, że powinieneś użyć kodu w rodzaju tego:
      function include_my_cpt( $query ) {
      if ( $query->is_home() && $query->is_main_query() ) {
      $query->set( 'post_type’, 'post,books,movies’ );
      }
      }
      add_action( 'pre_get_posts’, 'include_my_cpt’ );

      Nie jestem na 100% pewien, ale powinno zadziałać. :)