# Wp Debugging

> 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.

- Skill: `iwritec0de/wp-debugging` (Agent Skill)
- Install (CLI): `npx skillmds@latest add iwritec0de/wp-debugging`
- Raw SKILL.md: https://api.skillmd.com/api/skills/iwritec0de/wp-debugging/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- License: MIT
- Author: iwritec0de (https://skillmd.com/u/iwritec0de)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/iwritec0de/wp-debugging

---


# 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:

```php
// 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:

```bash
wp eval 'echo WP_DEBUG ? "WP_DEBUG is ON" : "WP_DEBUG is OFF";'
```

### 1.2 Read the Debug Log

```bash
# 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:

```bash
# 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.

```php
// 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.

```bash
# 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.

```bash
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

```bash
# 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:**

```bash
# 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:**

```bash
# 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:**

```bash
# 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.

```php
// 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:**

```php
// 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.

```php
// 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:**

```bash
# 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.

```php
// 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:**

```php
// 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.

```bash
# 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.

```php
// Increase WordPress memory limit in wp-config.php.
define( 'WP_MEMORY_LIMIT', '256M' );       // Frontend.
define( 'WP_MAX_MEMORY_LIMIT', '512M' );   // Admin/backend.
```

```bash
# 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.

```bash
# 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.

```bash
# 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:**

```php
// 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:**

```php
// 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:**

```php
// 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

```php
// 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

```php
// 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`:

```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:

```bash
php -v | grep -i xdebug
wp eval 'echo phpinfo();' 2>/dev/null | grep -i xdebug
```

### 3.4 Quick CLI Checks

```bash
# 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

```bash
# 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

```bash
# 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
<?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

```bash
# 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

```bash
# 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:

```bash
wp transient delete --all
wp cache flush
wp eval 'function_exists("opcache_reset") && opcache_reset();'
```

6. Test with `WP_DEBUG` still enabled — no new notices, warnings, or deprecations.
7. 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 |

