agentsclimarketplace

Wp debugging

Skill iwritec0de/wp-dev/skills/wp-debugging

WordPress development plugin for Claude Code

Install
npx -y skills add iwritec0de/wp-dev --skill wp-debugging

Assembled 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_DEBUG first. 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 / include of 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 with wp 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_args parameter (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)
  • .htaccess not writable by the web server
  • Conflicting rewrite rules from multiple plugins
  • register_post_type() called too late (must fire on init)
  • Missing 'publicly_queryable' => true on 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' => -1 on 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_CRON set to true without 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_TIMEOUT too high, preventing concurrent runs
  • Caching plugin serving cached wp-cron.php responses

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

  1. Run phpunit — all tests pass, including the new regression test.
  2. Run phpcs — no coding standard violations introduced.
  3. Test in the classic editor if the fix touches post editing.
  4. Test in the block editor (Gutenberg) if the fix touches post editing.
  5. Clear all caches:
wp transient delete --all
wp cache flush
wp eval 'function_exists("opcache_reset") && opcache_reset();'
  1. Test with WP_DEBUG still enabled — no new notices, warnings, or deprecations.
  2. 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

StrikeWhat happenedWhat to do
1First fix attempt failedRe-examine the evidence. Was the root cause correct?
2Second fix attempt failedBroaden the investigation. Check adjacent systems.
3Third fix attempt failedStop. 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

SymptomDiagnostic StepCommon CauseFix Approach
White screen (WSOD)tail -50 wp-content/debug.logPHP fatal error in recently changed fileFix the fatal, check php -l for syntax errors
500 errorCheck server error log + .htaccessCorrupted .htaccess or memory limitwp rewrite flush or increase WP_MEMORY_LIMIT
Plugin conflictDeactivate all, activate one by oneTwo plugins hooking the same filterAdjust hook priority or namespace the filter
Hook not firinghas_filter() / did_action() checkWrong priority or removed by another pluginFix priority, re-add hook after removal
REST API 401/403Check permission_callbackMissing or incorrect permission callbackAdd proper permission_callback to route
REST API 500Check debug.log after REST requestFatal in callback, missing rest_ensure_response()Fix callback, wrap return in rest_ensure_response()
Database error$wpdb->last_error / $wpdb->last_queryMissing table prefix, raw SQL without prepare()Use $wpdb->prefix and $wpdb->prepare()
404 on custom post typewp rewrite flushStale rewrite rulesFlush rewrites, check publicly_queryable
Memory exhaustionCheck peak memory with memory_get_peak_usage()Unbounded query (numberposts => -1)Add limits, paginate, use WP_Query with no_found_rows
Cron not firingwp cron event listDISABLE_WP_CRON without external cronSet up server cron or remove DISABLE_WP_CRON
WooCommerce cart emptyCheck session handler + cart fragmentsSession conflict or broken fragments AJAXDebug session ID, check fragments filter
Slow page loadQuery Monitor Queries panelDuplicate or N+1 queriesCache results, use update_meta_cache(), batch queries
Permalink 404swp rewrite listMissing flush after CPT registrationCall flush_rewrite_rules() on activation hook only

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.