Debugowanie hooków WordPress: akcje, filtry i kolejność wykonania

Debugowanie hooków WordPress: akcje, filtry i kolejność wykonania

Ostatnio zweryfikowano: 12 października 2026
25 min czytania
Przewodnik
Full-stack developer

WordPress działa na hookach (akcjach i filtrach). Czasem dzieje się coś nieoczekiwanego: treść znika, tytuły się zmieniają, style przestają działać. Podejrzewasz, że miesza jakaś wtyczka, ale która?

Musisz wiedzieć dokładnie, jakie funkcje są podpięte pod konkretny hook (np. the_content albo wp_head).

Najszybciej sprawdzisz to, czytając $wp_filter: wypisujesz callbacki według priorytetu i ustalasz, czy każdy z nich pochodzi z core, z wtyczki czy z aktywnego motywu.

Więcej o tym, jak pracujemy, znajdziesz na stronie o profesjonalnym programowaniu WordPress.

#Czym są akcje i filtry w WordPressie

System hooków WordPressa to podstawa jego rozszerzalności. Każda ważna funkcja WordPressa uruchamia hooki, dzięki czemu wtyczki i motywy zmieniają zachowanie systemu bez edytowania plików core.

Dwa rodzaje hooków:

  1. Akcje uruchamiają kod w określonym momencie i niczego nie zwracają (np. init, wp_head, wp_footer, template_redirect, save_post, wp_enqueue_scripts)
  2. Filtry dostają wartość, zmieniają ją i muszą ją zwrócić (np. the_title, the_content, excerpt_length, upload_mimes)

Oba rodzaje trafiają do tego samego rejestru. add_action() to cienka nakładka, która wywołuje add_filter(), więc jedna procedura inspekcji obsługuje jedno i drugie. Typowy objaw “filtr nie działa” to callback, który zapomina zwrócić wartość: filtr dostaje wtedy null zamiast treści.

Przy debugowaniu musisz zobaczyć:

  • jakie funkcje są podpięte,
  • ich priorytety (kolejność wykonania),
  • która wtyczka lub motyw je dodał,
  • nazwy funkcji callback.

#Jak WordPress przechowuje callbacki hooków w $wp_filter i WP_Hook

Od WordPressa 4.7 globalna zmienna $wp_filter to tablica, której kluczami są nazwy hooków, a każda wartość jest obiektem WP_Hook, a nie zwykłą zagnieżdżoną tablicą. WP_Hook implementuje ArrayAccess i Iterator wyłącznie dla zgodności wstecznej. Dlatego stare snippety, które robią $wp_filter['the_content'][10], nadal coś zwracają zamiast kończyć się błędem krytycznym. Nowy kod powinien czytać bezpośrednio publiczną właściwość callbacks.

Ta właściwość to tablica dwupoziomowa. Pierwszy poziom ma klucze w postaci priorytetu (liczby całkowitej). Drugi poziom ma klucze w postaci unikalnego identyfikatora callbacka, a każdy wpis to tablica z dokładnie dwoma kluczami: function (sam callable) i accepted_args (ile argumentów WordPress mu przekaże). Nic więcej nie jest zapisywane. Nie ma informacji, która wtyczka zarejestrowała callback, nie ma znacznika czasu ani ścieżki pliku. Wszystko, co tabela debugowania pokazuje w kolumnie źródła, jest odtwarzane w chwili inspekcji przez Reflection API. Dlatego właśnie ta kolumna najczęściej bywa błędna albo pusta.

Unikalny identyfikator callbacka tworzy wewnętrzna funkcja _wp_filter_build_unique_id() w pliku wp-includes/plugin.php. Dla zwykłego callbacka w postaci stringa jest to nazwa funkcji. Dla statycznego callable zapisanego jako tablica jest to nazwa klasy połączona z nazwą metody przez ::. Dla metody obiektu albo closure identyfikator wynika z tożsamości tego obiektu: do WordPressa 7.0 przez spl_object_hash(), od 7.1 przez spl_object_id(), przy czym identyfikatory closures dostają prefiks spl_object_id: (dodany w 7.1.1, żeby klucz nigdy nie został rzutowany na liczbę całkowitą). W obu przypadkach identyfikator opisuje jeden obiekt w jednym żądaniu, więc nie da się go wpisać na sztywno. Dlatego nie usuniesz closure, pisząc ją drugi raz, a metodę obiektu usuniesz tylko tą samą instancją, która ją zarejestrowała. Od wersji 6.9 ta sama funkcja zwraca null dla błędnie zbudowanego callbacka tablicowego, a add_filter() niczego wtedy po cichu nie rejestruje.

Nazwa hooka pojawia się w $wp_filter dopiero wtedy, gdy coś zostanie pod niego podpięte. Brak klucza nie oznacza, że hook nigdy się nie uruchamia. Oznacza, że w tej chwili nic go nie nasłuchuje.

#Jak wypisać wszystkie podpięte funkcje w PHP

Dodaj tę funkcję do functions.php albo użyj jej w MU-pluginie na czas pracy deweloperskiej:

/**
 * Inspect WordPress hooks with detailed information
 *
 * @param string $hook_name The hook to inspect (e.g., 'the_content', 'wp_head')
 * @param bool $show_details Show full callback details
 * @return void
 */
