Hvordan feilsøke WordPress hooks: list alle actions og filters

Hvordan feilsøke WordPress hooks: list alle actions og filters

Sist verifisert: 12. oktober 2026
28 min lesetid
Guide
Full-stack-utvikler

WordPress bygger på hooks (actions og filters). Noen ganger skjer uventede ting: innhold forsvinner, titler endres, stiler slutter å virke. Du mistenker at en plugin blander seg inn, men hvilken?

Du må vite nøyaktig hvilke funksjoner som er knyttet til en bestemt hook (f.eks. the_content eller wp_head).

Den raskeste veien er å inspisere $wp_filter, liste callbacks etter prioritet og spore om hver callback kommer fra kjernen, en plugin eller det aktive temaet.

Les mer om profesjonell WordPress-utvikling hos WPPoland.

#Hva er actions og filters i WordPress

Hook-systemet i WordPress er det som gjør plattformen utvidbar. Alle sentrale funksjoner i WordPress fyrer av hooks, slik at plugins og temaer kan endre oppførsel uten å redigere kjernefiler.

To typer hooks:

  1. Actions kjører kode på et bestemt punkt og returnerer ingenting (f.eks. init, wp_head, wp_footer)
  2. Filters mottar en verdi, endrer den og må returnere den (f.eks. the_title, the_content)

Begge ligger i det samme registeret. add_action() er en tynn innpakning som kaller add_filter(), så én inspeksjonsrutine dekker begge.

Ved feilsøking må du se:

  • Hvilke funksjoner som er hooket
  • Prioriteten deres (utførelsesrekkefølgen)
  • Hvilken plugin eller hvilket tema som la dem til
  • Navnene på callback-funksjonene

#Hvordan WordPress lagrer hook-callbacks i $wp_filter og WP_Hook

Siden WordPress 4.7 er den globale $wp_filter en array med hook-navnet som nøkkel, og hver verdi er et WP_Hook-objekt, ikke en vanlig nestet array. WP_Hook implementerer ArrayAccess og Iterator kun for bakoverkompatibilitet. Derfor returnerer ti år gamle snutter som gjør $wp_filter['the_content'][10] fortsatt noe i stedet for en fatal feil. Ny kode bør lese den offentlige egenskapen callbacks direkte.

Den egenskapen er en array i to nivåer. Første nivå har prioriteten som nøkkel, som heltall. Andre nivå har en unik callback-ID som nøkkel, og hver oppføring er en array med nøyaktig to nøkler: function (selve callbacken) og accepted_args (hvor mange argumenter WordPress sender til den). Ingenting annet lagres. Det finnes ingen oversikt over hvilken plugin som registrerte callbacken, ingen tidsstempel og ingen filsti. Alt som feilsøkingstabellen nedenfor viser i kolonnen Kilde, rekonstrueres ved inspeksjon med Reflection API. Derfor er det den kolonnen som oftest er feil eller tom.

Den unike callback-ID-en lages av den interne funksjonen _wp_filter_build_unique_id() i wp-includes/plugin.php. For en vanlig strengcallback er den funksjonsnavnet. For en statisk callback skrevet som array er den klassenavnet og metodenavnet satt sammen med ::. For en objektmetode eller en closure utledes den fra identiteten til objektet: spl_object_hash() til og med WordPress 7.0, og spl_object_id() fra 7.1, der ID-en for en closure også får prefikset spl_object_id: (lagt til i 7.1.1 slik at nøkkelen aldri gjøres om til et heltall). Uansett beskriver ID-en ett objekt i én forespørsel, så den kan ikke hardkodes. Derfor kan du ikke fjerne en closure ved å skrive den på nytt, og en objektmetode kan bare fjernes med den samme instansen som registrerte den. Fra 6.9 returnerer den samme funksjonen null for en ugyldig array-callback, og add_filter() registrerer da ingenting, uten varsel.

