Перейти к содержанию

Отладка PHP в Bugsaur

Адаптер: php — собственный DBGp-адаптер Bugsaur.

PHP — единственный язык, который Bugsaur не отдаёт наружу. В комплекте идёт php-dbgp-adapter, собираемый из того же workspace; он заменяет привычный мост DAP↔DBGp. Во время отладки участвуют только bugsaur, php-dbgp-adapter и Xdebug — ни Node.js, ни npm, ни VS Code, ни vscode-php-debug.

PHP устроен иначе, чем остальные языки

Это то, что нужно понять прежде всего остального:

Адаптер ничего не запускает — он слушает

Для Rust, Go и Python программу запускает Bugsaur. Для PHP — нет. php-dbgp-adapter открывает DBGp-порт и ждёт, а PHP-процесс поднимает кто-то другой: PHP-FPM, Docker или запуск из CLI.

Отсюда два следствия, объясняющие большинство неожиданностей:

  • cwd и env не применяются. Нет процесса, которому Bugsaur мог бы задать рабочий каталог и окружение. Задавайте их там, где PHP действительно стартует: в docker-compose.yml, в конфиге пула FPM, в .env контейнера.
  • Из тела DAP-запроса читаются ровно три ключа: port, pathMappings, sessionMode.

Установка

Внешний адаптер ставить не нужно. make build собирает php-dbgp-adapter рядом с bugsaur; выведите каталог сборки в PATH, чтобы каталог адаптеров его нашёл:

make build
export PATH="$PWD/target/debug:$PATH"

Настроить Xdebug

В самой PHP-среде — в контейнере или в локальном php.ini:

xdebug.mode=debug
xdebug.start_with_request=yes
xdebug.client_host=host.docker.internal
xdebug.client_port=9003

xdebug.start_with_request=yes означает, что Xdebug пытается подключиться на каждый новый PHP-запрос. Если PHP работает на той же машине, а не в контейнере, вместо host.docker.internal пишется localhost.

Запрос, завершившийся до старта отладки, задним числом не отладить

Xdebug подключается в начале запроса. Если listener в этот момент не работал, запрос просто выполнился без отладки — повторите его после запуска сессии.

Режимы сессии

Ключ sessionMode в launch_arguments определяет жизненный цикл:

Режим Когда использовать Порядок действий
server PHP-FPM, встроенный сервер или web-контейнер уже работает docker compose up -d web → запустить сессию → отправить новый запрос
cli одноразовый CLI-скрипт, который стартует и завершается запустить сессию → docker compose run --rm php

server — рекомендуемый режим для обычной веб-разработки: контейнер поднимается один раз, работает как обычно без отладки, а сессия запускается только тогда, когда нужно разобрать конкретный ответ.

cli существует для случая, когда контейнер сам запускает PHP-команду и завершается. Там адаптер обязан слушать до старта контейнера, иначе одноразовый процесс успеет закончиться раньше, чем Xdebug подключится.

Локальный проект

version = 1
default = "shop"

[profiles.shop]
adapter = "php"
program = "."

[profiles.shop.launch_arguments]
sessionMode = "server"
port = 9003

PHP в Docker

Здесь существенны отображения путей, а не рабочий каталог:

[profiles.docker]
adapter = "php"
program = "."

[profiles.docker.launch_arguments]
sessionMode = "server"
port = 9003
pathMappings = { "/app" = "${root}" }   # путь внутри контейнера -> путь на хосте

pathMappings переводит между путями, которые сообщает Xdebug — они существуют внутри контейнера, — и путями на вашей машине. Без него отладчик остановится в файле, который нечем показать, а точки останова по путям хоста никогда не совпадут с путями контейнера.

Полный разбор вместе с настройкой compose: Рецепты → Отладка PHP в Docker.

PHPUnit под курсором

Чтобы отладить тест под курсором, добавьте команду, запускающую PHPUnit внутри контейнера:

[profiles.docker.launch_arguments]
sessionMode = "server"
port = 9003
pathMappings = { "/app" = "${root}" }
testCommand = ["docker", "compose", "exec", "-T", "-e", "XDEBUG_TRIGGER=1", "php", "php", "vendor/bin/phpunit"]

:DebugTest распознаёт методы test* и методы с атрибутом #[Test] в файлах .php. Для метода передаётся --filter Class::method, а если курсор стоит вне метода, запускается весь файл.

testCommand выполняется после того, как адаптер начал ждать Xdebug, поэтому к моменту обратного соединения listener уже готов. Переменная XDEBUG_TRIGGER=1 также передаётся внешнему процессу.

Два PHP-проекта одновременно

DBGp-порт занимает адаптер, а порт на машине один — по умолчанию 9003. Второй проект, стартующий одновременно, получит отказ. Дайте каждому проекту свой порт и пропишите тот же порт в его контейнере:

# a/.bugsaur/config.toml
[profiles.docker.launch_arguments]
port = 9003
pathMappings = { "/app" = "${root}" }

# b/.bugsaur/config.toml
[profiles.docker.launch_arguments]
port = 9004
pathMappings = { "/app" = "${root}" }
; xdebug.ini проекта B
xdebug.client_port=9004

Окна при этом разъедутся сами: они именуются по каталогу проекта.

Диагностика

Симптом Вероятная причина Что делать
У точек останова приглушённое кольцо Xdebug ещё не подключился до первого запроса это нормально — отправьте запрос
Сессия стартовала, но ничего не останавливается запрос ушёл до старта listener'а или выключен start_with_request повторите запрос; проверьте xdebug.mode=debug
Порт занят 9003 держит другой проект дайте этому проекту свой порт, см. выше
Останавливается, но исходник не показан pathMappings отсутствует или неверен Исходник не найден
Xdebug не достучится до хоста неверный client_host для вашей схемы из контейнера host.docker.internal, локально localhost
cwd / env в профиле ничего не делают так задумано — адаптер ничего не запускает задавайте их там, где стартует PHP