WordPress - motywy, wtyczki, informacje, programowanie

Hak pre_get_posts

Hakopedia: pre_get_posts – boczne drzwi do WP_Query

WordPress załatwia za nas masę spraw automatycznie. W zależności od szablonu strony, który programujemy, zaczytywane są wpisy właściwego typu, formatu, z odpowiedniego zakresu dat, odpowiednio posortowane. Najczęściej tego właśnie potrzebujemy, czasem jednak chcemy coś zmienić. Nieocenionym hakiem jest w takich wypadkach pre_get_posts.
Artykułem o pre_get_posts rozpoczynamy cykl poradników o nazwie Hakopedia, opisujących poszczególne haki WordPressa, zarówno te powszechnie znane, jak i te nie omówione nawet w dokumentacji na codex.wordpress.org.

Hak pre_get_posts jest wywołany przez WordPressa na samym początku funkcji get_posts(). To z pewnością najważniejsza funkcja całego WordPressa (i chyba najdłuższa!). Przez tę funkcję i TYLKO przez nią są pobierane wpisy z tabeli wp_posts.

Do czego może się przydać pre_get_post?

Pobranie wpisów może się odbyć w dwóch przypadkach:

  1. Automatycznie, kiedy wchodzimy na jedną ze stron serwisu. Na podstawie adresu URL strony WordPress decyduje jakie wpisy pobrać z bazy i pobiera je. Programista ma do nich dostęp już w pliku header.php, czyli zanim do użytkownika zostaną wysłane choćby nagłówki strony. Jest to tak zwane główne zapytanie WordPressa (main query).
  2. Wtedy, kiedy gdzieś w kodzie jednego z szablonów, albo w jakimś widgecie, wykonane jest dodatkowe zapytanie, które ma pobrać wpisy. Najczęściej są to wpisy polecane, wpisy powiązane, podobne tematycznie itp.

Zarówno w pierwszym przypadku jak i w drugim, niezależnie od tego jakiej metody na pobranie wpisów użył programista, ostatecznie wywoływana jest funkcja get_posts(), a na jej początku hak pre_get_posts.

Skorzystanie z niego jest jedynym sposobem modyfikacji głównego zapytania (main query) WordPressa i do tego najczęściej jest używane. Może się też przydać jeśli chcemy zmodyfikować zapytanie wykonywane przez jedną z wtyczek, lub jeden z domyślnych widgetów WordPressa.

Podglądamy main query

Kiedy otwieramy stronę serwisu, WordPress na podstawie jej adresu decyduje jakie wpisy powinien obrać z tabeli wp_posts. Aby prześledzić jakie zmienne znajdują się w adresie, warto ustawić na chwilę domyślny format URLi. Aby to zrobić trzeba przejść w interface admina do sekcji Ustawienia, a następnie do strony Bezpośrednie odnośniki i przełączyć na format domyślny.

Bezpośrednie odnośniki

Chodząc teraz po serwisie widzimy parametry zawarte w adresie strony. Jeśli używamy jednego z pozostałych formatów URL, przyjaznego dla ludzi i wyszukiwarek, prawdziwa postać zapytania jest ukryta. Oczywiście to dobrze, praktycznie nigdy nie powinniśmy używać formatu domyślnego, teraz jednak potrzebujemy go do testów.

Możemy zaobserwować następujące parametry w adresie strony:

  • Dla pojedynczego wpisu parametr p określający numer wpisu do wyświetlenia:
    http://example.com/?p=1
  • Dla strony (page) parametr page_id określający jej numer:
    http://example.com/?page_id=2
  • Dla archiwum po dacie, parametr m oznaczający miesiąc, z którego mają być wyświetlone wpisy:
    http://example.com/?m=201210
  • Dla archiwum kategorii parametr cat określający numer kategorii, z której maja być wyświetlone wpisy:
    http://example.com/?cat=6
  • Dla archiwum dodatkowego, zdefiniowanego przez nas typu wpisu (np. typu film), parametr post_type określający jego skróconą nazwę:
    http://example.com/?post_type=film
  • Dla strony wyników wyszukiwania, parametr s. Na przykład dla słowa „witryna”:
    http://example.com/?s=witryna
  • A dla strony głównej serwisu nie mamy rzecz jasna żadnych parametrów.
    http://example.com/