Et hook-navn dukker først opp i $wp_filter når noe er knyttet til det. En manglende nøkkel betyr ikke at hooken aldri fyrer. Den betyr at ingenting lytter akkurat nå.

#PHP-snutt som lister alle hooked funksjoner i WordPress

Legg denne funksjonen i functions.php, eller bruk den i en Must-Use-plugin under utvikling:

/**
 * 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

#Hvordan vise hooked funksjoner i bunnteksten

#Grunnleggende bruk

Legg dette midlertidig i 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' );
    }
} );

Åpne en hvilken som helst side og rull ned til bunnteksten. Der ser du en tabell med alle funksjoner som er hooket på the_content.

#Hvordan inspisere flere hooks samtidig

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 );
    }
} );

#Hvordan vise fil og linje for hver callback

// 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
    }
} );

#Hvordan lese hook-prioritet og callback-kilde

Utdataene er en tabell med tre kolonner:

#Kolonnen Priority

WordPress kjører callbacks i prioritetsrekkefølge (lavere tall først). Standardprioriteten er 10.

Vanlige prioriteter:

  • 1-9: tidlig kjøring (før standard)
  • 10: standardprioritet
  • 11-99: sen kjøring (etter standard)
  • 999: svært sent (nesten sist)

Eksempel: callbackene som WordPress-kjernen selv knytter til the_content, slik de registreres i wp-includes/default-filters.php og 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

Callbacks med samme prioritet kjører i den rekkefølgen de ble lagt til. Det står i dokumentasjonen til WP_Hook::add_filter(), og det er grunnen til at fem kjernefiltre kan ligge på 10 uten konflikt. En plugin-callback på 10 som ble lagt til etter wpautop, får innhold som allerede har avsnittstagger. Kommentaren i kjernen ved do_shortcode betyr «ETTER wpautop()» (egen oversettelse).

#Kolonnen Function

Viser navnet på callbacken:

  • Strengfunksjoner: wpautop, do_shortcode
  • Klassemetoder: MyPlugin::process_content
  • Closures: Closure (anonyme funksjoner)
  • Kallbare objekter: ClassName::__invoke

#Kolonnen Source

Viser hvor callbacken er deklarert. WordPress lagrer ikke dette, så snutten rekonstruerer det fra filen Reflection rapporterer:

  • Core: en fil under wp-includes eller wp-admin
  • Plugin eller MU plugin: mappenavnet under WP_PLUGIN_DIR eller WPMU_PLUGIN_DIR
  • Theme: temamappen under get_theme_root()
  • Other: alt annet, for eksempel en drop-in som object-cache.php, med full sti
  • Unknown: en intern PHP-funksjon, eller feilmeldingen fra Reflection

Filen som deklarerer en funksjon, er ikke alltid filen som registrerte den. En plugin som knytter en kjernefunksjon som wp_strip_all_tags til et filter, vises som Core, fordi Reflection rapporterer hvor funksjonen er definert, ikke hvem som kalte add_filter().

#Hvorfor $wp_filter ikke viser callbacks for en hook

Den vanligste falske negativen i hook-feilsøking er å lese $wp_filter for tidlig. Arrayen bygges opp mens koden kjører, så det du ser, avhenger helt av når du ser. En callback som registreres inne i en template_redirect-handler, finnes ikke under init. En callback fra en plugin som bare lastes på enkeltvisninger, mangler på arkivsiden du tilfeldigvis testet.

Registrer inspeksjonen så sent som problemet tillater. wp_footer fungerer for front-end-maler fordi alt som berører the_content, er registrert innen da. For alt som kjører utenfor temaet, er shutdown tryggere, fordi den fyrer på nesten alle typer forespørsler, også adminsider og REST-svar.

WordPress har fem funksjoner som svarer på tidsspørsmålet uten egen kode. did_action( 'init' ) returnerer hvor mange ganger den actionen har fyrt så langt. Telleren i $wp_actions økes når do_action() starter, før callbackene kjører, så den er allerede 1 inne i en init-callback. Det er den raskeste måten å bevise at en hook i det hele tatt har fyrt. current_filter() returnerer navnet på hooken som kjører akkurat nå. doing_action() og doing_filter() svarer på det samme for et bestemt navn. did_filter() kom i WordPress 6.1 og gjør for filters det did_action() gjør for actions. Den globale $wp_current_filter holder hele den nestede stakken. Det betyr noe fordi filters ofte fyrer inne i andre filters, og den innerste er ikke alltid den du feilsøker.

Mangler hook-navnet virkelig i $wp_filter, mens did_action() likevel returnerer et tall over null, fyrer hooken uten at noe er knyttet til den. Det er ingen feil, og det hjelper ikke å kjøre snutten på nytt. Problemet ligger et annet sted.

#Hvilken plugin endrer innholdet eller tittelen

#Hvilket filter tømmer innleggsinnholdet

Problem: Innleggsinnholdet er tomt eller mangler.

Feilsøk:

wppoland_inspect_hook( 'the_content' );

Se etter:

  • Funksjoner som returnerer tomme strenger
  • Funksjoner med høy prioritet som kan overstyre innholdet
  • Plugin-funksjoner som fjerner HTML

#Hvilket filter endrer sidetittelen

Problem: Sidetitler blir endret av noe.

Feilsøk:

wppoland_inspect_hook( 'the_title' );

Se etter:

  • SEO-plugins som endrer titler
  • Oversettelsesplugins
  • Egne tittelfiltre

#Hvorfor CSS- eller JS-filer slutter å lastes riktig

Problem: CSS eller JS lastes ikke riktig.

Feilsøk:

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

Se etter:

  • Plugins som fjerner stilark
  • Motstridende enqueue-prioriteter
  • Skript som lastes i feil rekkefølge

#Sjekke eller fjerne en hooked funksjon i PHP

#Hvordan sjekke om en bestemt funksjon er hooket

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';
}

#Hvordan fjerne en hook midlertidig

// 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 );
            }
        }
    }
}

#Hvorfor remove_filter ikke gjør noe

remove_filter() og remove_action() returnerer en boolsk verdi og varsler aldri. Når de returnerer false, skjedde ingenting, og siden ser helt lik ut. Feilen er stille og forveksles lett med et cacheproblem. Det finnes fire ulike årsaker. De to første ser du i inspeksjonstabellen, de to siste ikke.

Feil prioritet. Fjerning matcher på prioritet i tillegg til callback. Registrerte pluginen på prioritet 20 og du fjerner på standard 10, mislykkes kallet. Kolonnen Priority finnes nettopp for å hindre dette. Les den, ikke anta 10.

Feil callback-identitet. For en objektmetode matcher WordPress på den unike ID-en bygd fra identiteten til akkurat den instansen (spl_object_hash() til og med 7.0, spl_object_id() fra 7.1). Lager du en ny instans av samme klasse og sender den til remove_filter(), får du en annen ID og fjerner ingenting. Du trenger det opprinnelige objektet, og det får du bare ved å hente $callback['function'][0] ut av callbacks-arrayen i $wp_filter, eller via en offentlig metode som pluginen tilbyr.

Fjerningen kjører før registreringen. Fjerning av hooks er ikke deklarativ. Kjører koden din på plugins_loaded og pluginen registrerer på init, fjernet du en callback som ikke fantes ennå, og den legges til et øyeblikk senere. Flytt fjerningen til en senere hook, eller til samme hook med et høyere prioritetstall.

Callbacken er en closure. Den har ikke noe navn du kan sende med, og du kan ikke lage det samme objektet på nytt. Den eneste veien er å hente selve closure-objektet ut av $wp_filter i samme forespørsel, som vist i neste seksjon.

Sjekk også returverdien riktig. has_filter() returnerer false når callbacken ikke er knyttet til hooken, og prioriteten som heltall når den er det. Prioritet 0 er en gyldig verdi og er falsy i PHP, så sammenlign med !== false i stedet for en vanlig sannhetstest. Fra WordPress 6.9 tar has_filter() og has_action() et tredje argument, $priority. Sender du både callback og prioritet, er returverdien en ren boolsk verdi for akkurat den prioriteten, som er den ryddigste sjekken før et remove_filter()-kall.

#Hvordan fjerne en anonym funksjon fra en hook

Fordi den unike ID-en lages på nytt for hver forespørsel, er den eneste pålitelige måten å koble fra en anonym funksjon å finne closure-objektet i callbacks-arrayen i samme forespørsel og gi akkurat det objektet tilbake til remove_filter(). WordPress bygger ID-en fra objektets identitet, så objektet du hentet ut, gir nøyaktig den nøkkelen det ble lagret under:

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;
}

Match på filen Reflection oppgir som deklarasjonssted, ikke på et navn du har gjettet. Det har betydning at du går via remove_filter() i stedet for å kalle unset() på callbacks-arrayen. WP_Hook holder en egen liste over prioriteter (den beskyttede egenskapen $priorities, lagt til i WordPress 6.4) og justerer en iterasjon som kjører akkurat nå. WP_Hook::remove_filter() oppdaterer begge når en prioritetsbøtte blir tom. En rå unset() oppdaterer ingen av dem, så en tømt bøtte blir liggende igjen som en tom array, og en fjerning mens hooken kjører kan få iterasjonen ut av takt. Kall funksjonen sent nok til at closuren allerede er registrert, og tidlig nok til at hooken ikke har fyrt ennå.

#Feil i hook-feilsøkingssnutten på PHP 8

Feilsøkingskode som selv krasjer, er verre enn ingen. Tidligere versjoner av denne snutten, også den denne artikkelen publiserte før, feiler på PHP 8 på fire punkter, og koden over unngår alle fire.

strpos() på en callback. En løkke som kaller strpos( $callback['function'], 'ProblemPlugin' ), fungerer bare så lenge function er en streng. Så snart den treffer en array-callback eller en closure, kaster PHP 8 en TypeError, fordi interne funksjoner ikke lenger godtar en array eller et objekt der de forventer en streng. Finn et navn først med is_string(), is_array() og is_object(), og sammenlign etterpå, slik eksemplet for massefjerning gjør.

ksort() på det levende objektet. Å sortere $wp_filter[ $hook ]->callbacks på stedet endrer global tilstand i den forespørselen du prøver å observere. WP_Hook::add_filter() holder allerede bøttene sortert ved å kalle ksort( $this->callbacks, SORT_NUMERIC ) hver gang en ny prioritet dukker opp, så sorteringen tilfører ingenting for korrektheten. Inspeksjonsfunksjonen sorterer en lokal kopi.

Unntak fra Reflection. new ReflectionFunction() godtar et vanlig funksjonsnavn eller en Closure og kaster ReflectionException på en streng på formen Class::method. ReflectionMethod kaster når metoden håndteres av __call() i stedet for å være deklarert, noe som er vanlig i plugins som bruker magiske metoder som hook-mellomledd. På PHP 8.4 gir det i tillegg en deprecation-melding å sende én enkelt Class::method-streng til new ReflectionMethod(), med anbefaling om ReflectionMethod::createFromMethodName(). Å dele strengen og sende to argumenter fungerer på alle PHP 8-versjoner. Funksjonen pakker alt inn i én try/catch og skriver ut unntaksmeldingen, så en manglende metode og en magisk metode kan skilles fra hverandre.

Hardkodede stier. Å finne kilden med delstrengene wp-content/plugins og wp-content/themes merker hver rad feil på nettsteder som har flyttet innholdsmappen med WP_CONTENT_DIR, og den kjenner aldri igjen must-use-plugins. Funksjonen sammenligner med WP_PLUGIN_DIR, WPMU_PLUGIN_DIR og get_theme_root() etter wp_normalize_path(), som også gjør Windows-stier sammenlignbare. Ett tilfelle gjenstår: Reflection rapporterer den oppløste stien til en symlinket plugin, så en slik callback havner i raden Other med den faktiske stien.

#Hvordan liste alle hooks med callbacks på siden

Snutten over ser på én hook om gangen. Vil du ha hele listen over hooks som har noe knyttet til seg i den aktuelle forespørselen, holder det å gå gjennom alle nøklene i $wp_filter. Navnet på en hook sier ikke om den er en action eller et filter. Det eneste WordPress registrerer, er at do_action() har økt telleren i $wp_actions, så funksjonen under skiller mellom actions som har fyrt og resten:

/**
 * List every hook that has callbacks attached on this request
 */
