feat(debugger): add PHP Remote Debug (Xdebug) run configuration (#1635)

A debug-only PHP Remote Debug (Xdebug) configuration: php-debug listens on the port (9003 by default) with optional path mappings, and the form shows how to start the remote side. php-debug is also offered as a debug adapter in the configuration form.

Co-authored-by: Inf1mum <8135225+Inf1mum@users.noreply.github.com>
This commit is contained in:
Vadim Lazuk
2026-11-06 06:28:02 +08:00
committed by GitHub
co-authored by Inf1mum
parent 3ec30fff3f
commit 83d2f67b4f
6 changed files with 95 additions and 13 deletions
+24 -1
View File
@@ -30,7 +30,9 @@ const {
goRemoteType,
pythonRemoteType,
nodeRemoteType,
phpRemoteType,
jdwpAgentArgs,
xdebugCommand,
remoteAttachConfig,
remoteJvmTarget,
} = remote;
@@ -67,7 +69,9 @@ describe("remote debug configuration types", () => {
port: "Port must be a number from 1 to 65535",
});
expect(missingParams(configOf(type, { port: "abc" }), type).port).toBeDefined();
expect(missingParams(configOf(type, { host: "" }), type)).toEqual({ host: "Host is required" });
// PHP Remote Debug has no host: php-debug listens itself, Xdebug connects to it.
if (type.fields.some((f) => f.key === "host"))
expect(missingParams(configOf(type, { host: "" }), type)).toEqual({ host: "Host is required" });
});
it("Remote JVM Debug defaults to attaching to localhost:5005", () => {
@@ -157,6 +161,25 @@ describe("remote attach requests", () => {
});
});
it("PHP Remote Debug defaults to port 9003 and shows the xdebug command line", () => {
expect(configOf(phpRemoteType).params).toEqual({ port: "9003" });
const field = phpRemoteType.fields.find((f) => f.key === "hint")!;
expect(field.control).toBe("output");
expect(field.value!({ port: "9004" })).toContain("-dxdebug.client_port=9004");
expect(xdebugCommand({})).toBe(
"php -dxdebug.mode=debug -dxdebug.start_with_request=yes -dxdebug.client_port=9003 script.php",
);
});
it("PHP Remote Debug launches php-debug without a program and maps the roots", () => {
const l = remoteAttachConfig(configOf(phpRemoteType, { localRoot: ".", remoteRoot: "/var/www/html" }), "/p")!;
expect(l).toMatchObject({ adapter: "php-debug", request: "launch", cwd: "/p" });
expect(l.program).toBeNull();
expect(JSON.parse(l.extraJson!)).toEqual({ port: 9003, pathMappings: { "/var/www/html": "/p" } });
const plain = JSON.parse(remoteAttachConfig(configOf(phpRemoteType), "/p")!.extraJson!);
expect(plain).toEqual({ port: 9003 });
});
it("start-the-remote-side hints follow the port", () => {
expect(remote.delveServerCommand({ port: "4000" })).toBe(
"dlv debug --headless --listen=:4000 --api-version=2 --accept-multiclient",
+60 -9
View File
@@ -2,6 +2,7 @@
* Remote debug run configurations: the debugger attaches to a process started
* elsewhere (another machine, a container) over host:port, like IDEA's Remote JVM
* Debug, GoLand's Go Remote, PyCharm's remote debugpy and IDEA's Attach to Node.js.
* PHP Remote Debug is the exception: its php-debug listens and Xdebug connects to it.
* Only Debug applies to them (`debugOnly`); they launch no process.
*/
import type { LaunchConfig, RunConfig } from "@/platform/ipc";
@@ -13,6 +14,7 @@ export const GO_REMOTE = "go-remote";
export const PYTHON_REMOTE = "python-remote";
export const NODE_REMOTE = "node-remote";
export const GODOT_ATTACH = "godot-attach";
export const PHP_REMOTE = "php-remote";
type Params = Readonly<Record<string, string>>;
@@ -221,6 +223,38 @@ export const nodeRemoteType: RunConfigTypeContribution = {
commandLine: () => null,
};
// ---------------------------------------------------------------------------
// PHP Remote Debug (Xdebug): php-debug listens, the remote PHP connects to it
/** How to start the remote side, so Xdebug connects back to the listening php-debug. */
export function xdebugCommand(p: Params): string {
return `php -dxdebug.mode=debug -dxdebug.start_with_request=yes -dxdebug.client_port=${portOf(p, 9003)} script.php`;
}
export const phpRemoteType: RunConfigTypeContribution = {
id: PHP_REMOTE,
label: "PHP Remote Debug (Xdebug)",
icon: "file-php",
description: "Listen for Xdebug connections: debug PHP requests served elsewhere (php-fpm, a container)",
defaults: { port: "9003" },
debugOnly: true,
debugAdapter: "php-debug",
fields: [
portField,
{
key: "hint",
label: "Start the remote side with",
control: "output",
mono: true,
value: xdebugCommand,
hint: "For web requests: the XDEBUG_SESSION cookie or query parameter; point xdebug.client_host at this machine when PHP runs in a container",
},
localRootField,
remoteRootField("e.g. /var/www/html"),
],
commandLine: () => null,
};
// ---------------------------------------------------------------------------
// Attach to Godot (the editor's debug adapter, Editor Settings › Network › Debug Adapter)
@@ -250,13 +284,14 @@ export const REMOTE_TYPES: readonly RunConfigTypeContribution[] = [
goRemoteType,
pythonRemoteType,
nodeRemoteType,
phpRemoteType,
godotAttachType,
];
export const isRemoteConfig = (c: Pick<RunConfig, "kind">) => REMOTE_TYPES.some((t) => t.id === c.kind);
// ---------------------------------------------------------------------------
// Attach requests
// Attach / listen requests
function isAbsolute(p: string) {
return p.startsWith("/") || /^[A-Za-z]:[\\/]/.test(p);
@@ -281,11 +316,17 @@ export function serverAddress(host: string, port: number): string {
return `${host.includes(":") && !host.startsWith("[") ? `[${host}]` : host}:${port}`;
}
function attachBase(rc: RunConfig, adapter: string, root: string, extra: Record<string, unknown>): LaunchConfig {
function requestBase(
rc: RunConfig,
adapter: string,
root: string,
request: "attach" | "launch",
extra: Record<string, unknown>,
): LaunchConfig {
return {
name: rc.name,
adapter,
request: "attach",
request,
program: null,
args: [],
cwd: root,
@@ -295,9 +336,11 @@ function attachBase(rc: RunConfig, adapter: string, root: string, extra: Record<
}
/**
* The attach request of a Go / Python / Node.js remote configuration. Delve's headless
* The request of a Go / Python / Node.js / PHP remote configuration. Delve's headless
* server and `debugpy --listen` speak DAP themselves (`debugServer` connects there);
* js-debug runs here and connects to the inspector.
* js-debug runs here and connects to the inspector. PHP Remote Debug is the one
* `launch`: php-debug is the side that listens, and each Xdebug connection becomes
* a thread, so nothing attaches and no program is named.
*/
export function remoteAttachConfig(rc: RunConfig, root: string): LaunchConfig | null {
const p = rc.params ?? {};
@@ -306,27 +349,35 @@ export function remoteAttachConfig(rc: RunConfig, root: string): LaunchConfig |
const skip = workflowSettings().debugger.skipLibraryCode;
switch (rc.kind) {
case GO_REMOTE:
return attachBase(rc, "delve", root, {
return requestBase(rc, "delve", root, "attach", {
debugServer: serverAddress(host, portOf(p, 2345)),
mode: "remote",
...(map ? { substitutePath: [{ from: map.local, to: map.remote }] } : {}),
});
case PYTHON_REMOTE:
return attachBase(rc, "debugpy", root, {
return requestBase(rc, "debugpy", root, "attach", {
debugServer: serverAddress(host, portOf(p, 5678)),
justMyCode: skip,
...(map ? { pathMappings: [{ localRoot: map.local, remoteRoot: map.remote }] } : {}),
});
case GODOT_ATTACH:
return attachBase(rc, "godot", root, { debugServer: serverAddress(host, portOf(p, 6006)), project: root });
return requestBase(rc, "godot", root, "attach", {
debugServer: serverAddress(host, portOf(p, 6006)),
project: root,
});
case NODE_REMOTE:
return attachBase(rc, "js-debug", root, {
return requestBase(rc, "js-debug", root, "attach", {
type: "pwa-node",
address: host,
port: portOf(p, 9229),
...(map ? { localRoot: map.local, remoteRoot: map.remote } : {}),
skipFiles: skip ? ["<node_internals>/**", "**/node_modules/**"] : [],
});
case PHP_REMOTE:
return requestBase(rc, "php-debug", root, "launch", {
port: portOf(p, 9003),
...(map ? { pathMappings: { [map.remote]: map.local } } : {}),
});
default:
return null;
}
+3 -2
View File
@@ -49,8 +49,8 @@ import { TomcatSettings } from "./TomcatSettings";
import styles from "./Run.module.css";
/**
* Debug adapters with a real launch template (`debugger/launch.ts`, `debugger/java.ts`),
* verified against small programs. Others (dart, php-debug, …) only got
* Debug adapters with a real launch template (`debugger/launch.ts`, `debugger/java.ts`,
* `debugger/remote.ts`), verified against small programs. Others (dart, …) only got
* generic launch arguments and are left out until they have one; a configuration that
* already names one keeps it.
*/
@@ -63,6 +63,7 @@ const ADAPTERS: ReadonlyArray<{ id: string; label: string }> = [
{ id: "delve", label: "Delve (Go)" },
{ id: "codelldb", label: "CodeLLDB (Rust, C, C++)" },
{ id: "lldb-dap", label: "lldb-dap (C, C++, Swift)" },
{ id: "php-debug", label: "php-debug (PHP)" },
];
/** Options "Modify options" adds to the form: value fields (shown once added) and flags (chips). */
+4 -1
View File
@@ -30,5 +30,8 @@
"{added} line added": "{added} line added",
"{added} lines added": "{added} lines added",
"{removed} line deleted": "{removed} line deleted",
"{removed} lines deleted": "{removed} lines deleted"
"{removed} lines deleted": "{removed} lines deleted",
"PHP Remote Debug (Xdebug)": "PHP Remote Debug (Xdebug)",
"Listen for Xdebug connections: debug PHP requests served elsewhere (php-fpm, a container)": "Listen for Xdebug connections: debug PHP requests served elsewhere (php-fpm, a container)",
"For web requests: the XDEBUG_SESSION cookie or query parameter; point xdebug.client_host at this machine when PHP runs in a container": "For web requests: the XDEBUG_SESSION cookie or query parameter; point xdebug.client_host at this machine when PHP runs in a container"
}
+3
View File
@@ -3653,6 +3653,9 @@
"pip install debugpy on the remote machine": "在远程机器上执行 pip install debugpy",
"Attach to Node.js": "附加到 Node.js",
"Attach to a Node.js process started with --inspect here or on another machine": "附加到以 --inspect 启动的 Node.js 进程(本机或其他机器)",
"PHP Remote Debug (Xdebug)": "PHP 远程调试 (Xdebug)",
"Listen for Xdebug connections: debug PHP requests served elsewhere (php-fpm, a container)": "监听 Xdebug 连接:调试在其他地方(php-fpm、容器中)提供服务的 PHP 请求",
"For web requests: the XDEBUG_SESSION cookie or query parameter; point xdebug.client_host at this machine when PHP runs in a container": "对于 Web 请求:设置 XDEBUG_SESSION cookie 或查询参数;当 PHP 运行在容器中时,将 xdebug.client_host 指向本机",
"Copied": "已复制",
"'{name}' can only be debugged": "'{name}' 只能调试",
"It attaches the debugger to a process started elsewhere.": "它将调试器附加到在别处启动的进程。",
+1
View File
@@ -86,6 +86,7 @@ const BUILT_AT_RUNTIME = new Set<string>([]);
const UNTRANSLATED = new Set<string>([
"dotnet test",
"netcoredbg (.NET)",
"php-debug (PHP)",
// Product names
"Cargo",
"CMake",