Są też odpowiednie parametry określające tag, dzień, z którego mają być wyświetlone wpisy, wybrany format wpisu, dodatkową taksonomię, i kilka innych.

Kiedy WordPress zaczyna wyświetlać stronę, parametry z jej adresu URL są umieszczane (bezpośrednio lub przekształcone) w zmiennej typu WP_Query. WordPress uzupełnia jeszcze kilka pól informujących na jakiej stronie jesteśmy (pojedyńczej stronie wpisu, wynikach wyszukiwania, stronie archiwum, stronie głównej, itd.) i wywoływana jest funkcja get_posts(), a na samym jej początku hak pre_get_posts.

Podglądnijmy, co znajduje się w obiekcie WP_Query na wejściu. Aby to zrobić umieścimy w pliku functions.php naszego motywu następujący fragment kodu:

function test_pre_get_post ($query) {
    if (!is_admin() && $query->is_main_query()) {
        $h = fopen('c:\\temp\\dump.txt','w'); // dla windowsów
        fwrite($h, print_r($query,true));
        fclose($h);
    }
}

add_action('pre_get_posts', 'test_pre_get_post');

Naszą funkcję test_pre_get_posts() zarejestrowaliśmy przez add_action() jako akcję dla haka pre_get_posts. Jej zadaniem jest zapisanie do pliku c:\temp\dump.txt (oczywiście na systemie innym niż Windows trzeba podać inną ścieżkę) zwartości obiektu $query otrzymanego na wejściu.

Ponieważ hak pre_get_posts wywoływany jest dla wszystkich zapytań o wpisy, a nas interesuje tylko zapytanie główne, korzystamy z metody is_main_query(). Przy pomocy warunku !is_admin() upewniamy się, że nie jesteśmy w adminie (z jakiegoś powodu zapytania o wpisy w adminie zwracane są także jako main query).

Przejdźmy na naszym przykładowym blogu do widoku jednej z kategorii i zobaczmy co zostało zapisane do pliku dump.txt

WP_Query Object
(
    [query_vars] => Array
        (
            [category_name] => powitania
            [error] =>
            [m] => 0
            [p] => 0
            [post_parent] =>
            [subpost] =>
            [subpost_id] =>
            [attachment] =>
            [attachment_id] => 0
            [name] =>
            [static] =>
            [pagename] =>
            [page_id] => 0
            [second] =>
            [minute] =>
            [hour] =>
            [day] => 0
            [monthnum] => 0
            [year] => 0
            [w] => 0
            [tag] =>
            [cat] =>
            [tag_id] =>
            [author_name] =>
            [feed] =>
            [tb] =>
            [paged] => 0
            [comments_popup] =>
            [meta_key] =>
            [meta_value] =>
            [preview] =>
            [s] =>
            [sentence] =>
            [fields] =>
            [category__in] => Array
                (
                )

            [category__not_in] => Array
                (
                )

            [category__and] => Array
                (
                )

            [post__in] => Array
                (
                )

            [post__not_in] => Array
                (
                )

            [tag__in] => Array
                (
                )

            [tag__not_in] => Array
                (
                )

            [tag__and] => Array
                (
                )

            [tag_slug__in] => Array
                (
                )

            [tag_slug__and] => Array
                (
                )

        )

    [tax_query] => WP_Tax_Query Object
        (
            [queries] => Array
                (
                    [0] => Array
                        (
                            [taxonomy] => category
                            [terms] => Array
                                (
                                    [0] => powitania
                                )

                            [include_children] => 1
                            [field] => slug
                            [operator] => IN
                        )

                )

            [relation] => AND
        )

    [meta_query] =>
    [post_count] => 0
    [current_post] => -1
    [in_the_loop] =>
    [comment_count] => 0
    [current_comment] => -1
    [found_posts] => 0
    [max_num_pages] => 0
    [max_num_comment_pages] => 0
    [is_single] =>
    [is_preview] =>
    [is_page] =>
    [is_archive] => 1
    [is_date] =>
    [is_year] =>
    [is_month] =>
    [is_day] =>
    [is_time] =>
    [is_author] =>
    [is_category] => 1
    [is_tag] =>
    [is_tax] =>
    [is_search] =>
    [is_feed] =>
    [is_comment_feed] =>
    [is_trackback] =>
    [is_home] =>
    [is_404] =>
    [is_comments_popup] =>
    [is_paged] =>
    [is_admin] =>
    [is_attachment] =>
    [is_singular] =>
    [is_robots] =>
    [is_posts_page] =>
    [is_post_type_archive] =>
    [query_vars_hash] => 417ac961acf232b805e6e6eb3a4fe2fc
    [query_vars_changed] =>
    [thumbnails_cached] =>
    [query] => Array
        (
            [category_name] => powitania
        )

)