function wppoland_list_all_hooks() {
    global $wp_filter, $wp_actions;

    $rows = array();
    foreach ( $wp_filter as $hook_name => $hook ) {
        $count = 0;
        foreach ( $hook->callbacks as $callbacks ) {
            $count += count( $callbacks );
        }
        // $wp_actions only knows hooks fired through do_action(), so the rest are
        // filters or actions that have not fired yet.
        $type = isset( $wp_actions[ $hook_name ] ) ? 'action (fired)' : 'filter / not fired';
        $rows[ $hook_name ] = array( $type, $count );
    }
    ksort( $rows );

    echo '<pre style="font-size:11px;">';
    foreach ( $rows as $hook_name => $row ) {
        echo esc_html( sprintf( "%-50s %-20s %d\n", $hook_name, $row[0], $row[1] ) );
    }
    echo '</pre>';
}

// Usage: add_action( 'shutdown', 'wppoland_list_all_hooks' );

Query Monitor bruker samme skille: hooks-samleren leser $wp_actions som standard, mens $wp_filter bare leses når konstanten QM_SHOW_ALL_HOOKS er satt. Begrens visningen til administratorer på samme måte som med inspeksjonsfunksjonen.

#Liste hooks med Query Monitor

For en visuell liste er det vanlige verktøyet Query Monitor av John Blackbourn, gratis på WordPress.org. I readme-filen beskrives den som «utviklerverktøypanelet for WordPress og WooCommerce» (John Blackbourn, Query Monitor på WordPress.org, egen oversettelse), og hooks og actions står på listen over det den feilsøker.