function wppoland_inspect_hook( $hook_name, $show_details = false ) {
    global $wp_filter;

    if ( ! isset( $wp_filter[ $hook_name ] ) ) {
        echo '<div style="background:#fff3cd; border:2px solid #ffc107; padding:15px; margin:20px 0;">';
        echo '<strong>Hook ' . esc_html( $hook_name ) . ' has no attached functions.</strong>';
        echo '</div>';
        return;
    }

    // Work on a copy: sorting the live WP_Hook object would change global state.
    $callbacks_by_priority = $wp_filter[ $hook_name ]->callbacks;
    ksort( $callbacks_by_priority, SORT_NUMERIC );

    $plugin_dir    = wp_normalize_path( WP_PLUGIN_DIR );
    $mu_plugin_dir = wp_normalize_path( WPMU_PLUGIN_DIR );
    $theme_root    = wp_normalize_path( get_theme_root() );

    echo '<div style="background:#fff; border:2px solid #dc3545; padding:20px; margin:20px 0; font-family:monospace; font-size:12px; max-width:100%; overflow-x:auto;">';
    echo '<h2 style="margin-top:0; color:#dc3545;">Debugging Hook: <code>' . esc_html( $hook_name ) . '</code></h2>';

    echo '<table style="width:100%; border-collapse:collapse;">';
    echo '<thead><tr style="background:#f8f9fa;"><th style="padding:8px; text-align:left; border:1px solid #dee2e6;">Priority</th><th style="padding:8px; text-align:left; border:1px solid #dee2e6;">Function</th><th style="padding:8px; text-align:left; border:1px solid #dee2e6;">Source</th></tr></thead>';
    echo '<tbody>';

    foreach ( $callbacks_by_priority as $priority => $callbacks ) {
        foreach ( $callbacks as $callback ) {
            $function      = $callback['function'];
            $function_name = 'Unknown';
            $source_file   = '';
            $error         = '';

            try {
                if ( is_string( $function ) ) {
                    $function_name = $function;
                    if ( strpos( $function, '::' ) !== false ) {
                        $reflection = new ReflectionMethod( ...explode( '::', $function, 2 ) );
                    } elseif ( function_exists( $function ) ) {
                        $reflection = new ReflectionFunction( $function );
                    }
                } elseif ( is_array( $function ) ) {
                    $class         = is_object( $function[0] ) ? get_class( $function[0] ) : $function[0];
                    $function_name = $class . '::' . $function[1];
                    $reflection    = new ReflectionMethod( $function[0], $function[1] );
                } elseif ( $function instanceof Closure ) {
                    $function_name = 'Closure';
                    $reflection    = new ReflectionFunction( $function );
                } elseif ( is_object( $function ) ) {
                    $function_name = get_class( $function ) . '::__invoke';
                    $reflection    = new ReflectionMethod( $function, '__invoke' );
                }

                if ( isset( $reflection ) && $reflection->getFileName() ) {
                    $source_file = wp_normalize_path( $reflection->getFileName() ) . ':' . $reflection->getStartLine();
                }
            } catch ( ReflectionException $e ) {
                $error = $e->getMessage();
            }
            unset( $reflection );

            // Detect plugin, must-use plugin or theme from the real directory constants.
            $source_type = 'Other';
            $owner       = $source_file;
            if ( preg_match( '#/wp-(includes|admin)/#', $source_file ) ) {
                $source_type = 'Core';
                $owner       = 'WordPress';
            }
            foreach ( array( 'Plugin' => $plugin_dir, 'MU plugin' => $mu_plugin_dir, 'Theme' => $theme_root ) as $type => $dir ) {
                if ( strpos( $source_file, $dir . '/' ) === 0 ) {
                    $source_type = $type;
                    $relative    = substr( $source_file, strlen( $dir ) + 1 );
                    $owner       = strtok( $relative, '/:' );
                    break;
                }
            }
            if ( '' === $source_file ) {
                $source_type = 'Unknown';
                $owner       = '' !== $error ? $error : 'internal or unresolved';
            }

            echo '<tr style="border-bottom:1px solid #dee2e6;">';
            echo '<td style="padding:8px; border:1px solid #dee2e6;"><strong>' . esc_html( $priority ) . '</strong></td>';
            echo '<td style="padding:8px; border:1px solid #dee2e6;"><code>' . esc_html( $function_name ) . '</code></td>';
            echo '<td style="padding:8px; border:1px solid #dee2e6;">' . esc_html( $source_type . ': ' . $owner ) . '</td>';
            echo '</tr>';

            if ( $show_details ) {
                echo '<tr><td colspan="3" style="padding:4px 8px; font-size:11px; color:#666; border:1px solid #dee2e6;">';
                echo 'File: ' . esc_html( $source_file );
                echo '</td></tr>';
            }
        }
    }

    echo '</tbody></table>';
    echo '</div>';
}

// Usage Examples:
// add_action( 'wp_footer', function(){ wppoland_inspect_hook('the_content'); } );
// add_action( 'wp_footer', function(){ wppoland_inspect_hook('wp_head', true); } ); // With details

#Jak wyświetlić podpięte funkcje w stopce strony

#Podstawowe użycie

Dodaj to tymczasowo do functions.php:

// Inspect the_content hook
add_action( 'wp_footer', function() {
    if ( current_user_can( 'manage_options' ) ) { // Only for admins
        wppoland_inspect_hook( 'the_content' );
    }
} );

Otwórz dowolną stronę i przewiń do stopki. Zobaczysz szczegółową tabelę wszystkich funkcji podpiętych pod the_content.

#Jak sprawdzić kilka hooków naraz

add_action( 'wp_footer', function() {
    if ( ! current_user_can( 'manage_options' ) ) return;

    $hooks_to_check = array( 'the_content', 'wp_head', 'the_title', 'excerpt_length' );

    foreach ( $hooks_to_check as $hook ) {
        wppoland_inspect_hook( $hook );
    }
} );

