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:
- Akcje uruchamiają kod w określonym momencie i niczego nie zwracają (np.
init,wp_head,wp_footer,template_redirect,save_post,wp_enqueue_scripts) - 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 detailsJak 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ślny11-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_smiliesKomentarz 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-includesalbowp-admin - Plugin lub MU plugin: nazwa katalogu w
WP_PLUGIN_DIRalboWPMU_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
- Interfejs wizualny: jedna tabela na żądanie, z filtrowaniem
- Ścieżki plików: każdy callback prowadzi do swojego pliku i linii
- Nazwy komponentów: każdy wiersz podaje odpowiedzialną wtyczkę, motyw albo komponent core
- 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.orgBez WP-CLI: w panelu wybierz Wtyczki, Dodaj nową, wyszukaj Query Monitor, zainstaluj i włącz.
Jak znaleźć hook w Query Monitor
- Zainstaluj i włącz Query Monitor
- Otwórz dowolną stronę serwisu jako zalogowany administrator
- Otwórz menu Query Monitor na pasku narzędzi admina, które pokazuje przegląd bieżącej strony
- Wybierz panel Hooks & Actions
- Filtruj po nazwie hooka albo po komponencie
- 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.phpTrzy 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:
- Hook jeszcze się nie uruchomił (sprawdź moment wykonania)
- Logika warunkowa blokuje uruchomienie hooka
- 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:
- Konflikt priorytetów
- Logika warunkowa
- 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_contenttylko przy zdefiniowanej stałejQM_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 hookall, - 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.






