Wp debugging
WordPress development plugin for Claude Code
npx -y skills add iwritec0de/wp-dev --skill wp-debuggingAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
This skill should be used when the user asks to 'debug a WordPress error', 'fix a white screen', 'debug a plugin conflict', 'fix a 500 error', 'debug WooCommerce', 'fix a REST API error', 'debug a hook issue', 'investigate a fatal error', or mentions 'WordPress debugging', 'WP_DEBUG', 'error log', 'WSOD', 'plugin conflict', 'PHP fatal error', 'wp-cli debug'. Provides systematic WordPress/PHP debugging methodology covering fatal errors, white screens, plugin conflicts, REST API issues, and WooCommerce problems.
The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
28.9 KB, as published. Nobody here has run it
WordPress Debugging Methodology
Systematic debugging for WordPress and PHP. Four phases: root cause investigation, pattern analysis, hypothesis testing, implementation. No guessing. No shotgun fixes.
Critical Rules
- Never apply a fix without identifying the root cause. Guessing wastes time and introduces new bugs.
- Enable
WP_DEBUGfirst. Every debugging session starts by enabling debug mode. No exceptions. - Check error logs before guessing. The answer is almost always in
debug.log, the PHP error log, or the server error log. Read them. - One change at a time. If you change two things and the bug disappears, you do not know which change fixed it. Revert one and verify.
- Always test in staging. Never debug production by pushing untested changes to it. Clone the environment or use
wp-env. - Document what you find. Leave a comment explaining the root cause at the fix site. Future developers (including you) will thank you.
Phase 1: Root Cause Investigation
Before forming any hypothesis, gather evidence. This phase is about observation, not action.
1.1 Enable WordPress Debug Mode
Add these constants to wp-config.php above the /* That's all, stop editing! */ line:
// Enable debug mode — shows PHP errors and WordPress deprecation notices.
define( 'WP_DEBUG', true );
// Log errors to wp-content/debug.log instead of displaying on screen.
define( 'WP_DEBUG_LOG', true );
// Do NOT display errors on the frontend (security risk on production).
define( 'WP_DEBUG_DISPLAY', false );
@ini_set( 'display_errors', 0 );
// Load unminified versions of core CSS and JS files (useful for debugging core/Gutenberg).
define( 'SCRIPT_DEBUG', true );
Verify debug mode is active:
wp eval 'echo WP_DEBUG ? "WP_DEBUG is ON" : "WP_DEBUG is OFF";'
1.2 Read the Debug Log
# View the last 50 lines of the WordPress debug log.
tail -50 wp-content/debug.log
# Follow the log in real time while reproducing the bug.
tail -f wp-content/debug.log
# Search for fatal errors.
grep -i "fatal error" wp-content/debug.log | tail -20
# Search for a specific plugin's errors.
grep "my-plugin" wp-content/debug.log | tail -20
# Clear the log before a fresh reproduction attempt.
> wp-content/debug.log
1.3 Read PHP Error Logs
The PHP error log location varies by server configuration:
# Find the configured error log path.
php -r 'echo ini_get("error_log") . PHP_EOL;'
# Common locations.
tail -50 /var/log/php_errors.log
tail -50 /var/log/php-fpm/error.log
tail -50 /var/log/apache2/error.log
tail -50 /var/log/nginx/error.log
# On macOS with MAMP/Homebrew.
tail -50 /usr/local/var/log/php-fpm.log
1.4 Targeted Debugging with error_log()
Use error_log() to trace execution flow and inspect values. This is your printf-debugging for WordPress.
// Log a simple message.
error_log( 'DEBUG: my_function() was called' );
// Log a variable's value.
error_log( 'DEBUG: $post_id = ' . print_r( $post_id, true ) );
// Log an array or object.
error_log( 'DEBUG: $args = ' . print_r( $args, true ) );
// Log a stack trace to see who called this function.
error_log( 'DEBUG: backtrace — ' . wp_debug_backtrace_summary() );
// Conditional logging — only log for a specific post type.
if ( 'product' === get_post_type( $post_id ) ) {
error_log( 'DEBUG: Processing product #' . $post_id );
}
1.5 WP-CLI Debugging Commands
WP-CLI lets you interrogate WordPress state without touching the browser.
# Evaluate arbitrary PHP in the WordPress context.
wp eval 'var_dump( get_option("active_plugins") );'
# Open an interactive PHP shell with WordPress loaded.
wp shell
# Query the database directly.
wp db query "SELECT option_name, option_value FROM wp_options WHERE option_name = 'active_plugins';"
# Read a specific option.
wp option get siteurl
wp option get active_plugins --format=json
# Check if a plugin is active.
wp plugin is-active woocommerce && echo "Active" || echo "Inactive"
# Get plugin status and version info.
wp plugin list --status=active --format=table
# Check PHP and WordPress versions.
wp --info
wp core version
# Test if a function exists.
wp eval 'var_dump( function_exists("my_custom_function") );'
# Test if a class exists.
wp eval 'var_dump( class_exists("My_Custom_Class") );'
# Check a specific hook's registered callbacks.
wp eval '
global $wp_filter;
if ( isset( $wp_filter["init"] ) ) {
foreach ( $wp_filter["init"]->callbacks as $priority => $hooks ) {
foreach ( $hooks as $hook ) {
$callback = $hook["function"];
if ( is_array( $callback ) ) {
$callback = ( is_object( $callback[0] ) ? get_class( $callback[0] ) : $callback[0] ) . "::" . $callback[1];
}
error_log( "init @ priority {$priority}: {$callback}" );
}
}
}
'
1.6 Query Monitor Plugin
Install Query Monitor for in-browser debugging of hooks, database queries, HTTP requests, and more.
wp plugin install query-monitor --activate
Key panels to check:
- PHP Errors — caught errors, warnings, notices, deprecations
- Queries — slow queries, duplicate queries, queries by caller
- Hooks & Actions — what fired, in what order, what's attached
- HTTP API Calls — outbound requests, response codes, timing
- Transients — what was set, what was fetched
- Environment — PHP version, extensions, memory limits
1.7 Server Error Logs
# Apache error log.
sudo tail -100 /var/log/apache2/error.log
# Nginx error log.
sudo tail -100 /var/log/nginx/error.log
# PHP-FPM log (pool-specific).
sudo tail -100 /var/log/php8.2-fpm.log
# Systemd journal for PHP-FPM.
sudo journalctl -u php8.2-fpm --since "10 minutes ago" --no-pager
Phase 2: Common WordPress Bug Patterns
Use the evidence from Phase 1 to match against these known patterns.
2.1 White Screen of Death (WSOD)
Symptom: Blank white page. No error message. No HTML output at all.
Root cause: Almost always a PHP fatal error that kills execution before any output is sent.
Diagnosis:
# Step 1: Enable WP_DEBUG and check the log.
tail -20 wp-content/debug.log
# Step 2: If no log output, check the PHP error log.
php -r 'echo ini_get("error_log") . PHP_EOL;'
# Step 3: Check what changed recently.
wp plugin list --recently-active --format=table
# Step 4: Try loading WordPress from CLI (bypasses web server issues).
wp eval 'echo "WordPress loaded successfully.";'
Common causes:
- Syntax error in a recently edited file (
Parse error: syntax error, unexpected...) - Calling an undefined function (typo or missing dependency)
require/includeof a file that does not exist- Exhausted memory limit
2.2 500 Internal Server Error
Symptom: Server returns HTTP 500. May be intermittent.
Diagnosis:
# Check .htaccess for corruption.
cat .htaccess
# Regenerate .htaccess via WP-CLI.
wp rewrite flush
# Check PHP memory limit.
wp eval 'echo "Memory limit: " . ini_get("memory_limit") . PHP_EOL;'
# Check file permissions (typical: 755 for dirs, 644 for files).
find . -type d ! -perm 755 | head -20
find . -type f ! -perm 644 | head -20
# Check for .htaccess in subdirectories that may conflict.
find . -name ".htaccess" -not -path "./vendor/*" -not -path "./node_modules/*"
Common causes:
- Corrupted
.htaccess(regenerate withwp rewrite flush) - PHP memory exhaustion (increase
WP_MEMORY_LIMIT) - Incorrect file permissions (especially after deployment)
- PHP version incompatibility (check
phpinfo()output) - Broken plugin/theme throwing a fatal in a hooked callback
2.3 Plugin Conflicts
Symptom: Feature works in isolation, breaks when another plugin is active.
Diagnosis — systematic deactivation:
# Step 1: Record current active plugins.
wp plugin list --status=active --format=csv > /tmp/active-plugins.csv
# Step 2: Deactivate ALL plugins.
wp plugin deactivate --all
# Step 3: Activate ONLY your plugin. Test. If the bug is gone, it is a conflict.
wp plugin activate my-plugin
# Step 4: Activate other plugins one at a time. Test after each.
wp plugin activate plugin-a
# Test... works? Continue.
wp plugin activate plugin-b
# Test... broken! plugin-b conflicts with my-plugin.
# Step 5: Restore original state after testing.
wp plugin activate $(cat /tmp/active-plugins.csv | tail -n +2 | cut -d',' -f1 | tr '\n' ' ')
Common conflict sources:
- Two plugins enqueuing different versions of the same JS library (e.g., jQuery UI, Select2)
- Two plugins hooking the same filter and returning incompatible data
- Namespace collisions (two plugins defining the same function or class name)
- Two plugins modifying the same REST API endpoint
2.4 Hook and Filter Issues
Symptom: Callback never fires, fires at the wrong time, or receives wrong arguments.
// Problem: Callback registered with wrong number of accepted_args.
// add_filter signature: add_filter( $hook, $callback, $priority, $accepted_args )
// The filter passes 3 arguments, but accepted_args defaults to 1.
// WRONG — only receives $value, loses $post_id and $meta_key.
add_filter( 'get_post_metadata', 'my_meta_filter', 10 );
function my_meta_filter( $value, $post_id, $meta_key ) {
// $post_id and $meta_key are undefined here!
}
// CORRECT — explicitly accept 3 arguments.
add_filter( 'get_post_metadata', 'my_meta_filter', 10, 3 );
function my_meta_filter( $value, $post_id, $meta_key ) {
if ( 'my_key' === $meta_key ) {
return 'overridden_value';
}
return $value;
}
Debugging hook execution order:
// Check if an action has fired (and how many times).
error_log( 'init fired: ' . did_action( 'init' ) . ' times' );
// Check if we are currently inside a specific action.
if ( doing_action( 'save_post' ) ) {
error_log( 'We are inside save_post right now.' );
}
// Check if another plugin removed your hook.
wp eval '
global $wp_filter;
$has_hook = has_filter( "the_content", "my_content_filter" );
echo $has_hook !== false ? "Hook exists at priority {$has_hook}" : "Hook is MISSING";
'
Common causes:
- Wrong priority (your callback runs before the data it depends on is set)
- Missing
$accepted_argsparameter (defaults to 1) - Another plugin called
remove_action()/remove_filter()on your hook - Hooking too early (e.g., hooking in the global scope before WordPress loads the hook system)
2.5 REST API Errors
Symptom: API returns 401, 403, 500, or malformed responses.
// Problem: Permission callback missing or incorrect.
// WRONG — no permission_callback means WordPress 5.5+ shows a _doing_it_wrong notice.
register_rest_route( 'myplugin/v1', '/items', array(
'methods' => 'GET',
'callback' => 'my_get_items',
) );
// CORRECT — always specify a permission_callback.
register_rest_route( 'myplugin/v1', '/items', array(
'methods' => 'GET',
'callback' => 'my_get_items',
'permission_callback' => function () {
return current_user_can( 'read' );
},
) );
// Debugging a REST endpoint.
function my_get_items( WP_REST_Request $request ) {
$items = get_posts( array(
'post_type' => 'my_cpt',
'numberposts' => $request->get_param( 'per_page' ) ?? 10,
) );
if ( empty( $items ) ) {
// WRONG — do not return raw arrays or wp_send_json.
// return array();
// CORRECT — always use rest_ensure_response().
return rest_ensure_response( array() );
}
return rest_ensure_response( $items );
}
Debugging REST API from the command line:
# Test the endpoint directly.
wp eval '
$request = new WP_REST_Request( "GET", "/myplugin/v1/items" );
$response = rest_do_request( $request );
$data = $response->get_data();
echo print_r( $data, true );
'
# Check registered routes.
wp eval '
$server = rest_get_server();
$routes = $server->get_routes();
foreach ( $routes as $route => $handlers ) {
if ( strpos( $route, "myplugin" ) !== false ) {
echo $route . PHP_EOL;
}
}
'
# Test with nonce authentication (for cookie-based auth).
# In browser console:
# fetch('/wp-json/myplugin/v1/items', { headers: { 'X-WP-Nonce': wpApiSettings.nonce } })
2.6 Database Errors
Symptom: Data not saving, incorrect data returned, or $wpdb error messages.
// Inspect the last query and error.
global $wpdb;
$results = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}my_table WHERE id = 5" );
if ( $wpdb->last_error ) {
error_log( 'DB Error: ' . $wpdb->last_error );
error_log( 'DB Query: ' . $wpdb->last_query );
}
// WRONG — using table name without prefix. This breaks multisite and custom prefixes.
$wpdb->get_results( "SELECT * FROM my_table" );
// CORRECT — always use $wpdb->prefix.
$wpdb->get_results( "SELECT * FROM {$wpdb->prefix}my_table" );
// WRONG — string interpolation in queries (SQL injection risk).
$wpdb->query( "DELETE FROM {$wpdb->prefix}my_table WHERE id = {$_GET['id']}" );
// CORRECT — use $wpdb->prepare() for ALL user-supplied values.
$wpdb->query(
$wpdb->prepare(
"DELETE FROM {$wpdb->prefix}my_table WHERE id = %d",
absint( $_GET['id'] )
)
);
Debugging queries with SAVEQUERIES:
// Add to wp-config.php (REMOVE after debugging — performance hit).
define( 'SAVEQUERIES', true );
// Then in your code or a mu-plugin:
add_action( 'shutdown', function () {
global $wpdb;
error_log( 'Total queries: ' . count( $wpdb->queries ) );
// Find slow queries (over 0.05 seconds).
foreach ( $wpdb->queries as $query ) {
if ( $query[1] > 0.05 ) {
error_log( sprintf(
'Slow query (%.4fs): %s | Caller: %s',
$query[1],
$query[0],
$query[2]
) );
}
}
} );
2.7 Permalink Issues
Symptom: 404 errors on pages that exist. Custom post types or REST routes returning 404.
# Flush rewrite rules via WP-CLI.
wp rewrite flush
# List current rewrite rules.
wp rewrite list --format=table | head -30
# Check if .htaccess is writable.
ls -la .htaccess
# Regenerate .htaccess.
wp rewrite structure '/%postname%/'
Common causes:
- Stale rewrite rules (always flush after registering custom post types or taxonomies)
.htaccessnot writable by the web server- Conflicting rewrite rules from multiple plugins
register_post_type()called too late (must fire oninit)- Missing
'publicly_queryable' => trueon custom post type
2.8 Memory Exhaustion
Symptom: Fatal error: Allowed memory size of X bytes exhausted in the error log.
// Increase WordPress memory limit in wp-config.php.
define( 'WP_MEMORY_LIMIT', '256M' ); // Frontend.
define( 'WP_MAX_MEMORY_LIMIT', '512M' ); // Admin/backend.
# Check current PHP memory limit.
wp eval 'echo "PHP: " . ini_get("memory_limit") . " | WP: " . WP_MEMORY_LIMIT . PHP_EOL;'
# Find what is consuming memory — add to mu-plugin temporarily.
wp eval '
echo "Memory at load: " . round( memory_get_usage() / 1024 / 1024, 2 ) . "MB" . PHP_EOL;
echo "Peak memory: " . round( memory_get_peak_usage() / 1024 / 1024, 2 ) . "MB" . PHP_EOL;
'
Common causes:
- Loading all posts with no limit (
'numberposts' => -1on a site with 100k posts) - Infinite loop in a recursive function or a hook that triggers itself
- Large image manipulation without increasing memory limit
- Autoloading massive option values from
wp_options
2.9 Cron Issues
Symptom: Scheduled events never fire, fire too often, or are missing.
# List all scheduled cron events.
wp cron event list --format=table
# Check if a specific event is scheduled.
wp eval '
$next = wp_next_scheduled( "my_cron_event" );
echo $next ? "Next run: " . date( "Y-m-d H:i:s", $next ) : "NOT scheduled";
'
# Run all due cron events now.
wp cron event run --due-now
# Run a specific event.
wp cron event run my_cron_event
# Check if WP-Cron is disabled (external cron configured).
wp eval 'echo defined("DISABLE_WP_CRON") && DISABLE_WP_CRON ? "WP-Cron DISABLED" : "WP-Cron active";'
# Test that the cron URL is reachable.
wp eval '
$response = wp_remote_get( site_url( "/wp-cron.php" ) );
echo "Status: " . wp_remote_retrieve_response_code( $response ) . PHP_EOL;
'
Common causes:
DISABLE_WP_CRONset totruewithout an external cron job configured- Cron event registered with wrong recurrence or wrong arguments
- Long-running cron task hitting PHP
max_execution_time WP_CRON_LOCK_TIMEOUTtoo high, preventing concurrent runs- Caching plugin serving cached
wp-cron.phpresponses
2.10 WooCommerce Issues
Symptom: Cart issues, checkout failures, order status not updating, template overrides not loading.
# Check WooCommerce system status.
wp wc --info 2>/dev/null || wp eval 'echo WC()->version;'
# Check for template overrides in the active theme.
wp eval '
$overrides = WC()->api->get_endpoint_data( "/wc/v3/system_status" );
' 2>/dev/null
# Manually check for template overrides.
find wp-content/themes/*/woocommerce/ -name "*.php" 2>/dev/null
Session and cart fragment issues:
// Cart fragments AJAX returning stale data.
// Check if fragments are being filtered correctly.
add_filter( 'woocommerce_add_to_cart_fragments', function ( $fragments ) {
error_log( 'Cart fragments filter called. Fragment count: ' . count( $fragments ) );
error_log( 'Cart contents: ' . print_r( WC()->cart->get_cart_contents_count(), true ) );
return $fragments;
} );
// Session not persisting — check if session handler is working.
add_action( 'init', function () {
if ( function_exists( 'WC' ) && WC()->session ) {
error_log( 'WC Session ID: ' . WC()->session->get_customer_id() );
}
} );
Order status hook debugging:
// Log all order status transitions.
add_action( 'woocommerce_order_status_changed', function ( $order_id, $old_status, $new_status ) {
error_log( sprintf(
'Order #%d status changed: %s -> %s | Backtrace: %s',
$order_id,
$old_status,
$new_status,
wp_debug_backtrace_summary()
) );
}, 10, 3 );
Template override conflicts:
// Check if a template is being overridden and from where.
add_filter( 'wc_get_template', function ( $template, $template_name, $args, $template_path, $default_path ) {
if ( $template !== $default_path . $template_name ) {
error_log( "WC Template Override: {$template_name} -> {$template}" );
}
return $template;
}, 10, 5 );
Phase 3: Diagnostic Techniques
Advanced tools for when the standard log reading is not enough.
3.1 Query Debugging with SAVEQUERIES
// In wp-config.php (remove after debugging).
define( 'SAVEQUERIES', true );
// In your code or a mu-plugin — log queries from a specific caller.
add_action( 'shutdown', function () {
global $wpdb;
foreach ( $wpdb->queries as $q ) {
// $q[0] = SQL, $q[1] = elapsed time, $q[2] = caller stack.
if ( strpos( $q[2], 'my_plugin_function' ) !== false ) {
error_log( "Query by my_plugin_function ({$q[1]}s): {$q[0]}" );
}
}
} );
3.2 Query Monitor Integration
// Log custom debug data to the Query Monitor panel.
do_action( 'qm/debug', 'My debug message' );
do_action( 'qm/info', 'Informational message' );
do_action( 'qm/warning', 'Something looks wrong' );
do_action( 'qm/error', 'Something is definitely wrong' );
// Log variables.
do_action( 'qm/debug', $my_variable );
// Log with context.
do_action( 'qm/debug', array(
'post_id' => $post_id,
'meta' => get_post_meta( $post_id ),
) );
3.3 Xdebug Configuration
Add to php.ini or a dedicated xdebug.ini:
[xdebug]
zend_extension=xdebug
xdebug.mode=debug
xdebug.start_with_request=yes
xdebug.client_host=127.0.0.1
xdebug.client_port=9003
xdebug.log=/tmp/xdebug.log
xdebug.idekey=VSCODE
Verify Xdebug is loaded:
php -v | grep -i xdebug
wp eval 'echo phpinfo();' 2>/dev/null | grep -i xdebug
3.4 Quick CLI Checks
# Check if a function exists.
wp eval 'var_dump( function_exists( "my_custom_function" ) );'
# Check if a class exists.
wp eval 'var_dump( class_exists( "My_Custom_Class" ) );'
# Check a constant's value.
wp eval 'echo defined( "MY_CONSTANT" ) ? MY_CONSTANT : "NOT DEFINED";'
# Dump a specific option.
wp option get my_plugin_settings --format=json | python3 -m json.tool
# Check hook execution order.
wp eval '
echo "did_action(init): " . did_action( "init" ) . PHP_EOL;
echo "did_action(wp_loaded): " . did_action( "wp_loaded" ) . PHP_EOL;
echo "did_action(template_redirect): " . did_action( "template_redirect" ) . PHP_EOL;
'
# Check active theme.
wp theme list --status=active --format=table
# Check for PHP errors in a specific file.
php -l wp-content/plugins/my-plugin/includes/class-main.php
3.5 Cache Debugging
# Delete all transients.
wp transient delete --all
# Delete a specific transient.
wp transient delete my_plugin_cache_key
# Flush the object cache.
wp cache flush
# Check OPcache status.
wp eval '
if ( function_exists( "opcache_get_status" ) ) {
$status = opcache_get_status( false );
echo "OPcache enabled: " . ( $status["opcache_enabled"] ? "Yes" : "No" ) . PHP_EOL;
echo "Cached scripts: " . $status["opcache_statistics"]["num_cached_scripts"] . PHP_EOL;
echo "Memory used: " . round( $status["memory_usage"]["used_memory"] / 1024 / 1024, 2 ) . "MB" . PHP_EOL;
} else {
echo "OPcache not available." . PHP_EOL;
}
'
# Invalidate OPcache for a specific file after editing it.
wp eval 'opcache_invalidate( ABSPATH . "wp-content/plugins/my-plugin/my-plugin.php", true );'
# Reset entire OPcache (nuclear option).
wp eval 'opcache_reset(); echo "OPcache cleared.";'
3.6 PHP-FPM and Server-Level Debugging
# Check PHP-FPM status (if status page is enabled).
curl -s http://localhost/status?full
# Check PHP-FPM pool config for memory/process limits.
grep -E "pm\.|memory_limit|max_execution" /etc/php/8.2/fpm/pool.d/www.conf
# Check PHP version and loaded modules.
php -m | sort
wp eval 'echo "PHP " . phpversion() . PHP_EOL;'
# Check for PHP version compatibility issues.
wp eval '
if ( version_compare( phpversion(), "8.0", ">=" ) ) {
echo "PHP 8.0+ — watch for named arguments, union types, nullsafe operator changes." . PHP_EOL;
}
if ( version_compare( phpversion(), "8.1", ">=" ) ) {
echo "PHP 8.1+ — watch for enum usage, fibers, readonly properties, intersection types." . PHP_EOL;
}
'
Phase 4: Fix and Verify
Once the root cause is confirmed, fix it properly.
4.1 Write a Failing Test First
<?php
/**
* Test that the bug is fixed and stays fixed.
*/
class Test_Bug_Fix extends WP_UnitTestCase {
/**
* Regression test: saving post meta should not trigger a fatal error
* when the meta value is an empty array.
*
* @see https://github.com/your-org/your-plugin/issues/42
*/
public function test_save_meta_with_empty_array_does_not_fatal() {
$post_id = $this->factory->post->create();
// This should not throw a fatal error or warning.
update_post_meta( $post_id, '_my_plugin_settings', array() );
$result = get_post_meta( $post_id, '_my_plugin_settings', true );
$this->assertIsArray( $result );
$this->assertEmpty( $result );
}
/**
* Regression test: REST endpoint returns 200, not 500,
* when no items exist.
*/
public function test_rest_endpoint_returns_empty_array_not_error() {
wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );
$request = new WP_REST_Request( 'GET', '/myplugin/v1/items' );
$response = rest_do_request( $request );
$this->assertEquals( 200, $response->get_status() );
$this->assertIsArray( $response->get_data() );
}
}
4.2 Apply the Fix
- Fix at the source, not the symptom. If a function receives bad data, fix where the data is produced, not where it is consumed.
- If the fix is a workaround, document why the proper fix is not possible and create a follow-up issue.
4.3 Run the Test Suite
# Run your specific test.
phpunit --filter Test_Bug_Fix
# Run the full suite to check for regressions.
phpunit
# Run with verbose output if tests fail.
phpunit --verbose --debug
4.4 Run PHPCS
# Check the changed file against WordPress coding standards.
phpcs --standard=WordPress wp-content/plugins/my-plugin/includes/class-fixed.php
# Auto-fix what can be auto-fixed.
phpcbf --standard=WordPress wp-content/plugins/my-plugin/includes/class-fixed.php
4.5 Post-Fix Verification Checklist
- Run
phpunit— all tests pass, including the new regression test. - Run
phpcs— no coding standard violations introduced. - Test in the classic editor if the fix touches post editing.
- Test in the block editor (Gutenberg) if the fix touches post editing.
- Clear all caches:
wp transient delete --all
wp cache flush
wp eval 'function_exists("opcache_reset") && opcache_reset();'
- Test with
WP_DEBUGstill enabled — no new notices, warnings, or deprecations. - Test on staging with production-like data before deploying.
4.6 If 3+ Fixes Fail, Question the Architecture
If you have attempted three fixes and the bug persists or keeps returning, stop writing code. The problem is likely architectural:
- A hook is being misused as a data pipeline when it should be a direct function call.
- Tight coupling between plugins that should communicate via a defined API.
- State is being stored in globals when it should be in a proper data store.
- The plugin is fighting WordPress conventions instead of working with them.
Before attempt #4: Write a brief analysis of what you have tried, why each attempt failed, and what architectural assumption seems wrong. Discuss with the team before proceeding.
The 3-Strike Rule
| Strike | What happened | What to do |
|---|---|---|
| 1 | First fix attempt failed | Re-examine the evidence. Was the root cause correct? |
| 2 | Second fix attempt failed | Broaden the investigation. Check adjacent systems. |
| 3 | Third fix attempt failed | Stop. The architecture may be fundamentally flawed. |
After 3 failed attempts:
- Do not attempt a 4th fix.
- Write up what you tried and why each attempt failed.
- The underlying design likely needs to change, not just the code at the error site.
- Discuss the architectural issue before writing more code.
Quick Reference
| Symptom | Diagnostic Step | Common Cause | Fix Approach |
|---|---|---|---|
| White screen (WSOD) | tail -50 wp-content/debug.log | PHP fatal error in recently changed file | Fix the fatal, check php -l for syntax errors |
| 500 error | Check server error log + .htaccess | Corrupted .htaccess or memory limit | wp rewrite flush or increase WP_MEMORY_LIMIT |
| Plugin conflict | Deactivate all, activate one by one | Two plugins hooking the same filter | Adjust hook priority or namespace the filter |
| Hook not firing | has_filter() / did_action() check | Wrong priority or removed by another plugin | Fix priority, re-add hook after removal |
| REST API 401/403 | Check permission_callback | Missing or incorrect permission callback | Add proper permission_callback to route |
| REST API 500 | Check debug.log after REST request | Fatal in callback, missing rest_ensure_response() | Fix callback, wrap return in rest_ensure_response() |
| Database error | $wpdb->last_error / $wpdb->last_query | Missing table prefix, raw SQL without prepare() | Use $wpdb->prefix and $wpdb->prepare() |
| 404 on custom post type | wp rewrite flush | Stale rewrite rules | Flush rewrites, check publicly_queryable |
| Memory exhaustion | Check peak memory with memory_get_peak_usage() | Unbounded query (numberposts => -1) | Add limits, paginate, use WP_Query with no_found_rows |
| Cron not firing | wp cron event list | DISABLE_WP_CRON without external cron | Set up server cron or remove DISABLE_WP_CRON |
| WooCommerce cart empty | Check session handler + cart fragments | Session conflict or broken fragments AJAX | Debug session ID, check fragments filter |
| Slow page load | Query Monitor Queries panel | Duplicate or N+1 queries | Cache results, use update_meta_cache(), batch queries |
| Permalink 404s | wp rewrite list | Missing flush after CPT registration | Call flush_rewrite_rules() on activation hook only |