#Jak pokazać plik i linię każdego callbacka

// Show file paths and line numbers
add_action( 'wp_footer', function() {
    if ( current_user_can( 'manage_options' ) ) {
        wppoland_inspect_hook( 'the_content', true ); // true = show details
    }
} );

#Jak czytać priorytet hooka i źródło callbacka

Wynik to tabela z trzema kolumnami.

#Kolumna priorytetu

WordPress wykonuje callbacki w kolejności priorytetów (niższe liczby najpierw). Domyślny priorytet to 10.

Typowe priorytety:

  • 1-9: wcześniejsze wykonanie (przed domyślnym)
  • 10: priorytet domyślny
  • 11-99: późniejsze wykonanie (po domyślnym)
  • 999: bardzo późno (prawie na końcu)

Przykład: callbacki, które sam core WordPressa podpina pod the_content, tak jak są zarejestrowane w wp-includes/default-filters.php i wp-includes/class-wp-embed.php:

Priority 8:  WP_Embed::run_shortcode, WP_Embed::autoembed, apply_block_hooks_to_content_from_post_object
Priority 9:  do_blocks
Priority 10: wptexturize, wpautop, shortcode_unautop, prepend_attachment, wp_replace_insecure_home_url
Priority 11: capital_P_dangit, do_shortcode (comment in core: "AFTER wpautop()")
Priority 12: wp_filter_content_tags
Priority 20: convert_smilies

Komentarz przy do_shortcode w kodzie core mówi, że ten filtr działa po wpautop() (tłumaczenie własne, default-filters.php). Callbacki o tym samym priorytecie wykonują się w kolejności dodania. Opisuje to dokumentacja WP_Hook::add_filter() i dlatego pięć filtrów core może mieć priorytet 10 bez konfliktu. Callback wtyczki z priorytetem 10 dodany po wpautop dostaje treść, która ma już znaczniki akapitów.

#Kolumna funkcji

Pokazuje nazwę funkcji callback:

  • funkcje w postaci stringa: wpautop, do_shortcode
  • metody klas: MyPlugin::process_content
  • closures: Closure (funkcje anonimowe)
  • obiekty wywoływalne: ClassName::__invoke

#Kolumna źródła

Wskazuje, gdzie callback jest zadeklarowany. WordPress tego nie przechowuje, więc snippet odtwarza to z pliku, który zwraca Reflection:

  • Core: plik w wp-includes albo wp-admin
  • Plugin lub MU plugin: nazwa katalogu w WP_PLUGIN_DIR albo WPMU_PLUGIN_DIR
  • Theme: katalog motywu w get_theme_root()
  • Other: wszystko inne, na przykład drop-in w rodzaju object-cache.php, z pełną ścieżką
  • Unknown: wewnętrzna funkcja PHP albo komunikat błędu Reflection

Plik deklaracji nie zawsze jest plikiem rejestracji. Wtyczka, która podpina pod filtr funkcję core, na przykład wp_strip_all_tags, pojawi się jako Core, bo Reflection podaje miejsce definicji funkcji, a nie to, kto wywołał add_filter().

#Dlaczego $wp_filter nie pokazuje callbacków dla hooka

Najczęstszy fałszywy wynik negatywny przy debugowaniu hooków to zbyt wczesne odczytanie $wp_filter. Tablica rośnie w miarę wykonywania kodu, więc to, co zobaczysz, zależy wyłącznie od momentu odczytu. Callback zarejestrowany w handlerze template_redirect nie istnieje podczas init. Callback dodawany przez wtyczkę, która ładuje się tylko na pojedynczych wpisach, nie pojawi się na archiwum, które akurat testujesz.

Podepnij inspekcję tak późno, jak pozwala problem. wp_footer sprawdza się przy wyjściu szablonu na froncie, bo do tego momentu wszystko, co dotyka the_content, jest już zarejestrowane. Dla kodu działającego poza motywem bezpieczniejszy jest shutdown, bo uruchamia się prawie przy każdym typie żądania, także w panelu admina i w odpowiedziach REST.

WordPress ma pięć funkcji, które odpowiadają na pytanie o moment wykonania bez własnego kodu. did_action( 'init' ) zwraca, ile razy dana akcja dotąd się uruchomiła. Licznik w $wp_actions rośnie w chwili startu do_action(), zanim wykonają się callbacki, więc wewnątrz callbacka init ma już wartość 1. To najszybszy sposób, żeby udowodnić, że hook w ogóle się uruchomił. current_filter() zwraca nazwę aktualnie wykonywanego hooka. doing_action() i doing_filter() odpowiadają na to samo pytanie dla konkretnej nazwy. did_filter() pojawiła się w WordPressie 6.1 i robi dla filtrów to, co did_action() dla akcji. Globalna $wp_current_filter przechowuje cały zagnieżdżony stos. Filtry regularnie uruchamiają się wewnątrz innych filtrów i najgłębszy z nich nie zawsze jest tym, który debugujesz.

Jeśli nazwy hooka naprawdę nie ma w $wp_filter, a did_action() zwraca liczbę większą od zera, hook się uruchamia i nic nie jest pod niego podpięte. To nie jest błąd i ponowne uruchamianie snippetu tego nie zmieni. Problem leży gdzie indziej.

#Jak znaleźć wtyczkę, która zmienia treść albo tytuł

#Który filtr czyści treść wpisu

Problem: treść wpisu jest pusta albo jej brakuje.

Debugowanie:

wppoland_inspect_hook( 'the_content' );

