Содержание

Вместо введения
Что, опять?
Как это работает?
Создаём проект в PHPStorm
Создаём контейнер Docker
Добавляем расширение Xdebug
Настраиваем Xdebug
Что там в логах Xdebug?
Настраиваем PhpStorm
  Устанавливаем CLI Interpreter
  Устанавливаем сервер, на котором происходит отладка
Проверяем, как работает отладка
Бонусные материалы для самых терпеливых
  Run/Debug конфигурация
  Использование docker-compose
А на этом всё

Вместо введения

Одним из ключевых аспектов успешной разработки является эффективная отладка кода. Статья посвящена настройке и использованию PhpStorm, Xdebug и Docker для отладки PHP-скриптов в Docker-контейнере. Статья предлагает актуальную информацию (на момент написания) и оформлена в виде подробнейшего пошагового туториала. Информация действительна для ОС Windows. В других ОС возможны варианты.

Цель статьи — не просто создать пошаговой конспект настроек, а объяснить, как всё это взаимодействует. Это поможет избежать затруднений при изменении интерфейса IDE в будущем. Возможно, статья выглядит слишком педантичной. Но гуру всегда смогут прочесть её по диагонали, зато новички в этой теме найдут для себя много полезного.

Что, опять?

Ничего не поделаешь. Я сам удивляюсь, как быстро меняются инструменты и технологии, и то, что было актуально полтора года назад, сегодня выглядит устаревшим. Интерфейс IDE PhpStorm преображается, ключи настоек Xdebug меняются, рабочие рецепты, бездумно скопированные из легаси-туториалов обрастают кучей ненужных зависимостей и противоречий. На днях мне понадобилось отладить php-сценарий в докер-контейнере. Я пробежался вроде бы по не очень древним статьям, но не взлетело. Пришлось садиться и вдумчиво разбираться. Начнём?

Как это работает?

Xdebug подключается модулем к интерпретатору PHP. Когда интерпретатор обрабатывает код, он обращается к модулю Xdebug. А Xdebug подключается к хосту, указанному в настойках, на порт, указанный в настройках. В нашем случае на этом хосту должен быть запущен PhpStorm, который слушает этот порт. Точнее все порты, указанные в настройках. Проверяем: Ctrl+Alt+S — откроется окно настроек. Слева сверху находим PHP>Debug

Скриншот окна настроек
Настройки секции Debug
Настройки секции Debug

Видим порты 9003 и 9000. PhpStorm начинает слушать эти порты при выполнении команды Run>Start Listening for PHP Debug Connection. А также при нажатии кнопок с изображением трубки в старом интерфейсе, или с изображением жука в новом интерфейсе. Лично мне приглянулся новый интерфейс, поэтому далее скриншоты будут из него.

Скриншот жука и телефона

Проверим свободны ли эти порты. Запускаем Windows Terminal. У кого нет, можно взять здесь

Подаём команду: netstat -ano | findstr :900*. В выводе пусто. Теперь включим прослушивание отладочных соединений. Я нажму на жука. Повторяем команду: netstat -ano | findstr :900*

Результат выполнения команды
Результат выполнения команды
Результат выполнения команды

Из вывода команды видно, что система слушает (LISTENING) порты 9000 и 9003 и готова принимать подключения с любого IP-адреса и любого порта. А pid процесса-слушателя 27540. Давайте откроем диспетчер задач, вкладку «Подробности» и найдём процесс с pid 27540.

Диспетчер задач
Диспетчер задач
Диспетчер задач

Видим, что порты слушает PhpStorm.

Таким образом, PHP исполняя скрипт запускает Xdebug, тот связывается с PhpStorm, они о чём-то между собою договариваются, и мы наблюдаем процесс отладки в своей любимой IDE. Наша оставшаяся задача — настроить Xdebug и PhpStorm таким образом, чтобы они соединялись и понимали, что вот это вот соединение относится к вот этому проекту, а файлы от проекта лежат вон там-то, их и будем дебажить.

На этом с теорией всё. Перейдём к практике.

ВАЖНО: отключите прослушивание (телефон и жук должны быть не активны) и расширение Xdebug в браузере. Это важно для подачи материала, иначе примеры по ходу статьи будут расходиться с вашими результатами.

Создаём проект в PHPStorm

Создайте каталог проекта. Я использую C:\WWW\debug. Откройте его в PhpStorm.

Создадим в проекте два файла, которые и будем дебажить: index.php и 1.php

index.php
<?php

phpinfo();
1.php
<?php

