DEV Community

Cover image for Debugging WordPress PHP Callbacks With Xdebug
Martijn Assie
Martijn Assie

Posted on

Debugging WordPress PHP Callbacks With Xdebug

Problem: Hook fires, callback runs, something breaks

Error log: Fatal error on line 42

Line 42: return apply_filters('some_filter', $value);

Me staring at code: "Which of these 8 callbacks is causing this?!?"

Before Xdebug:

  • Add var_dump() everywhere
  • Reload page 50 times
  • Still guessing
  • 4 hours wasted!!

After Xdebug:

  • Set breakpoint on line 42
  • Step INTO filter callbacks
  • See exact callback causing issue
  • Fixed in 10 minutes!!

Here's how to debug WordPress PHP callbacks like a pro using Xdebug:

Why Xdebug For WordPress Callbacks?

WordPress = hook-based architecture

add_action('save_post', 'my_callback', 10);
add_filter('the_content', 'another_callback', 20);
add_action('wp_head', 'third_callback', 5);
Enter fullscreen mode Exit fullscreen mode

Problem:

  • 10 plugins hooking same action
  • Which callback is breaking?
  • Can't see execution order
  • var_dump() only shows ONE callback!!

Xdebug solution:

  • Set breakpoint on hook
  • Step through EACH callback
  • Inspect variables at each step
  • See call stack
  • No more guessing!!

Setup: Local + VS Code + Xdebug

Prerequisites

Install:

  • Local by Flywheel (includes Xdebug built-in!)
  • VS Code
  • PHP Debug extension for VS Code

Local download: https://localwp.com/

Step 1: Enable Xdebug in Local

Open Local → Select your site → Bottom of page:

Toggle Xdebug ON

That's it!! Local handles all php.ini configuration!!

Verify Xdebug is running:

Create /wp-content/xdebug-test.php:

<?php
phpinfo();
Enter fullscreen mode Exit fullscreen mode

Visit: yoursite.local/wp-content/xdebug-test.php

Search for "Xdebug" section → should show:

xdebug support: enabled
Version: 3.x
Enter fullscreen mode Exit fullscreen mode

Step 2: Install PHP Debug Extension in VS Code

VS Code → Extensions → Search "PHP Debug"

Install: PHP Debug (by Xdebug)

Step 3: Configure VS Code

Open your site folder in VS Code

Click Debug icon (sidebar) → Create launch.json:

File: .vscode/launch.json

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Listen for Xdebug",
      "type": "php",
      "request": "launch",
      "port": 9003,
      "pathMappings": {
        "/app/public": "${workspaceRoot}"
      },
      "log": false
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Key settings:

port: 9003 - Xdebug 3.x uses port 9003 (Xdebug 2.x used 9000)

pathMappings - Maps Local's internal path to your workspace

Step 4: Install Xdebug Browser Extension

Chrome: Xdebug Helper

Firefox: Xdebug Helper

Enable debugging:

  • Click extension icon
  • Select "Debug"
  • Icon turns green ✅

This tells browser to send XDEBUG_SESSION cookie!!

Debugging WordPress Hooks

Example 1: Debug save_post Callbacks

Scenario: Post saves slowly, need to find which callback is slow

Code: wp-content/plugins/my-plugin/my-plugin.php

<?php
add_action('save_post', 'my_slow_callback', 10, 2);

function my_slow_callback($post_id, $post) {
    // Set breakpoint HERE ↓
    $data = get_post_meta($post_id, 'some_key', true);

    // Slow API call
    $result = wp_remote_get('https://api.example.com/data');

    if (is_wp_error($result)) {
        return;
    }

    update_post_meta($post_id, 'api_data', $result['body']);
}
Enter fullscreen mode Exit fullscreen mode

Steps:

  1. Click left of line number to set breakpoint (red dot appears)
  2. VS Code → Debug panel → Click green play "Listen for Xdebug" (F5)
  3. Browser → Edit any post → Click "Update"
  4. VS Code highlights breakpoint line!!

Now you can:

Inspect variables:

  • Hover over $post_id → see value
  • Hover over $data → see what was retrieved
  • Expand arrays/objects