Szukaj:

  • funkcji, które zwracają pusty string,
  • funkcji z wysokim priorytetem, które mogą nadpisywać treść,
  • funkcji wtyczek, które usuwają HTML.

#Który filtr zmienia tytuł strony

Problem: coś modyfikuje tytuły stron.

Debugowanie:

wppoland_inspect_hook( 'the_title' );

Szukaj:

  • wtyczek SEO zmieniających tytuły,
  • wtyczek tłumaczeniowych,
  • własnych filtrów tytułu.

#Dlaczego pliki CSS albo JS przestają się poprawnie ładować

Problem: CSS albo JS nie ładuje się poprawnie.

Debugowanie:

wppoland_inspect_hook( 'wp_head' );
wppoland_inspect_hook( 'wp_enqueue_scripts' );

Szukaj:

  • wtyczek usuwających arkusze stylów,
  • konfliktów priorytetów przy kolejkowaniu,
  • skryptów ładowanych w złej kolejności.

#Jak sprawdzić albo usunąć podpiętą funkcję w PHP

#Jak sprawdzić, czy konkretna funkcja jest podpięta

function wppoland_is_function_hooked( $hook_name, $function_name ) {
    global $wp_filter;

    if ( ! isset( $wp_filter[ $hook_name ] ) ) {
        return false;
    }

    foreach ( $wp_filter[ $hook_name ]->callbacks as $priority => $callbacks ) {
        foreach ( $callbacks as $callback ) {
            if ( is_string( $callback['function'] ) && $callback['function'] === $function_name ) {
                return array( 'priority' => $priority, 'found' => true );
            }
            if ( is_array( $callback['function'] ) && $callback['function'][1] === $function_name ) {
                return array( 'priority' => $priority, 'found' => true );
            }
        }
    }

    return false;
}

// Usage
if ( wppoland_is_function_hooked( 'the_content', 'wpautop' ) ) {
    echo 'wpautop is hooked to the_content';
}

#Jak tymczasowo usunąć hook

// Remove a problematic hook (the priority must match the one it was added with)
remove_filter( 'the_content', 'problematic_function', 10 );

// Or remove every callback whose class or function name contains a prefix
global $wp_filter;
if ( isset( $wp_filter['the_content'] ) ) {
    // foreach works on a copy of the array, so removing inside the loop is safe
    foreach ( $wp_filter['the_content']->callbacks as $priority => $callbacks ) {
        foreach ( $callbacks as $callback ) {
            $function = $callback['function'];
            $name     = '';

            if ( is_string( $function ) ) {
                $name = $function;
            } elseif ( is_array( $function ) ) {
                $name = is_object( $function[0] ) ? get_class( $function[0] ) : (string) $function[0];
            } elseif ( is_object( $function ) && ! ( $function instanceof Closure ) ) {
                $name = get_class( $function );
            }

            if ( '' !== $name && strpos( $name, 'ProblemPlugin' ) !== false ) {
                remove_filter( 'the_content', $function, $priority );
            }
        }
    }
}

#Dlaczego remove_filter po cichu nic nie robi

remove_filter() i remove_action() zwracają wartość logiczną i nigdy nie ostrzegają. Gdy zwracają false, nic się nie stało, a strona wygląda dokładnie tak samo. Błąd jest więc cichy i łatwo go pomylić z problemem cache. Są cztery odrębne przyczyny. Dwie pierwsze widać w tabeli inspekcji, dwóch pozostałych nie.

Zły priorytet. Usuwanie dopasowuje zarówno callback, jak i priorytet. Jeśli wtyczka zarejestrowała callback z priorytetem 20, a ty usuwasz z domyślnym 10, wywołanie się nie uda. Kolumna priorytetu istnieje właśnie po to, żeby temu zapobiec. Czytaj ją, nie zakładaj 10.

Zła tożsamość callbacka. Dla metody obiektu WordPress porównuje unikalny identyfikator zbudowany z tożsamości dokładnie tej instancji (spl_object_hash() do wersji 7.0, spl_object_id() od 7.1). Utworzenie nowej instancji tej samej klasy i przekazanie jej do remove_filter() daje inny identyfikator i niczego nie usuwa. Potrzebujesz oryginalnego obiektu, który zdobędziesz tylko, sięgając do $wp_filter i wyciągając $callback['function'][0] z tablicy callbacków, albo przez publiczny akcesor udostępniony przez wtyczkę.

Usuwanie przed rejestracją. Usuwanie hooków nie jest deklaratywne. Jeśli twój kod działa na plugins_loaded, a wtyczka rejestruje callback na init, usuwasz coś, czego jeszcze nie ma, a chwilę później zostaje to dodane. Przenieś usuwanie na późniejszy hook albo na ten sam hook z wyższym numerem priorytetu.

Callback jest closure. Nie ma nazwy, którą można przekazać, a closure napisana drugi raz to inny obiekt. Standardowe wywołanie z nazwą nie zadziała, potrzebny jest sposób z następnej sekcji.

Sprawdzaj też poprawnie wartość zwracaną. has_filter() zwraca false, gdy callback nie jest podpięty, i liczbę całkowitą z priorytetem, gdy jest. Priorytet 0 jest poprawną wartością, a w PHP jest fałszywy, więc porównuj przez !== false, a nie zwykłym testem prawdziwości. Od WordPressa 6.9 has_filter() i has_action() przyjmują trzeci argument $priority. Gdy podasz callback i priorytet, wynik jest zwykłą wartością logiczną dla tego dokładnego priorytetu, co daje najczystsze sprawdzenie przed wywołaniem remove_filter().

#Jak usunąć funkcję anonimową z hooka

