Tools
Tools: Debugging WordPress PHP Callbacks With Xdebug
2026-02-03
0 views
admin
Why Xdebug For WordPress Callbacks? ## Setup: Local + VS Code + Xdebug ## Prerequisites ## Step 1: Enable Xdebug in Local ## Step 2: Install PHP Debug Extension in VS Code ## Step 3: Configure VS Code ## Step 4: Install Xdebug Browser Extension ## Debugging WordPress Hooks ## Example 1: Debug save_post Callbacks ## Example 2: Multiple Callbacks on Same Hook ## Example 3: Debugging AJAX Callbacks ## Advanced: Conditional Breakpoints ## Debugging Hooks Execution Order ## Common WordPress Debugging Scenarios ## Scenario 1: Filter Returns NULL ## Scenario 2: Hook Priority Conflict ## Scenario 3: Callback Receives Wrong Arguments ## VS Code Debugging Panel Explained ## Troubleshooting Xdebug ## Breakpoints Not Hitting ## Path Mapping Issues ## Performance Impact ## Bottom Line 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?!?" Here's how to debug WordPress PHP callbacks like a pro using Xdebug: WordPress = hook-based architecture Local download: https://localwp.com/ Open Local → Select your site → Bottom of page: That's it!! Local handles all php.ini configuration!! Verify Xdebug is running: Create /wp-content/xdebug-test.php: Visit: yoursite.local/wp-content/xdebug-test.php Search for "Xdebug" section → should show: VS Code → Extensions → Search "PHP Debug" Install: PHP Debug (by Xdebug) Open your site folder in VS Code Click Debug icon (sidebar) → Create launch.json: File: .vscode/launch.json port: 9003 - Xdebug 3.x uses port 9003 (Xdebug 2.x used 9000) pathMappings - Maps Local's internal path to your workspace Chrome: Xdebug Helper Firefox: Xdebug Helper This tells browser to send XDEBUG_SESSION cookie!! Scenario: Post saves slowly, need to find which callback is slow Code: wp-content/plugins/my-plugin/my-plugin.php Problem: 5 plugins all hooking the_content, one breaks Set breakpoint in WordPress core where filter fires: File: wp-includes/plugin.php Find apply_filters() function: Problem: AJAX request returns error, can't see why Trigger AJAX in browser console: Debugger stops on breakpoint!! Common AJAX issues found: Problem: Hook fires 100 times, only want to debug when $post_id == 456 Solution: Right-click breakpoint → Edit Breakpoint → Add condition Now debugger only stops when condition is TRUE!! Other useful conditions: Problem: Callbacks running in wrong order Use: Query Monitor + Xdebug combo Install Query Monitor plugin Query Monitor shows all hooks and priorities: Set breakpoint in each callback See execution order in real-time!! Without Xdebug: All titles disappear, WTF?! Problem: Execution order undefined when priority is same Set breakpoint in both functions Function only receives 1 argument but hook passes 3: When breakpoint hits: Add to wp-config.php: Check error log → should see message 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 Test: Click breakpoint, should turn red 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 Stop using var_dump() for WordPress debugging!! Setup time: 10 minutes Time saved: HUNDREDS of hours Xdebug = WordPress developer superpower!! 🔥 This article contains affiliate links! Templates let you quickly answer FAQs or store snippets for re-use. 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 🔥 Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse CODE_BLOCK:
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 CODE_BLOCK:
add_action('save_post', 'my_callback', 10);
add_filter('the_content', 'another_callback', 20);
add_action('wp_head', 'third_callback', 5); CODE_BLOCK:
add_action('save_post', 'my_callback', 10);
add_filter('the_content', 'another_callback', 20);
add_action('wp_head', 'third_callback', 5); CODE_BLOCK:
<?php
phpinfo(); Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
<?php
phpinfo(); CODE_BLOCK:
<?php
phpinfo(); CODE_BLOCK:
xdebug support: enabled
Version: 3.x Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
xdebug support: enabled
Version: 3.x CODE_BLOCK:
xdebug support: enabled
Version: 3.x CODE_BLOCK:
{ "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 CODE_BLOCK:
{ "version": "0.2.0", "configurations": [ { "name": "Listen for Xdebug", "type": "php", "request": "launch", "port": 9003, "pathMappings": { "/app/public": "${workspaceRoot}" }, "log": false } ]
} CODE_BLOCK:
{ "version": "0.2.0", "configurations": [ { "name": "Listen for Xdebug", "type": "php", "request": "launch", "port": 9003, "pathMappings": { "/app/public": "${workspaceRoot}" }, "log": false } ]
} CODE_BLOCK:
<?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 CODE_BLOCK:
<?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']);
} CODE_BLOCK:
<?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']);
} CODE_BLOCK:
<?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 CODE_BLOCK:
<?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); CODE_BLOCK:
<?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); COMMAND_BLOCK:
<?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 COMMAND_BLOCK:
<?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;
} COMMAND_BLOCK:
<?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;
} CODE_BLOCK:
<?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 CODE_BLOCK:
<?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);
} CODE_BLOCK:
<?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);
} CODE_BLOCK:
jQuery.post(ajaxurl, { action: 'my_action', post_id: 123
}); Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
jQuery.post(ajaxurl, { action: 'my_action', post_id: 123
}); CODE_BLOCK:
jQuery.post(ajaxurl, { action: 'my_action', post_id: 123
}); CODE_BLOCK:
$post_id == 456 Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
$post_id == 456 CODE_BLOCK:
$post_id == 456 CODE_BLOCK:
// 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 CODE_BLOCK:
// 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) CODE_BLOCK:
// 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) CODE_BLOCK:
Hook: save_post - my_callback (priority 10) - another_callback (priority 20) - third_callback (priority 5) ← runs FIRST! Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
Hook: save_post - my_callback (priority 10) - another_callback (priority 20) - third_callback (priority 5) ← runs FIRST! CODE_BLOCK:
Hook: save_post - my_callback (priority 10) - another_callback (priority 20) - third_callback (priority 5) ← runs FIRST! CODE_BLOCK:
add_filter('the_title', 'my_title_filter'); function my_title_filter($title) { // Forgot to return!! $title = strtoupper($title);
} Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
add_filter('the_title', 'my_title_filter'); function my_title_filter($title) { // Forgot to return!! $title = strtoupper($title);
} CODE_BLOCK:
add_filter('the_title', 'my_title_filter'); function my_title_filter($title) { // Forgot to return!! $title = strtoupper($title);
} CODE_BLOCK:
function my_title_filter($title) { $title = strtoupper($title); return $title; // ← ADD THIS!!
} Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
function my_title_filter($title) { $title = strtoupper($title); return $title; // ← ADD THIS!!
} CODE_BLOCK:
function my_title_filter($title) { $title = strtoupper($title); return $title; // ← ADD THIS!!
} CODE_BLOCK:
// Theme
add_action('init', 'theme_setup', 10); // Plugin
add_action('init', 'plugin_setup', 10); // Same priority!! Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
// Theme
add_action('init', 'theme_setup', 10); // Plugin
add_action('init', 'plugin_setup', 10); // Same priority!! CODE_BLOCK:
// Theme
add_action('init', 'theme_setup', 10); // Plugin
add_action('init', 'plugin_setup', 10); // Same priority!! CODE_BLOCK:
add_action('init', 'plugin_setup', 5); // Run before theme Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
add_action('init', 'plugin_setup', 5); // Run before theme CODE_BLOCK:
add_action('init', 'plugin_setup', 5); // Run before theme CODE_BLOCK:
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 CODE_BLOCK:
add_action('save_post', 'my_callback'); function my_callback($post_id) { // Missing $post parameter!! var_dump($post); // Undefined variable
} CODE_BLOCK:
add_action('save_post', 'my_callback'); function my_callback($post_id) { // Missing $post parameter!! var_dump($post); // Undefined variable
} CODE_BLOCK:
do_action('save_post', $post_id, $post, $update); Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
do_action('save_post', $post_id, $post, $update); CODE_BLOCK:
do_action('save_post', $post_id, $post, $update); CODE_BLOCK:
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 CODE_BLOCK:
add_action('save_post', 'my_callback', 10, 3); // Accept 3 args!! function my_callback($post_id, $post, $update) { // Now we have all arguments
} CODE_BLOCK:
add_action('save_post', 'my_callback', 10, 3); // Accept 3 args!! function my_callback($post_id, $post, $update) { // Now we have all arguments
} CODE_BLOCK:
<?php
if (function_exists('xdebug_info')) { error_log('Xdebug is loaded!');
} Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
<?php
if (function_exists('xdebug_info')) { error_log('Xdebug is loaded!');
} CODE_BLOCK:
<?php
if (function_exists('xdebug_info')) { error_log('Xdebug is loaded!');
} CODE_BLOCK:
"pathMappings": { "/app/public": "${workspaceRoot}"
} Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
"pathMappings": { "/app/public": "${workspaceRoot}"
} CODE_BLOCK:
"pathMappings": { "/app/public": "${workspaceRoot}"
} - Add var_dump() everywhere
- Reload page 50 times
- Still guessing
- 4 hours wasted!! - Set breakpoint on line 42
- Step INTO filter callbacks
- See exact callback causing issue
- Fixed in 10 minutes!! - 10 plugins hooking same action
- Which callback is breaking?
- Can't see execution order
- var_dump() only shows ONE callback!! - Set breakpoint on hook
- Step through EACH callback
- Inspect variables at each step
- See call stack
- No more guessing!! - Local by Flywheel (includes Xdebug built-in!)
- PHP Debug extension for VS Code - Click extension icon
- Select "Debug"
- Icon turns green ✅ - Click left of line number to set breakpoint (red dot appears)
- VS Code → Debug panel → Click green play "Listen for Xdebug" (F5)
- Browser → Edit any post → Click "Update"
- VS Code highlights breakpoint line!! - Hover over $post_id → see value
- Hover over $data → see what was retrieved
- Expand arrays/objects - F10: Step Over (run line, don't enter functions)
- F11: Step Into (enter function calls)
- Shift+F11: Step Out (exit current function) - See entire execution path
- Trace back to what triggered this callback - 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!! - $_POST array
- $post_id value
- $data content - $_POST empty (missing data in JS)
- Wrong variable types
- Database queries failing
- Permission checks failing - Set breakpoint on $title = strtoupper($title);
- Step Over (F10)
- See function ends without return
- Instant diagnosis!! - Locals: Current function variables
- Superglobals: $_GET, $_POST, $_SERVER, etc.
- Constants: WP_DEBUG, ABSPATH, etc. - Add custom expressions
- Example: $post->post_status
- Updates in real-time as you step through - Shows execution path
- Click any frame to see that context
- Trace back to original trigger - All breakpoints listed
- Toggle on/off
- Conditional breakpoints marked - Run PHP expressions
- Example: get_option('siteurl')
- Evaluate during execution - Xdebug enabled in Local? (toggle ON)
- Browser extension enabled? (green icon)
- VS Code listening? (F5 pressed)
- Port correct in launch.json? (9003 for Xdebug 3.x) - Exact callback causing issue
- Variable values at each step
- Execution order
- Everything you need to fix bugs FAST!! - Simple bug: 30 minutes
- Complex hook issue: 4 hours
- "WTF is happening?!" moments: Daily - Simple bug: 5 minutes
- Complex hook issue: 30 minutes
- "WTF is happening?!" moments: GONE!! - Location Sint Maartensdijk
- Joined Aug 31, 2025
how-totutorialguidedev.toaiserverdatabase