Struktura jest ogromna, a i tak to, co widzimy nie zawiera wszystkich możliwych pól. Na przykład, przy zapytaniu o wpisy danego typu, znajdziemy jeszcze w tablicy query_vars zmienną (lub tablicę) post_type, a w zapytaniu o konkretny format wpisu znajdziemy zmienną post_format.

Po wyrzuceniu pól nie znaczących dla tego zapytania możemy się przyjrzeć wersji skróconej.

WP_Query Object
(
    [query_vars] => Array
        (
            [category_name] => powitania
            ...
        )
    [tax_query] => WP_Tax_Query Object
        (
            [queries] => Array
                (
                    [0] => Array
                        (
                            [taxonomy] => category
                            [terms] => Array
                                (
                                    [0] => powitania
                                )

                            [include_children] => 1
                            [field] => slug
                            [operator] => IN
                        )

                )

            [relation] => AND
        )
    ...
    [is_archive] => 1
    ...
    [is_category] => 1
	...
    [query] => Array
        (
            [category_name] => powitania
        )

)

Jak widać do zapytania został użyty nie numer, a skrócona nazwa kategorii (tutaj mamy nazwę przykładowej kategorii „powitania”), oraz zostało ono rozwinięte do postaci ogólnej zapytania z uwzględnieniem taksonomii (tax_query). Szczegóły tego jak WordPress przekształca i zadaje zapytania można znaleźć w dokumentacji klasy WP_Query.

W obiekcie mamy też ustawione pola is_archive i is_category, które mówią nam, że to jest strona archiwum oraz, że jest to archiwum kategorii. WordPress zawsze ustawia podobne pola informacyjne, dzięki temu w obsłudze haka pre_get_posts możemy uzależnić naszą ingerencję w obiekt WP_Query od tego, na której jesteśmy stronie i z jakim zapytaniem mamy do czynienia.

Warto zobaczyć jak wygląda zawartość zmiennej $query w zależności od strony, i zapytania. Polecam samodzielne eksperymenty.

Przykładowe zastosowania

Przedstawmy kilka z życia wziętych przykładów użycia pre_get_posts.

Wyświetlanie tylko wybranych kategorii wpisów

Czasem chcemy wykluczyć wyświetlanie wpisów z wybranych kategorii na niektórych, lub na wszystkich stronach serwisu. WordPress nie daje nam takiej możliwości domyślnie, musimy to zaprogramować samodzielnie.

