---
model_tier: high
name: php-debugging
description: "Use when debugging PHP with Xdebug — breakpoints, step-through, dual-container setup, IDE configuration, header-based routing — even when the user just says 'why does this blow up on request X'."
domain: engineering
workspaces:
  - engineering
packs:
  - php
trust:
  level: professional
install:
  default: false
  removable: true
---

# php-debugging

## When to use

Use this skill when:
- Setting up or troubleshooting Xdebug
- Debugging PHP code with breakpoints
- Investigating performance issues
- Running code coverage
- Helping users configure their IDE for debugging

This skill extends `php-coder` and `php`.

## Procedure: Debug with Xdebug

1. **Detect the project's debug setup** — check `docker-compose.yml` / `compose.yaml` for Xdebug containers.
2. **Check Dockerfile** — look for a `dev-xdebug` build stage or Xdebug installation.
3. **Check NGINX config** — look for header-based routing to a debug container.
4. **Read project docs** — check `Docs/XDEBUG_SETUP.md` or `docs/` for setup instructions.
5. **Check `.env`** — look for `DOCKER_XDEBUG_MODE` and `DOCKER_XDEBUG_PORT`.

## Dual-container architecture

Many projects use two PHP containers for optimal performance:

| Container | Purpose | When used |
|---|---|---|
| `*-php` | Fast PHP-FPM, no Xdebug overhead | All normal requests |
| `*-php-xdebug` | PHP-FPM with Xdebug enabled | Only debug requests |

NGINX routes requests based on HTTP headers:
- **No debug header** → fast container (no Xdebug)
- **Debug header present** → Xdebug container

### Debug headers

| Header | Value | Recommended for |
|---|---|---|
| `X-Xdebug-Enable` | `1` or `true` | Manual tests, Postman |
| `X-Debug-Session` | `PHPSTORM` | IDE integration |
| `XDEBUG-SESSION` | any value | Standard Xdebug header |

**Important:** Use hyphens, not underscores. `XDEBUG_SESSION` does NOT work — use `XDEBUG-SESSION`.

### Verify routing

Check the `X-PHP-Backend` response header to confirm which container handled the request:

```bash
# Normal request
curl -I http://localhost:8002/  # → X-PHP-Backend: *-php:9000

# Debug request
curl -I -H "X-Xdebug-Enable: 1" http://localhost:8002/  # → X-PHP-Backend: *-php-xdebug:9000
```

## Xdebug configuration

### Environment variables

```bash
DOCKER_XDEBUG_MODE=develop,debug,coverage   # Xdebug modes
DOCKER_XDEBUG_PORT=9003                      # IDE listens on this port
```

### Xdebug modes

| Mode | Purpose |
|---|---|
| `debug` | Step debugging with breakpoints |
| `develop` | Enhanced error messages, var_dump improvements |
| `coverage` | Code coverage for tests |
| `profile` | Performance profiling (generates cachegrind files) |
| `trace` | Function call tracing |

### PHP-FPM on-demand (Xdebug container)

The Xdebug container typically uses `pm = ondemand` to save resources:
- Workers start only when a debug request arrives
- Idle workers terminate after 60s
- Reduces memory usage by ~60-70% when not debugging
- Unlimited request timeout for breakpoint sessions

## IDE setup

### PhpStorm

1. **Debug port**: Settings → PHP → Debug → Port: `9003`
2. **Enable listening**: Click "Start Listening for PHP Debug Connections" (phone icon)
3. **Server config**: Settings → PHP → Servers:
   - Host: `localhost`, Port: `80` (internal container port, not host port)
   - Enable path mappings: local project root → `/var/www/html`
4. **Browser extension**: Install "Xdebug helper", set IDE key to `PHPSTORM`

### VS Code

```json
{
  "name": "Listen for Xdebug",
  "type": "php",
  "request": "launch",
  "port": 9003,
  "pathMappings": {
    "/var/www/html": "${workspaceFolder}"
  }
}
```

## Debugging workflow

1. **Start listening** in IDE (PhpStorm: green phone icon)
2. **Set breakpoints** in code
3. **Send request with debug header** (browser extension, Postman, or curl)
4. **IDE breaks** at breakpoint → inspect variables, step through code
5. **Check `X-PHP-Backend` header** if debugging doesn't trigger

## CLI debugging (Artisan commands, tests)

For debugging Artisan commands or tests, enter the Xdebug container:

```bash
make console-xdebug    # Enter Xdebug container
php artisan your:command   # Xdebug connects to IDE automatically
```

### Verifying CLI fixes

After a fix, verify the command without re-attaching the debugger:

```bash
# Run the command, capture exit code + command output
php artisan your:command; echo "exit code: $?"

# Pest CLI assertion — expectsOutput / artisan test
vendor/bin/pest --filter='ProcessInvoicesCommand'
```

Assert on the **exit code** and **command output**; never trust "looks fine" from breakpoint inspection alone.

## Troubleshooting

| Problem | Solution |
|---|---|
| Breakpoints not hit | Check IDE is listening, verify path mappings, check `X-PHP-Backend` header |
| IDE not connecting | `make console-xdebug` then `nc -zv host.docker.internal 9003` — should show "open" |
| Wrong container used | Check response header `X-PHP-Backend`, verify debug header format (hyphens!) |
| Slow normal requests | Verify normal requests go to fast container (no `X-PHP-Backend: *-xdebug*`) |
| Xdebug logs | `make console-xdebug` then `tail -f /tmp/xdebug.log` |
| Container not running | `docker compose ps` — both PHP containers should be "Up" |
| Path mapping wrong | PhpStorm: Settings → PHP → Servers → verify local ↔ `/var/www/html` |

## Container management

```bash
make console              # Fast PHP container (no Xdebug)
make console-xdebug       # Xdebug container
make rebuild-php-xdebug   # Rebuild Xdebug container only
make rebuild-php-all      # Rebuild both PHP containers
```

### Filter noisy debug output

`tail -f /tmp/xdebug.log` and `docker logs` produce far too much
data to read line-by-line. Filter with `rg`/`grep` for the relevant
event:

```bash
# Only connection / breakpoint events
rg --color=never 'Connect|Step|breakpoint' /tmp/xdebug.log

# Only this request's frames in the laravel log
docker compose logs php-xdebug | rg --color=never "$REQUEST_ID"
```

## What NOT to do

- Do not leave Xdebug enabled in production containers.
- Do not use underscores in debug headers (`XDEBUG_SESSION` fails — use `XDEBUG-SESSION`).
- Do not set PhpStorm server port to the host port (e.g. 8002) — use the internal port (80).
- Do not run performance benchmarks against the Xdebug container.
- Do not forget to check path mappings when breakpoints are silently skipped.


## Output format

1. Xdebug configuration or debugging session setup
2. Root cause identified with evidence from debugger output

## Gotcha

- Xdebug runs in a separate container — don't confuse the fast container (port 80) with the debug container (port 8080).
- The model tends to suggest `dd()` or `var_dump()` — they're forbidden by PHPStan config. Use Xdebug breakpoints.
- Step-debugging over HTTP requires the `XDEBUG_SESSION` cookie/header — without it, breakpoints don't trigger.

## Do NOT

- Do NOT leave breakpoints or debug code in committed files.
- Do NOT use var_dump() or dd() — use Xdebug breakpoints.
- Do NOT debug in the fast container — switch to the Xdebug container.

## Auto-trigger keywords

- Xdebug
- PHP debugging
- breakpoint
- step debugging