Unikalny identyfikator powstaje na nowo w każdym żądaniu, więc jedyny pewny sposób odpięcia funkcji anonimowej to znaleźć obiekt closure w tablicy callbacków w tym samym żądaniu i przekazać ten sam obiekt do remove_filter(). WordPress buduje identyfikator z tożsamości obiektu, więc wyciągnięty obiekt daje dokładnie ten klucz, pod którym został zapisany:

function wppoland_remove_closure( $hook_name, $priority, $needle ) {
    global $wp_filter;

    if ( ! isset( $wp_filter[ $hook_name ]->callbacks[ $priority ] ) ) {
        return false;
    }

    foreach ( $wp_filter[ $hook_name ]->callbacks[ $priority ] as $id => $callback ) {
        if ( ! ( $callback['function'] instanceof Closure ) ) {
            continue;
        }

        $reflection = new ReflectionFunction( $callback['function'] );

        if ( strpos( (string) $reflection->getFileName(), $needle ) !== false ) {
            return remove_filter( $hook_name, $callback['function'], $priority );
        }
    }

    return false;
}

Dopasowuj po pliku deklaracji zwróconym przez Reflection, a nie po zgadywanej nazwie. Użycie remove_filter() zamiast unset() na tablicy callbacków ma znaczenie. WP_Hook trzyma osobną listę priorytetów (chroniona właściwość $priorities, dodana w WordPressie 6.4) i koryguje każdą trwającą iterację. WP_Hook::remove_filter() aktualizuje oba elementy, gdy kubełek priorytetu staje się pusty. Surowy unset() nie aktualizuje żadnego z nich, więc opróżniony kubełek zostaje jako pusta tablica, a usunięcie w trakcie działania hooka może rozjechać stan iteracji. Wywołaj funkcję na tyle późno, żeby closure była już zarejestrowana, i na tyle wcześnie, żeby hook jeszcze się nie uruchomił.

#Błędy snippetu do debugowania hooków na PHP 8

Kod debugujący, który sam się wywraca, jest gorszy niż brak takiego kodu. Wcześniejsze wersje tego snippetu, w tym ta, którą publikował ten artykuł, psują się na PHP 8 w czterech miejscach. Kod powyżej omija każde z nich.

strpos() na callbacku. Pętla wywołująca strpos( $callback['function'], 'ProblemPlugin' ) działa tylko, dopóki function jest stringiem. Gdy trafi na callback tablicowy albo closure, PHP 8 rzuca TypeError, bo funkcje wewnętrzne nie przyjmują już tablicy ani obiektu tam, gdzie oczekują stringa. Najpierw ustal nazwę przez is_string(), is_array() i is_object(), potem porównuj, tak jak robi to przykład masowego usuwania.

ksort() na żywym obiekcie. Sortowanie $wp_filter[ $hook ]->callbacks w miejscu zmienia stan globalny podczas żądania, które próbujesz obserwować. WP_Hook::add_filter() i tak utrzymuje kubełki posortowane, wywołując ksort( $this->callbacks, SORT_NUMERIC ) za każdym razem, gdy pojawia się nowy priorytet, więc dodatkowe sortowanie niczego nie poprawia. Funkcja inspekcji sortuje lokalną kopię.

Wyjątki Reflection. new ReflectionFunction() przyjmuje zwykłą nazwę funkcji albo Closure i rzuca ReflectionException dla stringa w formie Class::method. ReflectionMethod rzuca wyjątek, gdy metoda jest obsługiwana przez __call(), a nie zadeklarowana, co jest częste we wtyczkach używających magicznych metod jako pośredników hooków. Na PHP 8.4 przekazanie jednego stringa Class::method do new ReflectionMethod() dodatkowo wywołuje komunikat o przestarzałości z zaleceniem użycia ReflectionMethod::createFromMethodName(). Rozdzielenie stringa i przekazanie dwóch argumentów działa na każdej wersji PHP 8. Funkcja obejmuje to wszystko jednym try/catch i wypisuje komunikat wyjątku, więc brakującą metodę da się odróżnić od magicznej.

Ścieżki wpisane na sztywno. Wykrywanie źródła po fragmentach wp-content/plugins i wp-content/themes błędnie oznacza każdy wiersz na stronach, które przeniosły katalog treści przez WP_CONTENT_DIR, i nigdy nie rozpoznaje wtyczek must-use. Funkcja porównuje ścieżki z WP_PLUGIN_DIR, WPMU_PLUGIN_DIR i get_theme_root() po wp_normalize_path(), co pozwala też porównywać ścieżki z Windows. Zostaje jeden przypadek: Reflection podaje rozwiązaną ścieżkę wtyczki podlinkowanej symlinkiem, więc taki callback trafia do wiersza Other z prawdziwą ścieżką.

#Jak wyświetlić hooki we wtyczce Query Monitor

Do listy w interfejsie zwykle służy darmowa wtyczka Query Monitor autorstwa Johna Blackbourna, dostępna na WordPress.org. Jej readme opisuje ją jako “panel narzędzi deweloperskich dla WordPressa i WooCommerce” (tłumaczenie własne, readme Query Monitor) i wymienia hooki oraz akcje wśród rzeczy, które debuguje. Ten sam panel pokazuje też zapytania do bazy, błędy PHP i żądania HTTP.

#Dlaczego Query Monitor do debugowania hooków

  1. Interfejs wizualny: jedna tabela na żądanie, z filtrowaniem
  2. Ścieżki plików: każdy callback prowadzi do swojego pliku i linii
  3. Nazwy komponentów: każdy wiersz podaje odpowiedzialną wtyczkę, motyw albo komponent core
  4. Bez kodu: niczego nie wklejasz do functions.php