$a = 5;
$b = 3;

$c = $a + $b;

echo $c;

Создаём контейнер Docker

Настала пора создать тот самый контейнер Docker, в котором у нас будет проходить отладка. Я взял за основу образ php:7.4-cli и просто-напросто запустил отладочный сервер PHP. Создаём Dockerfile следующего содержания:

Содержимое Dockerfile
# Базовый образ PHP
FROM php:7.4-cli

# Установка рабочего каталога
WORKDIR /var/www/html

# Запуск скрипта PHP со встроенным сервером
CMD ["php", "-S", "0.0.0.0:8080"]

Собирать образ и создавать контейнер очень удобно из самой IDE. Нажимайте на зелёненькие стрелочки и выбирайте New Run Configuration

New Run Configuration
New Run Configuration
New Run Configuration

Далее введите имя конфигурации: debug, имя для Image tag: de/bug и имя для контейнера: de-bug

Конфигурация
Конфигурация
Конфигурация

Настройки контейнера надо чуть-чуть дополнить. Нажмите на Modify и поставьте галочки у Bind ports, Bind mounts и Environment variables

Скриншот окна настроек

Настройте Bind ports. Кликните на символ папки, на плюсик и вбейте значения 8080 для контейнера и хоста. Ключевые элементы интерфейса выделены стрелочками. Эта директива будет перенаправлять все запросы на порт 8080 хоста в контейнер, на порт 8080.

Скриншот окна настроек

Настройте Bind mounts. Настройка происходит аналогично Bind ports. Сопоставьте каталог C:\WWW\debug на хосте с путём /var/www/html в контейнере.

Скриншот окна настроек

Environment variables пока оставьте пустым. Нажмите на кнопку Run. Сначала собирается образ, потом создаётся и запускается контейнер.

За прогрессом удобно наблюдать в Build Log.

Давайте проверим, работает ли наш контейнер. Заходим в браузер и переходим по адресу: http://127.0.0.1:8080

Открылся вывод phpinfo. Рабочий контейнер создан, наши php скрипты обрабатываются.

Добавляем расширение Xdebug

Существует множество способ добавить Xdebug, но мне больше нравится пользоваться скриптом docker-php-extension-installer, созданным итальянским программистом Мишелем Локатти

Мишель Локатти
Мишель Локатти
Мишель Локатти

С описанием скрипта и его возможностями можно ознакомиться здесь

Скрипт установит все необходимые пакеты APT/APK, а в конце выполнения скрипта ненужные пакеты будут удалены, так что docker-образ станет намного меньше.

Добавляем в наш Dockerfile следующие строки после секции FROM:

# Xdebug
COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/bin/
RUN install-php-extensions Xdebug

Первая строка копирует скрипт из образа mlocati/php-extension-installer на Docker Hub, вторая его запускает.

Результирующий Dockerfile
# Базовый образ PHP
FROM php:7.4-cli

# Xdebug
COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/bin/
RUN install-php-extensions Xdebug

# Установка рабочего каталога
WORKDIR /var/www/html

# Запуск скрипта PHP со встроенным сервером
CMD ["php", "-S", "0.0.0.0:8080"]

Теперь снова нажимаем зелёные стрелочки и просто выбираем Run 'Dockerfile'. Наш контейнер настроен, а интеллектуальный PhpStorm сам остановит предыдущую версию и удалит её, а затем пересоберёт образ, создаст новый контейнер с тем же именем и запустит его.

Зелёные стрелочки

Переходим по адресу: http://127.0.0.1:8080

Изучаем вывод, ищем секцию Xdebug. Секция появилась, видно, что установилась версия Xdebug 3.1.6, но не все параметры по умолчанию нам подходят

Вывод phpinfo()

В частности выключена пошаговая отладка и клиентский хост установлен неверно: localhost внутри контейнера.

Настраиваем Xdebug

Создадим минимальную конфигурацию для Xdebug. Обычно достаточно следующих строк:

zend_extension=Xdebug.so
Xdebug.mode=debug
Xdebug.client_host=host.docker.internal
Xdebug.log=/var/log/Xdebug.log

host.docker.internal — специальное имя для IP адреса, по которому доступна хост-машина из контейнера Docker. Ходят слухи, что это имя прописывается в файле /etc/hosts контейнера, но на самом деле в Docker Desktop для Windows и Mac это имя автоматически разрешается на уровне DNS, что позволяет контейнерам обращаться к хосту без необходимости явного указания в /etc/hosts. А вот в Linux это имя недоступно. Я не исследовал вопрос, как разрешить IP хоста в Linux, надеюсь, гуру оставят своё мнение в комментариях.