#Hvorfor bruke Query Monitor til hooks

  1. Visuelt grensesnitt: én tabell per forespørsel, med filtre
  2. Filstier: hver callback lenker til fil og linje
  3. Komponentnavn: hver rad viser plugin, tema eller kjernekomponent som er ansvarlig
  4. Ingen kode: ingenting å lime inn i functions.php

Hooks-panelet måler ikke kjøretid per hook eller per callback. Kolonnene er hook-navn, type (action eller filter, når alle hooks vises), prioritet, callback og komponent. For tidsbruk har Query Monitor egne paneler, og en profiler er riktig verktøy for kostnaden per callback.

#Installasjon

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

# Or download it from WordPress.org

#Hvordan finne en hook i Query Monitor

  1. Installer og aktiver Query Monitor
  2. Åpne en hvilken som helst side mens du er logget inn som administrator
  3. Åpne Query Monitor-menyen i adminverktøylinjen, som viser en oversikt over den aktuelle siden
  4. Velg panelet Hooks & Actions
  5. Filtrer på hook-navn eller komponent
  6. Les av de tilknyttede callbackene med prioritet og komponent

Én detalj avgjør om dette fungerer for the_content. Som standard lister hooks-samleren bare navnene i $wp_actions, altså actions som har fyrt minst én gang i forespørselen. Filters som the_content, the_title eller excerpt_length er ikke med på den listen. For å ta dem med legger du dette i wp-config.php på et utviklingsnettsted:

define( 'QM_SHOW_ALL_HOOKS', true );

Med konstanten satt leser samleren alle nøklene i $wp_filter i stedet, panelet får navnet Hooks, Actions, & Filters, og en typekolonne skiller actions fra filters. En annen konstant, QM_HIDE_CORE_ACTIONS, skjuler callbacks som hører til kjernen, og korter ned tabellen når du bare bryr deg om plugins. Query Monitor flagger også den spesielle hooken all: er noe knyttet til den, viser panelet advarselen «all-actionen er ekstremt ressurskrevende. Prøv å unngå å bruke den.» (Query Monitor, hooks-panelet, egen oversettelse).

Som standard viser Query Monitor utdataene bare til administratorer på enkeltnettsteder og til superadministratorer på multisite, så den kan trygt stå aktiv på en stagingkopi.

#Når bruke Query Monitor eller en egen snutt

Bruk Query Monitor når:

  • Du trenger en permanent feilsøkingsløsning
  • Du også vil se databasespørringer, HTTP-kall og PHP-feil for den samme forespørselen
  • Du feilsøker mange hooks
  • Du foretrekker et grensesnitt framfor kode

Bruk en egen snutt når:

  • Du trenger rask feilsøking én gang
  • Du jobber i et utviklingsmiljø
  • Du vil sjekke hooks fra kode
  • Query Monitor ikke er tilgjengelig

