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 ? It will become hidden in your post, but will still be visible via the comment's permalink. as well , this person and/or 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: 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(); CODE_BLOCK: <?php phpinfo(); CODE_BLOCK: <?php phpinfo(); CODE_BLOCK: xdebug support: enabled Version: 3.x 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 } ] } 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']); } 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); 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; } 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); } 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 }); 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 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) 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! 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); } 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!! } 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!! 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 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 } 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); 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 } 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!'); } 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}" } 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
toolsutilitiessecurity toolsdebuggingwordpresscallbacksxdebugprerequisites