run-worker-tests.sh is the automated check; the rest of this document is the
manual procedure for working on the SAPI hooks by hand with an IDE.
tests/frankenphp/run-worker-tests.shBuilds the image, runs a worker with dbgp_listener.py standing in for an
IDE, and asserts that breakpoints still fire after the worker has served
requests with no debugger attached — without that, a single un-debugged
request stops every later request from breaking. It also checks that the
reported stack includes the calling function, and repeats the trigger on a
fresh worker as a control.
Overrides: IMAGE, HTTP_PORT (default 8081), DBGP_PORT (default 9004,
because an IDE usually holds 9003), POISON_REQUESTS, TRIGGER_REQUESTS,
SKIP_BUILD=1 to reuse an image you already built.
The script exits non-zero with the listener transcript on failure. It is run
by .github/workflows/frankenphp.yml on every push and pull request, and it
was verified to fail (0/5 breakpoints) against the code from before the worker
-mode fix, so a regression is caught rather than silently passing.
dbgp_listener.py can also be pointed at a different breakpoint for ad-hoc
work — DBGP_BREAKPOINT="-t call -m workload" exercises the observer's begin
handler instead of the statement handler that serves line breakpoints.
Smoke-tests the SAPI activate/deactivate hooks added in src/debugger/frankenphp.c.
From the repo root (the build context must include the whole tree):
docker build -f tests/frankenphp/Dockerfile -t php-debugger-frankenphp .Start your IDE listening on port 9003 for incoming DBGp connections.
- PhpStorm: Run → Start Listening for PHP Debug Connections (the little phone icon in the toolbar should turn green).
- VS Code: add a
launch.jsonwith"type": "php","request": "launch","port": 9003,"pathMappings": { "/app": "${workspaceFolder}/tests/frankenphp/app" }, then F5.
Then start the container. Bind-mount the app dir so PhpStorm can match incoming breakpoint paths to your local files:
# macOS / Windows: host.docker.internal works out of the box.
docker run --rm -p 8080:80 \
-v "$PWD/tests/frankenphp/app:/app" \
php-debugger-frankenphp
# Linux: add the host-gateway alias.
docker run --rm -p 8080:80 \
--add-host=host.docker.internal:host-gateway \
-v "$PWD/tests/frankenphp/app:/app" \
php-debugger-frankenphp
# Override host/port from the CLI:
docker run --rm -p 8080:80 \
-e XDEBUG_CLIENT_HOST=192.168.1.42 \
-e XDEBUG_CLIENT_PORT=9003 \
-v "$PWD/tests/frankenphp/app:/app" \
php-debugger-frankenphpThe XDEBUG_CLIENT_HOST / XDEBUG_CLIENT_PORT env vars are applied at
container startup by entrypoint.sh (PHP INI has no native env-var
fallback syntax).
Without a path mapping, PhpStorm receives breakpoint_set for /app/index.php
but doesn't know it corresponds to tests/frankenphp/app/index.php in your
project. The IDE will warn:
It may be caused by path mappings misconfiguration or not synchronized local and remote projects.
Configure it once:
- Settings → PHP → Servers → +
- Name:
localhost(any name; it just needs to exist) - Host:
localhost· Port:8080· Debugger: Xdebug - ✅ Use path mappings
- In the file tree, find
tests/frankenphp/appand set the Absolute path on the server to/app. - Apply / OK.
To prove the DBGp connection itself is working before fighting path mappings, toggle Run → Break at first line in PHP scripts and hit:
curl -b 'XDEBUG_SESSION=PHPSTORM' http://localhost:8080/PhpStorm should pause on the opening <?php regardless of any path config.
If even that doesn't fire, the connection isn't being made — see the
Troubleshooting section.
The script already covers the basics: a request without a trigger must not pause, a request with one must, and repeated debug requests against the same worker must all pause. What is left needs either a real IDE or a clean shutdown, so it stays a hand check.
Open tests/frankenphp/app/index.php in your IDE, then:
-
Set a breakpoint on line 4 (
$pid = getmypid();). -
curl -b 'XDEBUG_SESSION=PHPSTORM' http://localhost:8080/→ pauses on line 4. Resume (F9). -
Move the breakpoint to line 5 (
$now = ...) while the worker is idle. The IDE sendsbreakpoint_remove+breakpoint_setto the socket of the (now idle) worker thread — those commands sit queued. -
curl -b 'XDEBUG_SESSION=PHPSTORM' http://localhost:8080/.The next
sapi_module.activatecallsxdebug_dbgp_poll_pending(), which drains the queued commands and registers the new breakpoint. PhpStorm pauses on line 5.Without the poll, the IDE's queued
breakpoint_setis silently ignored and the new breakpoint is never honored.
docker exec $(docker ps -qf ancestor=php-debugger-frankenphp) \
tail -f /tmp/xdebug.logPer debug request expect to see:
[Step Debug] INFO: Connecting to configured address/port: host.docker.internal:9003.
[Step Debug] INFO: Connected to debugging client: ...
... (DBGp protocol messages) ...
Log closed at ...
A clean Log closed between requests confirms the per-request
deactivate hook tore the session down before the next activate set up a
fresh one.
docker stop <container> triggers PHP MSHUTDOWN, which calls
xdebug_frankenphp_mshutdown() to restore the original
sapi_module.activate / deactivate pointers. Clean exit (no segfault
in the container logs) means the restore worked.
| Symptom | Cause | Fix |
|---|---|---|
curl returns nothing / 308 redirect to https:// |
FrankenPHP using its default Caddyfile, not ours | Confirm tests/frankenphp/Caddyfile was copied to /etc/frankenphp/Caddyfile (not /etc/caddy/Caddyfile) |
Failed loading Zend extension 'php_debugger.so' |
.so not in versioned ext dir |
Confirm COPY --from=build /usr/local/lib/php/extensions/ ... (whole dir, not flattened) |
make: *** No rule to make target '/Users/.../xdebug.c' during build |
Host build artifacts (Makefile, *.dep) leaked into context |
Repo-root .dockerignore should exclude **/*.dep, **/*.lo, Makefile, etc. — check it exists |
| IDE warns about path mappings | Local file path ≠ container file path | Configure Settings → PHP → Servers as described above; bind-mount -v "$PWD/tests/frankenphp/app:/app" |
Connecting to ... :0 in xdebug log |
Tried to use ${VAR:default} in PHP INI, which doesn't expand |
Already fixed via entrypoint.sh; pass real XDEBUG_CLIENT_HOST / _PORT env vars at docker run |
Creating socket ... error in xdebug log |
IDE not reachable from container | Linux: add --add-host=host.docker.internal:host-gateway. Or set XDEBUG_CLIENT_HOST to the host IP visible from the container. Verify the IDE is actually listening on the chosen port. |