#Hvordan liste WordPress hooks med WP-CLI

Når nettstedet ikke vil rendre, når adminverktøylinjen ikke er tilgjengelig, eller når du sitter på en stagingserver over SSH, leser WP-CLI de samme globale variablene uten nettleser:

# 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

Tre ting kan overraske. For det første laster WP-CLI WordPress, men kjører aldri malstakken, så template_redirect, wp_head og wp_footer fyrer aldri, og alt som registreres inne i dem, mangler i utskriften. Callbacks som legges til på plugins_loaded, after_setup_theme og init, er alle med, og det dekker de fleste plugin-registreringer. For det andre kjører WP-CLI uten innlogget bruker med mindre du sender --user, så registreringer som avhenger av rettigheter, oppfører seg annerledes enn i nettleseren. For det tredje kjører wp eval koden din inne i en metode, så kommandodokumentasjonen advarer selv om at globale variabler som $wp_filter må deklareres med global først, noe enlinjeren gjør. Dokumentasjonen viser også et --hook=<hook>-valg som kjører koden etter at en gitt hook har fyrt, men merker det som ennå ikke del av en stabil WP-CLI-utgave. Det krever nightly-bygget (wp cli update --nightly).

De samme verktøyene gir en rask halvering når inspeksjonstabellen peker på en plugin, men du vil ha bevis:

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 og --skip-themes tar en kommaseparert liste og slår av lasting bare for den ene kommandoen, så ingenting deaktiveres for ekte besøkende. Kjører du samme sjekk to ganger, én gang vanlig og én gang uten den mistenkte pluginen, får du et sikkert svar, i motsetning til å lese en rekonstruert filsti.

#Hvordan feilsøke hooks i REST-, AJAX- og cron-forespørsler

En tabell i bunnteksten fungerer bare på forespørsler som rendrer en temamal. Mange reelle hook-feil oppstår i forespørsler som ikke gjør det: REST API-svar under /wp-json/, kall til admin-ajax.php, WP-Cron-hendelser, blokkrendering ved lagring og CLI-kjøringer. HTML skrevet ut i noen av dem havner enten ingen steder eller ødelegger et JSON-svar, som da ser ut som en annen, urelatert feil.

I slike sammenhenger skriver du til loggen i stedet for til siden. Sett WP_DEBUG og WP_DEBUG_LOG til true i wp-config.php, la WP_DEBUG_DISPLAY stå på false så ingenting lekker ut i svaret, og send utskriften gjennom error_log(). Den havner i wp-content/debug.log, og du kan følge den direkte med tail -f wp-content/debug.log mens du gjentar forespørselen som feiler. Under WP-CLI skriver WP_CLI::log() til terminalen i stedet.

