top of page

Pobieranie wpisów bez używania query_posts

Zdjęcie autora: Piotr BartczakPiotr Bartczak

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():

  1. resetuje główną pętlę z wpisami

  2. resetuje główną, globalną zmienną post

A tak naprawdę:

  1. tworzy ona nowy, kolejny obiekt WP_Query z parametrami jakie mu przekażesz

  2. 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ż:

  1. spojrzeć na URL strony…

  2. sparsować ten URL w poszukiwaniu wpisów, które pasują do jego wzorca…

  3. pobrać te wpisy z bazy danych (lub pamięci podręcznej)…

  4. 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:

  1. $wp_the_query (bez znaczenia)

  2. $wp_query (istotna)

  3. $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:

  1. Przefiltruj główną pętlę

  2. Kontynuuj tylko jeśli jesteśmy na stronie głównej

  3. Kontynuuj tylko jeśli nasze zapytanie nie jest popsute w jakiś sposób

  4. Kontynuuj tylko jeśli to jest główne zapytanie

  5. Kontynuuj tylko jeśli faktycznie mamy jakieś promowane wpisy

  6. Promowane wpisy? Sprawdźmy czy są wpisy przyklejone

  7. Pobierz wpisy jeśli istnieją

  8. (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)

  9. Pobierz ID każdego wpisu i trzymaj go w tablicy

  10. Zwróć zmodyfikowaną zmienną zawierającą główne zapytanie

  11. 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.

0 wyświetleń0 komentarzy

Ostatnie posty

Zobacz wszystkie

Comments


bottom of page