Panel hooków nie mierzy czasu wykonania hooka ani callbacka. Jego kolumny to nazwa hooka, typ (akcja albo filtr, gdy pokazane są wszystkie hooki), priorytet, callback i komponent. Do pomiaru czasu Query Monitor ma osobne panele, a koszt pojedynczego callbacka lepiej zmierzyć profilerem.

#Instalacja

# Via WP-CLI
wp plugin install query-monitor --activate

# Or download it from WordPress.org

Bez WP-CLI: w panelu wybierz Wtyczki, Dodaj nową, wyszukaj Query Monitor, zainstaluj i włącz.

#Jak znaleźć hook w Query Monitor

  1. Zainstaluj i włącz Query Monitor
  2. Otwórz dowolną stronę serwisu jako zalogowany administrator
  3. Otwórz menu Query Monitor na pasku narzędzi admina, które pokazuje przegląd bieżącej strony
  4. Wybierz panel Hooks & Actions
  5. Filtruj po nazwie hooka albo po komponencie
  6. Odczytaj podpięte callbacki z ich priorytetem i komponentem

Jeden szczegół decyduje, czy to zadziała dla the_content. Domyślnie kolektor hooków wypisuje tylko nazwy z $wp_actions, czyli akcje, które w danym żądaniu uruchomiły się co najmniej raz. Filtrów takich jak the_content, the_title czy excerpt_length na tej liście nie ma. Żeby je dołączyć, dodaj to do wp-config.php na środowisku deweloperskim:

define( 'QM_SHOW_ALL_HOOKS', true );

Po ustawieniu stałej kolektor czyta wszystkie klucze $wp_filter, panel zmienia nazwę na Hooks, Actions, & Filters, a kolumna typu odróżnia akcje od filtrów. Druga stała, QM_HIDE_CORE_ACTIONS, ukrywa callbacki należące do core, co skraca tabelę, gdy interesują cię tylko wtyczki. Query Monitor oznacza też specjalny hook all: jeśli coś jest pod niego podpięte, panel wyświetla ostrzeżenie, że akcja all “jest skrajnie zasobożerna” i lepiej jej unikać (tłumaczenie własne, Query Monitor, output/html/hooks.php).

Domyślnie Query Monitor pokazuje swoje dane tylko administratorom na pojedynczej instalacji i superadministratorom w multisite, więc można go bezpiecznie zostawić włączonego na kopii stagingowej.

#Kiedy Query Monitor, a kiedy własny snippet

Query Monitor, gdy:

  • potrzebujesz stałego narzędzia do debugowania,
  • chcesz mieć w tym samym żądaniu także zapytania do bazy, wywołania HTTP i błędy PHP,
  • debugujesz wiele hooków,
  • wolisz interfejs od kodu.

Własny snippet, gdy:

  • potrzebujesz szybkiego, jednorazowego sprawdzenia,
  • pracujesz w środowisku deweloperskim,
  • chcesz sprawdzać hooki programowo,
  • Query Monitor nie jest dostępny.

#Jak wypisać hooki WordPressa przez WP-CLI

Gdy strona się nie renderuje, pasek admina jest niedostępny albo pracujesz na stagingu przez SSH, WP-CLI czyta te same zmienne globalne bez przeglądarki:

# List every callback attached to the_content, priority first
wp eval 'global $wp_filter; foreach ( $wp_filter["the_content"]->callbacks as $p => $cbs ) { foreach ( $cbs as $id => $cb ) { echo $p . "\t" . $id . "\n"; } }'

# Same thing interactively
wp shell

# Longer scripts belong in a file
wp eval-file inspect-hooks.php

Trzy zachowania potrafią zaskoczyć. Po pierwsze, WP-CLI uruchamia WordPressa, ale nigdy nie przechodzi przez warstwę szablonów, więc template_redirect, wp_head i wp_footer się nie uruchamiają, a wszystko, co jest w nich rejestrowane, nie pojawi się w zrzucie. Callbacki dodane na plugins_loaded, after_setup_theme i init są obecne, a to obejmuje większość rejestracji wtyczek. Po drugie, WP-CLI działa bez zalogowanego użytkownika, jeśli nie podasz --user, więc rejestracje zależne od uprawnień zachowują się inaczej niż w przeglądarce. Po trzecie, wp eval wykonuje kod wewnątrz metody, dlatego dokumentacja polecenia ostrzega, że zmienne globalne, takie jak $wp_filter, trzeba najpierw zadeklarować przez global, co robi powyższy jednolinijkowiec. Dokumentacja wymienia też opcję --hook=<hook>, która uruchamia kod po wykonaniu wskazanego hooka, ale zaznacza, że nie jest ona jeszcze częścią stabilnego wydania WP-CLI i wymaga wersji nightly (wp cli update --nightly).

Te same narzędzia dają szybki bisect, gdy tabela inspekcji wskazuje wtyczkę, a ty chcesz dowodu:

wp plugin list --status=active --field=name
wp eval 'echo has_filter( "the_content", "wpautop" ) === false ? "absent" : "priority " . has_filter( "the_content", "wpautop" );'
wp eval 'echo has_filter( "the_content", "wpautop" ) === false ? "absent" : "priority " . has_filter( "the_content", "wpautop" );' --skip-plugins=suspect-plugin