Actionen shutdown er det riktige ankeret for denne typen inspeksjon. Den fyrer på slutten av nesten alle forespørselstyper, etter at alle betingede registreringer har skjedd, og bryr seg ikke om en mal var involvert. Kombiner den med wp_doing_ajax(), wp_doing_cron() og defined( 'REST_REQUEST' ), så hver logglinje viser hvilken type forespørsel som produserte den. Uten den merkingen er en logg full av the_content-utskrifter fra blandede forespørsler uleselig.

#Beste praksis for feilsøking og navngiving av hooks

#1. Feilsøk bare i utvikling

La aldri feilsøkingskode ligge igjen i produksjon:

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

#2. Begrens til administratorer

Sjekk alltid brukerrettigheter:

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

#3. Bruk beskrivende hook-navn

Bruk prefiks når du lager egne hooks:

// Good
do_action( 'wppoland_before_content' );

// Bad
do_action( 'before_content' );

#4. Dokumenter hookene dine

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

#Hook fyrer ikke eller funksjonen kjører ikke

#Hvorfor hooken ikke vises

Mulige årsaker:

  1. Hooken har ikke fyrt ennå (sjekk tidspunktet)
  2. Betinget logikk hindrer hooken i å fyre
  3. Hook-navnet er feilstavet

Løsning:

// 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)';

#Hvorfor den hookede funksjonen ikke kjører

Mulige årsaker:

  1. Prioritetskonflikt
  2. Betinget logikk
  3. Hooken er fjernet av en annen plugin

Løsning:

// 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;

#Hvordan logge rekkefølgen WordPress hooks fyrer i

Å liste hva som er knyttet til en hook, svarer på ett spørsmål. Det svarer ikke på det andre: i hvilken rekkefølge ting faktisk kjørte i denne forespørselen. WordPress reserverer hook-navnet all. Hvert kall til do_action() og apply_filters() sjekker etter det før hookens egne callbacks kjører, og sender videre via _wp_call_all_hook(), så en callback knyttet til all ser alle actions og filters som fyrer:

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'] ) ) );
} );

To regler gjør dette brukbart. Registrer den så tidlig som mulig, helst fra en MU-plugin, fordi alt som fyrer før callbacken din er knyttet til, er usynlig. Og filtrer alltid på prefiks før du logger, fordi én enkelt front-end-forespørsel fyrer hundrevis av hooks, og en ufiltrert sporing gir en logg ingen leser. Dybden fra $wp_current_filter er det som viser at et filter kjørte nestet inne i et annet, og det er den vanlige forklaringen når en callback ser ut til å kjøre to ganger.

Kostnaden er reell: all legger til et funksjonskall for hver hook som kjøres i forespørselen. Bruk den til å finne rekkefølgen, og slett den etterpå. Den hører ikke hjemme i en permanent feilsøkings-MU-plugin.

#Hvordan bekrefte at fjerning av en hook virket

At siden ser riktig ut etter at du fjernet en callback, er ikke bevis. Gå gjennom de samme sjekkene hver gang, i denne rekkefølgen.

Før du endrer noe, noter den nåværende tilstanden så du har noe å sammenligne med. Skriv ned den nøyaktige prioriteten fra inspeksjonstabellen, callback-ID-en og den oppløste kildefilen. Lagre den rendrede utgaven av én berørt URL, utlogget, så du kan sammenligne senere. Bekreft at symptomet faktisk kan gjenskapes i en ren forespørsel og ikke bare i din egen nettleserøkt.

Etter endringen kjører du inspeksjonen på samme hook igjen og bekrefter at raden er borte, ikke bare at siden ser annerledes ut. has_filter( 'the_content', 'callback_name' ) === false er den maskinlesbare versjonen av den samme sjekken og hører hjemme i en WP-CLI-enlinjer. Bekreft så at fjerningen ikke tok med seg noe annet: antall callbacks på den prioriteten skal gå ned med nøyaktig én.