W poniższym przykładzie pominiemy wpisy z kategorii o numerze 8 na wszystkich stronach, z wyjątkiem strony samego wpisu. Artykuły, które przydzielimy do tej kategorii nie będą widoczne w serwisie na żadnej liście (również na stronie głównej, czy wynikach wyszukiwania), ale jeśli ktoś dostanie od nas linka do takiego wpisu, będzie mógł go przeczytać.

function hide_category (WP_Query $query) {
    if (!is_admin() && $query->is_main_query() )
    $query->set('cat','-8');
}
add_action('pre_get_posts', 'hide_category');

możemy to również zrobić tak:

function hide_category (WP_Query $query) {
    if ( !is_admin() && $query->is_main_query() )
        $query->set('category__not_in',8);
}

add_action('pre_get_posts', 'hide_category');

Warto zwrócić uwagę, że nie musimy sprawdzać czy jesteśmy na docelowej stronie wpisu, na przykład tak:

if ( !is_admin() && $query->is_main_query() && !$query->is_singular )
...

ponieważ WordPress ignoruje warunki na taksonomie jeśli strona wyświetla pojedynczy wpis. Odpowiedzialny za to fragment kodu możemy znaleźć w pliku wp-includes/query.php w linii 2205 (WordPress w wersji 3.4.2):

// Taxonomies
if ( !$this->is_singular ) {
    $this->parse_tax_query( $q );

    $clauses = $this->tax_query->get_sql( $wpdb->posts, 'ID' );

    $join .= $clauses['join'];
    $where .= $clauses['where'];
}

Jeżeli wolimy się posłużyć skróconą nazwą kategorii (tutaj nazwą ‚ukryte’), a w dodatku zastosować najbardziej ogólną formę zapytania (nadającą się nie tylko dla kategorii, ale też dla innych taksonomii) możemy to zrobić tak:

function hide_category (WP_Query $query) {
    if (!is_admin() && $query->is_main_query())
        $query->set('tax_query',array(
            array(
               'taxonomy' => 'category',
               'field'    => 'slug',
               'terms'    => 'ukryte',
               'operator' => 'NOT IN',
            )
        ));
}

add_action('pre_get_posts', 'hide_category');

Łatwo się domyślić, że jest to dość wygodne rozwiązanie kiedy chcemy przed ostatecznym upublicznieniem wpisu przesłać go komuś do zaopiniowania, albo po prostu chcemy sami zobaczyć jak wpis wygląda zanim go ostatecznie pokażemy światu. W ten sposób możemy też na stałe ukryć  przed ogółem czytelników niektóre materiały, udostępniając je tylko tym, którym wyślemy linka.

Wyświetlenie wszystkich post_type

Kiedy korzystamy w serwisie z dodatkowych typów wpisów (Custom Post Types), są one wyświetlane przy pomocy oddzielnych szablonów stron, albo oddzielnych widgetów. W zapytaniu głównym na stronie głównej serwisu, w wynikach wyszukiwania i w kanale RSS WordPress umieszcza tylko zwykłe wpisy. Możemy jednak zmienić to zachowanie.

Na przykład w serwisie WPinternals.pl recenzje wtyczek i recenzje motywów są zaimplementowane przy pomocy oddzielnych typów wpisów: plugin_review i theme_review. Jednak te dodatkowe typy są wyświetlane  na stronie głownej, w wynikach wyszukiwania i kanałach RSS razem ze zwykłymi wpisami dzięki następującemu fragmentowi kodu w functions.php motywu:

function query_post_type( WP_Query $query ) {
    if ( !is_admin() && $query->is_main_query() &&
         ($query->is_home() || $query->is_archive() || $query->is_feed() )
    {
        $query->set('post_type',
                    array('post', 'theme_review', 'plugin_review'));
    }
}
add_filter('pre_get_posts', 'query_post_type');

Nietypowe sortowanie

Domyślnie wpisy wszystkich typów WordPress sortuje chronologicznie poczynając od wpisu o najnowszej dacie publikacji, a kończąc na najstarszym W wielu przypadkach dla danego typu wpisu jest to nieprzydatny sposób sortowania.

Jeśli zaimplementujemy w WordPressie na przykład bazę książek, osób czy filmów, najlepszym domyślnym ich porządkiem będzie porządek alfabetyczny. Poniższy przykład pokazuje jak to zrealizować dla wpisów typu ‘osoba’:

function sort_by_title( WP_Query $query ) {
    if ($query->get('post_type')=='osoba' && !is_admin() ) {
        $query->set('order', 'ASC');
        $query->set('orderby', 'title');
    }
}
add_filter('pre_get_posts', 'sort_by_title');

Ważne uwagi

  • Obiekt WP_Query jest przekazywany do funkcji obsługującej haka pre_get_posts przez referencję. Nie musimy więc zwracać żadnego wyniku.
  • Przez ten hak przechodza WSZYSTKIE zapytania o wpisy, również te w adminie, widgetach, itp. Aby uniknąć bardzo dziwnych efektów i precyzyjnie zmieniać tylko wybrane zapytania, Musimy aktywnie korzystać z funkcji is_admin(), oraz metod is_main_query, is_single, is_feed, is_archive, itd.
  • Korzystanie z pre_get_posts jest najbardziej efektywnym sposobem zmiany domyślnych zapytań, do których inaczej nie mamy dostępu. Dawniej korzystało się z funkcji query_posts() ale to już od dawna nie jest zalecana metoda. Przede wszystkim powoduje ona wykonanie drugiego  zapytania do bazy, podczas gdy wyniki tego domyślnego idą do kosza. W mało uczęszczanym serwisie może to mieć niewielkie znaczenie, ale jeśli serwis ma dużo odsłon i stoi na słabym hostingu, każda tego typu rozpusta w gospodarowaniu zasobami sumuje się w nieuchronne kłopoty z wydajnością.
  • Poprzez haka pre_get_posts dostajemy dostęp do obiektu WP_Query zanim jeszcze get_posts() ustawi wszystkie inne, domyślne parametry zapytania: kolejność pobieranych wpisów (domyślnie po dacie od najnowszego do najstarszego), liczbę wpisów (domyślnie taką jaka jest podana w Ustawieniach na stronie Czytanie) itp. Jeśli my ustawimy jeden z tych parametrów WordPress (poza nielicznymi wyjątkami) już tego nie zmieni. Aby zrozumieć te mechanizmy w pełni najlepiej poświęcić trochę czasu na analizę kodu funkcji get_posts().

Powiadomimy Cię o nowych artykułach

Komentarzy: 4

  1. Świetny artykuł, czekam z niecierpliwością na kolejne! Co masz następne w planach?

  2. Czy istnieje mozliwość dodania uprawnień użytkownikom WP do zainstalowanych wtyczek. Chodzi w tym przypadku konkretnie o WP PROPERTY

  3. A jak zmienić całkowicie główne zapytanie wordpressa a konkretnie chodzi mi o optymalizacje zapytania wyświetlającego archiwum kategorii. Mam wordpressa z ok 60000 wpisów podzielonych na ok 100 kategorii i podkategorii i standardowe zapytanie wyświetlające kategorie strasznie zarzyna mi bazę, wykonuje się od kilku do kilkudziesięciu sek. Stworzyłem sobie taką tabelę
    CREATE TABLE `posts_category` (
    `post_id` int(7) NOT NULL,
    `term_tax` int(7) NOT NULL,
    `post_date` datetime NOT NULL,
    PRIMARY KEY (`post_id`),
    KEY `date_taxonomy` (`post_date`,`term_tax`)
    )
    do której zrzucam ID wszystkich postów ze statusem published, term_taxonomy_id kategorii każdego z nich oraz post_date. Takie samo zapytanie na tej tabeli wykonuje się 100-1000 razy szybciej niż standardowe stąd moje pytanie jak zamienić standardowe zapytanie wyświetlające kategorie zapytaniem na tej tabeli???