--skip-plugins i --skip-themes przyjmują listę rozdzieloną przecinkami i wyłączają ładowanie tylko dla tego jednego polecenia, więc dla odwiedzających nic nie zostaje dezaktywowane. Dwa uruchomienia tego samego sprawdzenia, raz normalnie i raz z pominiętą podejrzaną wtyczką, dają rozstrzygającą odpowiedź, w przeciwieństwie do odczytu odtworzonej ścieżki pliku.

#Jak debugować hooki w żądaniach REST, AJAX i cron

Tabela wypisana w stopce działa tylko w żądaniu, które renderuje szablon motywu. Sporo realnych błędów hooków dzieje się w żądaniach, które tego nie robią: odpowiedzi REST API pod /wp-json/, wywołania admin-ajax.php, zdarzenia WP-Cron, renderowanie bloków przy zapisie i uruchomienia z CLI. Wypisanie HTML w którymkolwiek z nich albo trafia donikąd, albo psuje odpowiedź JSON, a to wygląda potem jak drugi, niezwiązany błąd.

W tych kontekstach pisz do logu, a nie na stronę. Ustaw WP_DEBUG i WP_DEBUG_LOG na true w wp-config.php, zostaw WP_DEBUG_DISPLAY na false, żeby nic nie wyciekło do wyjścia, i wysyłaj zrzut przez error_log(). Wynik trafia do wp-content/debug.log, który możesz śledzić na żywo przez tail -f wp-content/debug.log, powtarzając błędne żądanie. W WP-CLI WP_CLI::log() pisze zamiast tego do terminala.

Akcja shutdown to właściwy punkt zaczepienia dla takiej inspekcji. Uruchamia się na końcu prawie każdego typu żądania, po wszystkich warunkowych rejestracjach, i nie ma dla niej znaczenia, czy brał udział szablon. Połącz ją z wp_doing_ajax(), wp_doing_cron() i defined( 'REST_REQUEST' ), żeby każda linia logu zapisywała, jaki rodzaj żądania ją wytworzył. Bez tej etykiety log pełen zrzutów the_content z różnych typów żądań jest nieczytelny.

#Dobre praktyki debugowania i nazywania hooków

#1. Debuguj tylko na środowisku deweloperskim

Nigdy nie zostawiaj kodu debugującego na produkcji:

if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
    add_action( 'wp_footer', function() {
        wppoland_inspect_hook( 'the_content' );
    } );
}

#2. Ogranicz do administratorów

Zawsze sprawdzaj uprawnienia użytkownika:

if ( current_user_can( 'manage_options' ) ) {
    // Show debug info
}

#3. Używaj opisowych nazw hooków

Tworząc własne hooki, używaj prefiksów:

// Good
do_action( 'wppoland_before_content' );

// Bad
do_action( 'before_content' );

#4. Dokumentuj swoje hooki

/**
 * Fires before the main content area
 *
 * @param string $content The post content
 */
do_action( 'wppoland_before_content', $content );

#Hook się nie uruchamia albo funkcja się nie wykonuje

#Dlaczego mój hook się nie pojawia

Możliwe przyczyny:

  1. Hook jeszcze się nie uruchomił (sprawdź moment wykonania)
  2. Logika warunkowa blokuje uruchomienie hooka
  3. Nazwa hooka ma literówkę

Rozwiązanie:

// has_action() without a callback answers "is anything attached", not "does the hook exist"
if ( has_action( 'your_hook_name' ) ) {
    echo 'At least one callback is attached';
} else {
    echo 'Nothing is attached (the hook may still fire)';
}

// did_action() answers "has it fired on this request"
echo 'Fired ' . did_action( 'your_hook_name' ) . ' time(s)';

#Dlaczego podpięta funkcja się nie wykonuje

Możliwe przyczyny:

  1. Konflikt priorytetów
  2. Logika warunkowa
  3. Hook usunięty przez inną wtyczkę

Rozwiązanie:

// Check current priority (false means not attached; 0 is a valid priority)
$priority = has_filter( 'the_content', 'your_function' );
echo false === $priority ? 'Not attached' : 'Priority: ' . $priority;

#Jak zalogować kolejność uruchamiania hooków WordPressa

Lista podpiętych funkcji odpowiada na jedno pytanie. Nie odpowiada na drugie: w jakiej kolejności rzeczy naprawdę wykonały się w tym żądaniu. WordPress rezerwuje nazwę hooka all. Każde wywołanie do_action() i apply_filters() sprawdza ją przed uruchomieniem callbacków danego hooka i przekazuje sterowanie przez _wp_call_all_hook(), więc callback podpięty pod all widzi każdą uruchamianą akcję i każdy filtr:

add_action( 'all', function() {
    if ( ! defined( 'WP_DEBUG' ) || ! WP_DEBUG ) {
        return;
    }

    $hook = current_filter();

    if ( strpos( $hook, 'the_' ) !== 0 && strpos( $hook, 'wp_' ) !== 0 ) {
        return;
    }

    error_log( sprintf( '[hook] %s (depth %d)', $hook, count( $GLOBALS['wp_current_filter'] ) ) );
} );

Dwie zasady czynią to użytecznym. Zarejestruj callback jak najwcześniej, najlepiej z MU-pluginu, bo wszystko, co uruchomi się przed podpięciem, jest niewidoczne. I zawsze filtruj po prefiksie przed zapisem do logu, bo jedno żądanie frontu uruchamia setki hooków, a niefiltrowany ślad daje log, którego nie przeczytasz. Głębokość z $wp_current_filter mówi, że filtr wykonał się wewnątrz innego, co zwykle wyjaśnia callback, który pozornie działa dwa razy.

Koszt jest realny: all dokłada wywołanie funkcji do każdego uruchomienia hooka w żądaniu. Użyj go, żeby ustalić kolejność, potem usuń. Nie ma miejsca w stałym MU-pluginie do debugowania.