Utelukk deretter caching, som gir både falske feil og falske suksesser. Tøm objektcachen med wp cache flush, tøm eventuell sidecache eller CDN foran nettstedet, og last siden på nytt som utlogget besøkende i stedet for som administratoren som går forbi det meste av cachingen. Kjører verten OPcache med opcache.validate_timestamps slått av, er endringer i en MU-plugin ikke aktive før cachen er nullstilt, så en endring som ikke ser ut til å virke, kjører kanskje ikke ennå.

Sjekk til slutt rekkevidden. En fjerning som retter én sidemal, kan ødelegge en annen som var avhengig av den samme callbacken. Test minst ett enkeltinnlegg, ett arkiv, én adminside og ett REST-endepunkt. Har du fjernet et filter som annen kode regnet med, viser feilen seg som regel som uescapet eller uformatert utdata et helt annet sted, ikke som en PHP-feil.

Når rettelsen er bekreftet, erstatter du den midlertidige fjerningen med noe permanent og dokumentert: en navngitt funksjon i en MU-plugin med en kommentar som oppgir hooken, prioriteten, pluginen den gjelder og datoen. Anonyme fjerninger spredt rundt i functions.php er grunnen til at neste person må gjenta hele undersøkelsen.

#Finn hook-feil etter en plugin-oppdatering

Mange hook-feil oppstår når en plugin eller et tema oppdateres og en callback får ny prioritet eller forsvinner. Kjør derfor wppoland_inspect_hook() på de hookene nettstedet ditt er mest avhengig av, typisk the_content, the_title og wp_enqueue_scripts, før du oppdaterer, og lagre tabellen. Gjør det samme etterpå. Se på de tre kolonnene: en ny funksjon med prioritet 999 på the_content, en SEO- eller oversettelsesplugin som dukker opp på the_title, eller en callback som har byttet kilde fra tema til plugin, forklarer som regel feilen før du har åpnet en eneste fil.

Gjør sammenligningen i et utviklings- eller stagingmiljø med WP_DEBUG aktivert og visningen begrenset til brukere med manage_options, slik rådene over sier. Query Monitor gir samme oversikt i hooks-panelet, med filters først når QM_SHOW_ALL_HOOKS er satt. Gjør oppdateringen siden tregere, må du se i Query Monitors egne tidspaneler eller en profiler, fordi hooks-panelet ikke viser kjøretid. Må en callback ut midlertidig, sjekk først med has_filter() at den faktisk er registrert, og bruk remove_filter() med nøyaktig den prioriteten tabellen viser.

#Sammendrag

Den egne snutten viser for hver hook:

  • Hvilke funksjoner som er hooket
  • Utførelsesprioriteten deres
  • Hvilken plugin, hvilket tema eller hvilken kjernefil som deklarerer dem
  • Hvor kildefilene ligger

Hovedpoenger:

  • Les $wp_filter[ $hook ]->callbacks, den offentlige egenskapen til WP_Hook, og sorter en kopi, aldri det levende objektet
  • Query Monitor viser filters som the_content bare når QM_SHOW_ALL_HOOKS er definert
  • Fjern callbacks med remove_filter() og nøyaktig prioritet, med det opprinnelige objektet for metoder og closures
  • Bruk did_action(), current_filter() og hooken all for spørsmål om tidspunkt og rekkefølge
  • Begrens feilsøkingsutdata til utviklingsmiljøer og administratorer

Sitter du fast i et hook-hierarki ingen lenger har oversikt over, graver vi i det sammen med deg under feilsøking og WordPress-utvikling.

Neste steg

Gjør artikkelen om til faktisk implementering

Denne blokken styrker intern lenking og sender leseren videre til de mest relevante tjenestene og innholdet.

Vil du få dette implementert på nettstedet ditt?

Hvis du vil gjøre kunnskapen i artikkelen om til konkrete forbedringer, redesign eller en tydelig leveranseplan, kan jeg ta det videre.

Relevant klynge

Utforsk andre WordPress-tjenester og kunnskapsbase

Styrk virksomheten din med profesjonell teknisk støtte innen kjerneområdene i WordPress-økosystemet.

Relaterte artikler