Step through code:

  • F10: Step Over (run line, don't enter functions)
  • F11: Step Into (enter function calls)
  • Shift+F11: Step Out (exit current function)

Check call stack:

  • See entire execution path
  • Trace back to what triggered this callback

Example 2: Multiple Callbacks on Same Hook

Problem: 5 plugins all hooking the_content, one breaks

Code:

<?php
// Plugin A
add_filter('the_content', 'plugin_a_content', 10);

// Plugin B
add_filter('the_content', 'plugin_b_content', 20);

// Plugin C
add_filter('the_content', 'plugin_c_content', 30);
Enter fullscreen mode Exit fullscreen mode

Debug strategy:

Set breakpoint in WordPress core where filter fires:

File: wp-includes/plugin.php

Find apply_filters() function:

<?php
public function apply_filters($value, $args) {
    // Set breakpoint HERE ↓
    foreach ($this->callbacks as $priority => $callbacks) {
        foreach ($callbacks as $callback) {
            // Step INTO to see each callback
            $value = call_user_func_array($callback['function'], $args);
        }
    }
    return $value;
}
Enter fullscreen mode Exit fullscreen mode

Now:

  • F11 (Step Into) on call_user_func_array()
  • Debugger jumps to actual callback function
  • Inspect variables
  • Continue to next callback
  • Find exact callback causing issue!!

Example 3: Debugging AJAX Callbacks

Problem: AJAX request returns error, can't see why

Code:

<?php
add_action('wp_ajax_my_action', 'my_ajax_callback');
add_action('wp_ajax_nopriv_my_action', 'my_ajax_callback');

function my_ajax_callback() {
    // Set breakpoint HERE ↓
    $post_id = $_POST['post_id'];

    if (!$post_id) {
        wp_send_json_error('Missing post ID');
    }

    $data = get_post($post_id);

    wp_send_json_success($data);
}
Enter fullscreen mode Exit fullscreen mode

Trigger AJAX in browser console:

jQuery.post(ajaxurl, {
    action: 'my_action',
    post_id: 123
});
Enter fullscreen mode Exit fullscreen mode

Debugger stops on breakpoint!!

Inspect:

  • $_POST array
  • $post_id value
  • $data content

Common AJAX issues found:

  • $_POST empty (missing data in JS)
  • Wrong variable types
  • Database queries failing
  • Permission checks failing

Advanced: Conditional Breakpoints

Problem: Hook fires 100 times, only want to debug when $post_id == 456

Solution: Right-click breakpoint → Edit Breakpoint → Add condition

Condition:

$post_id == 456
Enter fullscreen mode Exit fullscreen mode

Now debugger only stops when condition is TRUE!!

Other useful conditions:

// Only debug for specific user
get_current_user_id() == 1

// Only debug for specific post type
$post->post_type == 'product'

// Only debug when variable is empty
empty($data)

// Only debug on error
is_wp_error($result)
Enter fullscreen mode Exit fullscreen mode

Debugging Hooks Execution Order

Problem: Callbacks running in wrong order

Use: Query Monitor + Xdebug combo

Install Query Monitor plugin

Query Monitor shows all hooks and priorities:

Hook: save_post
  - my_callback (priority 10)
  - another_callback (priority 20)
  - third_callback (priority 5) ← runs FIRST!
Enter fullscreen mode Exit fullscreen mode

Then:

Set breakpoint in each callback

See execution order in real-time!!

Common WordPress Debugging Scenarios

Scenario 1: Filter Returns NULL

Problem:

add_filter('the_title', 'my_title_filter');

function my_title_filter($title) {
    // Forgot to return!!
    $title = strtoupper($title);
}
Enter fullscreen mode Exit fullscreen mode

Without Xdebug: All titles disappear, WTF?!

With Xdebug:

  • Set breakpoint on $title = strtoupper($title);
  • Step Over (F10)
  • See function ends without return
  • Instant diagnosis!!

Fix:

function my_title_filter($title) {
    $title = strtoupper($title);
    return $title; // ← ADD THIS!!
}
Enter fullscreen mode Exit fullscreen mode

Scenario 2: Hook Priority Conflict

Code:

// Theme
add_action('init', 'theme_setup', 10);

// Plugin
add_action('init', 'plugin_setup', 10); // Same priority!!
Enter fullscreen mode Exit fullscreen mode

Problem: Execution order undefined when priority is same

Debug:

Set breakpoint in both functions

See which runs first

Fix: Change priority

add_action('init', 'plugin_setup', 5); // Run before theme
Enter fullscreen mode Exit fullscreen mode

Scenario 3: Callback Receives Wrong Arguments

Problem:

add_action('save_post', 'my_callback');

function my_callback($post_id) {
    // Missing $post parameter!!
    var_dump($post); // Undefined variable
}
Enter fullscreen mode Exit fullscreen mode

Xdebug shows:

Function only receives 1 argument but hook passes 3:

do_action('save_post', $post_id, $post, $update);
Enter fullscreen mode Exit fullscreen mode

Fix:

add_action('save_post', 'my_callback', 10, 3); // Accept 3 args!!

function my_callback($post_id, $post, $update) {
    // Now we have all arguments
}
Enter fullscreen mode Exit fullscreen mode

VS Code Debugging Panel Explained

When breakpoint hits:

Variables section:

  • Locals: Current function variables
  • Superglobals: $_GET, $_POST, $_SERVER, etc.
  • Constants: WP_DEBUG, ABSPATH, etc.

Watch section:

  • Add custom expressions
  • Example: $post->post_status
  • Updates in real-time as you step through

Call Stack section:

  • Shows execution path
  • Click any frame to see that context
  • Trace back to original trigger

Breakpoints section:

  • All breakpoints listed
  • Toggle on/off
  • Conditional breakpoints marked

Debug Console:

  • Run PHP expressions
  • Example: get_option('siteurl')
  • Evaluate during execution

Troubleshooting Xdebug

Breakpoints Not Hitting

Check:

  1. Xdebug enabled in Local? (toggle ON)
  2. Browser extension enabled? (green icon)
  3. VS Code listening? (F5 pressed)
  4. Port correct in launch.json? (9003 for Xdebug 3.x)

Verify connection:

Add to wp-config.php:

<?php
if (function_exists('xdebug_info')) {
    error_log('Xdebug is loaded!');
}
Enter fullscreen mode Exit fullscreen mode

Check error log → should see message

Path Mapping Issues

Problem: Breakpoints show as "unverified" (gray circle)

Cause: Path mismatch between Local and VS Code

Fix: Update pathMappings in launch.json

Local's internal path: /app/public

Your workspace: /Users/you/Local Sites/yoursite/app/public

Mapping:

"pathMappings": {
  "/app/public": "${workspaceRoot}"
}
Enter fullscreen mode Exit fullscreen mode

Test: Click breakpoint, should turn red

Performance Impact

Xdebug slows down PHP execution

Solution: Only enable when debugging

Local makes this easy → toggle ON/OFF

Pro tip: Use conditional breakpoints to reduce overhead

Bottom Line

Stop using var_dump() for WordPress debugging!!

Xdebug shows:

  • Exact callback causing issue
  • Variable values at each step
  • Execution order
  • Call stack
  • Everything you need to fix bugs FAST!!

My debugging time:

Before Xdebug:

  • Simple bug: 30 minutes
  • Complex hook issue: 4 hours
  • "WTF is happening?!" moments: Daily

After Xdebug:

  • Simple bug: 5 minutes
  • Complex hook issue: 30 minutes
  • "WTF is happening?!" moments: GONE!!

Setup time: 10 minutes

Time saved: HUNDREDS of hours

Xdebug = WordPress developer superpower!! 🔥

This article contains affiliate links!

Top comments (1)

Collapse
 
martijn_assie_12a2d3b1833 profile image
Martijn Assie

Been using Xdebug for 2 years now... used to waste hours with var_dumps trying to find which save_post callback was breaking. Now just set breakpoint, step into each callback, find issue in 5 minutes. Call stack feature is amazing for seeing full execution path. Pro tip: conditional breakpoints when hooks fire 100+ times 🔥