#Jak sprawdzić, czy usunięcie hooka zadziałało

Usunięcie callbacka i poprawny wygląd strony to jeszcze nie dowód. Za każdym razem przejdź te same kroki w tej kolejności.

Zanim cokolwiek zmienisz, zapisz stan bieżący, żeby mieć punkt odniesienia. Zanotuj dokładny priorytet z tabeli inspekcji, identyfikator callbacka i ustalony plik źródłowy. Zapisz wyrenderowany wynik jednego problematycznego adresu jako wylogowany użytkownik, żeby później porównać go diffem. Potwierdź, że objaw odtwarza się w czystym żądaniu, a nie tylko w twojej sesji przeglądarki.

Po zmianie uruchom inspekcję ponownie na tym samym hooku i potwierdź, że wiersz zniknął, a nie tylko, że strona wygląda inaczej. has_filter( 'the_content', 'callback_name' ) === false to maszynowa wersja tego samego sprawdzenia i lepiej trzymać ją w jednolinijkowcu WP-CLI niż w ocenie na oko. Potem potwierdź, że usunięcie nie zabrało niczego innego: liczba callbacków dla tego priorytetu powinna spaść dokładnie o jeden.

Następnie wyklucz cache, który daje zarówno fałszywe porażki, jak i fałszywe sukcesy. Wyczyść object cache przez wp cache flush, wyczyść cache stron albo warstwę CDN przed serwisem i przeładuj stronę jako wylogowany odwiedzający, a nie jako administrator, który omija większość cache. Jeśli hosting działa z OPcache przy wyłączonym opcache.validate_timestamps, zmiany w MU-pluginie nie działają do czasu zresetowania cache, więc zmiana, która pozornie nic nie daje, może po prostu jeszcze się nie wykonywać.

Na koniec sprawdź zasięg skutków. Usunięcie, które naprawia jeden szablon, może zepsuć inny, który zależał od tego samego callbacka. Przetestuj co najmniej jeden pojedynczy wpis, jedno archiwum, jeden ekran admina i jeden endpoint REST. Jeśli usunąłeś filtr, którego obecność zakładał inny kod, błąd zwykle wychodzi jako nieescapowane albo niesformatowane wyjście w zupełnie innym miejscu, a nie jako błąd PHP.

Gdy poprawka jest potwierdzona, zastąp tymczasowe usunięcie czymś trwałym i opisanym: nazwaną funkcją w MU-pluginie z komentarzem zawierającym hook, priorytet, wtyczkę, której dotyczy, i datę. Anonimowe usunięcia rozsiane po functions.php to powód, dla którego następna osoba musi powtarzać całe śledztwo.

#Podsumowanie

Własny snippet pokazuje dla dowolnego hooka:

  • jakie funkcje są podpięte,
  • ich priorytety wykonania,
  • która wtyczka, motyw albo plik core je deklaruje,
  • lokalizacje plików źródłowych.

Najważniejsze wnioski:

  • czytaj $wp_filter[ $hook ]->callbacks, publiczną właściwość WP_Hook, i sortuj kopię, nigdy żywy obiekt,
  • Query Monitor pokazuje filtry takie jak the_content tylko przy zdefiniowanej stałej QM_SHOW_ALL_HOOKS,
  • usuwaj callbacki przez remove_filter() z dokładnym priorytetem, a dla metod i closures używaj oryginalnego obiektu,
  • pytania o moment i kolejność wykonania rozwiązują did_action(), current_filter() i hook all,
  • ograniczaj wyjście debugowania do środowiska deweloperskiego i do administratorów.

Jeśli po debugowaniu zostaje problem z szybkością strony, zobacz nasze usługi optymalizacji szybkości WordPress.

Następny krok

Przekuj artykuł w realne wdrożenie

Pod tym wpisem dokładam linki, które domykają intencję użytkownika i prowadzą dalej w strukturze serwisu.

Chcesz wdrożyć ten temat na swojej stronie?

Jeśli problemem są Core Web Vitals, wolny frontend albo ciężki WordPress, rozpiszę i wdrożę konkretny plan optymalizacji.

Powiązany klaster

Sprawdź inne usługi WordPress i bazę wiedzy

Wzmocnij swój biznes dzięki profesjonalnemu wsparciu technicznemu w kluczowych obszarach ekosystemu WordPress.

FAQ do artykułu

Często zadawane pytania

Najważniejsze odpowiedzi, które pomagają wdrożyć temat w praktyce.

SEO-readyGEO-readyAEO-ready3 Q&A
Jak debugować kolejność hooków w WordPressie?#
Włącz WP_DEBUG_LOG, wypisz current_filter() i listę callbacków z $wp_filter dla podejrzanego tagu. Query Monitor albo spójny error_log z priorytetem pokazuje, kto nadpisuje wynik i w jakiej kolejności.
Czym różni się akcja od filtra przy debugowaniu?#
Akcja odpala skutki uboczne; filtr musi zwrócić wartość. Objaw "filtr nie działa" to często return null albo zły priorytet, nie brak add_filter.
Jak złapać konflikt wtyczek na tym samym hooku?#
Wyłącz połowę wtyczek na stagingu, powtórz repro, potem bisect. Zapisuj priorytety i liczbę argumentów accepted_args - rozjazdy tu są częstsze niż "zły kod" w jednej wtyczce.

Potrzebujesz FAQ dopasowanego do branży i rynku? Przygotujemy wersję pod Twoje cele biznesowe.

Porozmawiajmy

Polecane artykuły