Всё обилие остальных настроек, как правило, кочует из гайда в гайд, и зачастую либо не нужно, либо противоречит друг другу, либо создаёт ненужную нагрузку на сервер, замедляя выполнение сценариев. Если честно, то последняя строка тоже не нужна. Я вставил её для опытов. В дальнейшем её можно безболезненно удалить.

Создадим файл Xdebug.conf и поместим в него эти строки.

Теперь скопируем этот файл с хоста в контейнер. Добавим следующие строки в наш Dockerfile:

# Копирование конфигурационного файла Xdebug
COPY Xdebug.conf /usr/local/etc/php/conf.d/docker-php-ext-Xdebug.ini

PHP в контейнере просматривает эту директорию при запуске и применяет любые дополнительные конфигурационные файлы, которые он находит. Это позволяет настраивать поведение PHP без изменения основного конфигурационного файла php.ini.

Вот так выглядит наш файл Dockerfile теперь:

Результирующий Dockerfile
# Базовый образ PHP
FROM php:7.4-cli

# Xdebug
COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/bin/
RUN install-php-extensions Xdebug

# Копирование конфигурационного файла Xdebug
COPY Xdebug.conf /usr/local/etc/php/conf.d/docker-php-ext-Xdebug.ini

# Установка рабочего каталога
WORKDIR /var/www/html

# Запуск скрипта PHP со встроенным сервером
CMD ["php", "-S", "0.0.0.0:8080"]

Пересобираем контейнер. Помните про зелёные стрелочки? Заходим по адресу http://127.0.0.1:8080 и видим следующую картину:

Вывод phpinfo()

Настройки в порядке. Xdebug готов к отладке.

Что там в логах Xdebug?

Давайте заглянем в логи Xdebug. Отключили прослушивание и расширение Xdebug в браузере? Тогда соединимся с контейнером и заглянем, что там у него внутри. PhpStorm тут как всегда на высоте.

Открываем терминал

Выбираем Services, контейнер и жмём терминал. В терминале подаём команду:

tail -f /var/log/Xdebug.log

В ответ получаем следующие сообщения:

root@5c0b8f79fbe9:/var/www/html# tail -f /var/log/Xdebug.log
[1] Log opened at 2024-08-08 17:38:38.254112
[1] [Config] INFO: Trigger value for 'Xdebug_TRIGGER' not found, falling back to 'Xdebug_SESSION'
[1] [Config] INFO: Trigger value for 'Xdebug_SESSION' not found, so not activating
[1] Log closed at 2024-08-08 17:38:38.260438

Логи Xdebug показывают, что он не был активирован, поскольку не было найдено управляющего значения для Xdebug_TRIGGER, а затем и для Xdebug_SESSION.

Xdebug_TRIGGER и Xdebug_SESSION — это способы указать Xdebug, когда он должен начинать отладку. Они могут быть указаны, например, через параметры GET, POST или COOKIE в вашем запросе. Если указанные параметры отсутствуют, Xdebug не будет активирован.

Давайте изменим наш запрос на http://127.0.0.1:8080/index.php?Xdebug_SESSION=start

Смотрим в терминал, появились новые строчки:

[1] Log opened at 2024-08-08 17:52:52.050010
[1] [Config] INFO: Trigger value for 'Xdebug_TRIGGER' not found, falling back to 'Xdebug_SESSION'
[1] [Config] INFO: No shared secret: Activating
[1] [Step Debug] INFO: Connecting to configured address/port: host.docker.internal:9003.
[1] [Step Debug] WARN: Creating socket for 'host.docker.internal:9003', poll success, but error: Operation now in progress (29).
[1] [Step Debug] WARN: Creating socket for 'host.docker.internal:9003', connect: Network is unreachable.
[1] [Step Debug] ERR: Could not connect to debugging client. Tried: host.docker.internal:9003 (through Xdebug.client_host/Xdebug.client_port) :-(
[1] Log closed at 2024-08-08 17:52:52.060655

Видно, что Xdebug был активирован, но не удалось установить соединение на заданный хост и порт.

ERR: Could not connect to debugging client. Tried: host.docker.internal:9003 (through Xdebug.client_host/Xdebug.client_port) :-(

Чтобы избежать этого недоразумения, нажимаем кнопку с жуком (трубкой телефона) или выполняем Run>Start Listening for PHP Debug Connection.

Обновляем страницу на http://127.0.0.1:8080/index.php?Xdebug_SESSION=start. Смотрим в терминал. Видим много-много логов. Я не буду тащить их в статью. Но очевидно, что Xdebug очень плотно общался с PhpStorm.

Кто же подставляет все эти Xdebug_SESSION? Этим занимается расширение браузера Xdebug Chrome Extension

Оно вообще делает массу вещей под капотом. Включите это расширение, уберите index.php?Xdebug_SESSION=start, чтобы осталось http://127.0.0.1:8080/ и обновите страницу.

Секция PHP Variables

А теперь отключите расширение и обновите страницу. Сравните со скриншотом.

Возникает вопрос: раз у нас всё так замечательно, документально подтверждено, что отладка стартует, диалог с PHPStorm есть, так может быть пора ставить точку останова и приступать к отладке?

Пока не выйдет. Просто потому, что PhpStorm пока не знает, с каким проектом связать активный трафик с Xdebug и какие файлы брать за основу для отладки. Будем его этому обучать.

Настраиваем PhpStorm

Устанавливаем CLI Interpreter

Заходим в настройки: Ctrl+Alt+S Слева выбираем PHP, в CLI Interpreter нажимаем три точки

Скриншот настроек

В появившемся окне нажимаем плюсик, выбираем From Docker, Vagrant…

Скриншот настроек

Выбираем Radiobutton Docker и в Image name в выпадающем списке выбираем наш образ

Скриншот настроек

После того, как PHPStorm немного подумает и в фоне притащит Docker-контейнер phpstorm_helpers, он совершенно верно определит и версию PHP и версию Xdebug в контейнере.

Скриншот настроек

Нажимаем ОК, выходим на уровень вверх и там согласуем версии языка с реальной версией.

Скриншот настроек

С CLI Interpreter закончили.

Устанавливаем сервер, на котором происходит отладка

Заходим в настройки: Ctrl+Alt+S Слева выбираем PHP>Servers, плюсик, заполняем поля данными, как на скриншоте. Обязательно запомните имя сервера debug. Оно нам пригодится.

Скриншот настроек

Здесь же обязательно надо замаппить локальное расположение файлов и расположение файлов в контейнере. Для этого вскидываем флажок и в раскрывшиеся поля вбиваем следующую информацию:

Скриншот настроек

Теперь наконец-то дело дошло до Environment variables. Дорабатываем настройку нашего контейнера. Зелёные стрелочки помните?

Зелёные стрелочки > Edit 'Dockerfile', настраиваем переменную как на скриншоте. Вбиваем имя сервера, на котором происходит отладка (я просил его запомнить).

Скриншот настроек

Что это за переменная и для чего она нужна? Вот что написано в документации

Чтобы указать PhpStorm, какая конфигурация сопоставления путей должна использоваться для подключения с определенного компьютера, нужно установить значение переменной окружения PHP_IDE_CONFIG равным serverName=SomeName, где SomeName это имя конфигурации сервера отладки. В нашем случае это debug.

Проверяем, как работает отладка

Запускаем контейнер. Зелёные стрелочки > Run. В файле 1.php ставим точку останова на последней строке. В браузере включаем расширение Xdebug. В PphStorm включаем прослушивание (жук/трубка/команда). Заходим по адресу: http://http://127.0.0.1:8080/1.php. Упс! Точка останова сработала!

Отладка работает

Виден стек, значения переменных и т.п.

У кого горит проект, могут приступить к отладке, а самых терпеливых ждут бонусные материалы.

Бонусные материалы для самых терпеливых

Лично мне нравится иная конфигурация, без создания переменной PHP_IDE_CONFIG. Самостоятельно удалите её из опций сборки контейнера. Перезапустите контейнер.

Run/Debug конфигурация

Вместо этого давайте создадим Run/Debug конфигурацию. Для этого сверху справа (в новом интерфейсе) нажмите три точки и выберите Edit.

Скриншот настроек

Далее плюсик, PHP Remote Debug

Скриншот настроек

Заполняем данные, как на скриншоте. Ставим галочку Filter debug connection by IDE key, из выпадающего списка выбираем сервер, на котором происходит отладка и вводит сам key.

Скриншот настроек

IDE key задаётся в настройках Xdebug Chrome Extension

Xdebug Chrome Extension

Теперь следует включать для прослушки не жука слева, а жука справа. Жук слева может быть выключенным.

Секция PHP Variables

Можно пробовать отладку. У меня работает.

Использование docker-compose

Если у вас всего один контейнер на этом можно и остановиться. Но иногда для работы веб-приложения их нужно несколько. Можно, конечно, стартовать контейнеры вручную, но и здесь PhpStorm предлагает изящное решение.

Для начала создадим docker-compose.yml следующего содержания

docker-compose.yml
services:
  web:
    build: .
    ports:
      - "8080:8080"
    volumes:
      - .:/var/www/html

Во многих методичках пишут, что необходимо добавить следующую директиву:

    extra_hosts:
      - "host.docker.internal:host-gateway"

Директива создаст дополнительную запись в /etc/hosts внутри каждого запущенного контейнера Docker, соответствующую содержимому этой директивы. Эта запись будет устанавливать соответствие между host.docker.internal и IP-адресом хоста Docker, который указан в host-gateway.

Как я уже отмечал, в случае Windows и Mac в этом нет нужды. Но эта директива может оказаться полезной в Linux.

Теперь перейдём к настройкам Cli Interpreter. Мы уже настраивали его, вам должно быть всё знакомо.

Ctrl+Alt+S — PHP — три точки в строке Cli Interpreter

Скриншот настроек

В открывшемся окне плюсик, выбрать From Docker, Vagrant…

Скриншот настроек

В следующем окне выбираем Radiobutton Docker Compose, файл с конфигурацией определяется автоматически, а службу нужно выбрать в выпадающем списке.

Скриншот настроек

Жмём ОК. Убеждаемся, что версия PHP и XDebug определены верно. Также здесь можно настроить жизненный цикл контейнера. Выставьте по своему усмотрению. Снова ОК.

Скриншот настроек

Давайте запустим docker-compose

Скриншот настроек

После первого запуска, наша конфигурация попадёт в секцию Run/Debug Configuration и запускать контейнеры можно будет оттуда.

Run/Debug Configuration

Мы же выберем конфигурацию удалённой отладки debug и инициируем отладочную сессию.

Жмём на правого жука, мы это уже делали. Получаем такую картинку:

Запускаем отладку

Переходим на http://127.0.0.1:8080/1.php и проверяем отладку.

Отладка работает

У меня всё работает.

Замечание: у меня сложилось ощущение, что при использовании docker-compose всегда используются уже собранные образы. Т.е. можно попасть в такую ситуацию, когда в Dockerfile произошли изменения, но при этом будет использоваться прежний образ. Если я заблуждаюсь, отпишитесь в комментариях. Таким образом, данный способ работы хорош для уже отлаженных контейнеров, не требующих изменений. Единственное, что я нашёл, так это возможность настроить удаление образов, после команды down:

Скриншот настроек

А на этом всё

Надеюсь, теперь вам не составит труда адаптировать свои контейнеры для удалённой отладки. Приятной работы!

Комментарии (4)


  1. docxplus
    09.08.2024 08:27
    +1

    11 лет писал на php. Я думал на php. Но все равно каждый раз страдал пытаясь настроить xdebug. Чувствовал что что-то не так. Перешел на голанг и его дебагер просто наслаждение. Всего одна кнопка


    1. kevin
      09.08.2024 08:27
      +1

      Это вы еще в питоне отлаживали :) там все очень просто


    1. 2medic Автор
      09.08.2024 08:27

      Ну так это компилируемый язык, и всё, что нужно знать IDE для отладки это заранее подготовленную матаинформацию, что с чем соотносить в окне отладки.

      Так-то у меня и в Turbo-Pascal 5 не было проблем с интегрированной отладкой.

      Hidden text

      А в случае Docker скорее надо сравнивать с standalone отладкой. На примере того же TP-5, представим, что мы разработали программу. Скажем, управляющую оборудованием в операционной (истинная правда, такая программа была написана даже на 4-м Паскале). И в ней что-то идёт не так. Причём в лабораторных условиях проблему воспроизвести не удаётся. Тогда приходится вылезать из удобной IDE, брать исходники, создавать файлы с метаинформации, standalone debugger и идти дебажить в полевые условия.

      Так что вот такой кейс ближе к описываемому сценарию отладки.


  1. karrakoliko
    09.08.2024 08:27
    +2

    есть еще нюанс с docker-compose:

    если у вас указана перем.окружения COMPOSE_PROJECT_NAME=SOME

    то нужно прописывать его в настройках интерпретатора в phpstorm (в env variables). без этого что-то не срабатывает (уже не помню что именно, помню что провозился из за этого с настройкой docker+xdebug+phpunit)