- PVSM.RU - https://www.pvsm.ru -
Для отладки PHP-программ часто используют Xdebug [1]. Однако стандартных возможностей IDE и Xdebug не всегда достаточно. Часть проблем можно решить с помощью Xdebug proxy — pydbgpproxy, но всё же не все. Поэтому я реализовал PHP Xdebug proxy [2] на базе асинхронного фреймворка amphp.
Под катом я расскажу, что не так с pydbgpproxy, чего в нём не хватает и почему я не стал его дорабатывать. Также объясню, как работает PHP Xdebug proxy, и покажу на примере, как его расширять.
Xdebug proxy является промежуточным сервисом между IDE и Xdebug (проксирует запросы от Xdebug к IDE и обратно). Чаще всего он используется для multiuser debugging [3]. Это когда у вас один веб-сервер, а разработчиков — несколько.
В качестве proxy обычно используют pydbgpproxy. Но у него есть несколько проблем:
Поиск Xdebug proxy, написанного на PHP, на GitHub и в интернете результатов не дал. Поэтому я написал PHP Xdebug proxy [2]. Под капотом я использовал асинхронный фреймворк amphp [5].
Основные преимущества PHP Xdebug proxy перед pydbgpproxy:
PHP Xdebug proxy можно установить как dev-зависимость через composer [6]:
composer.phar require mougrim/php-xdebug-proxy --dev
Но если вы не хотите тащить в свой проект лишние зависимости, то PHP Xdebug proxy можно установить как проект через тот же composer:
composer.phar create-project mougrim/php-xdebug-proxy
cd php-xdebug-proxy
PHP Xdebug proxy расширяемый, но по умолчанию для работы требуется ext-dom [7] (расширение включено по умолчанию в PHP) для разбора XML и amphp/log [8] для асинхронной записи в логи:
composer.phar require amphp/log '^1.0.0'
Запускается proxy следующим образом:
bin/xdebug-proxy
Proxy запустится с настройками по умолчанию:
Using config path /path/to/php-xdebug-proxy/config
[2019-02-14 10:46:24] xdebug-proxy.NOTICE: Use default ide: 127.0.0.1:9000 array ( ) array ( )
[2019-02-14 10:46:24] xdebug-proxy.NOTICE: Use predefined ides array ( 'predefinedIdeList' => array ( 'idekey' => '127.0.0.1:9000', ), ) array ( )
[2019-02-14 10:46:24] xdebug-proxy.NOTICE: [Proxy][IdeRegistration] Listening for new connections on '127.0.0.1:9001'... array ( ) array ( )
[2019-02-14 10:46:24] xdebug-proxy.NOTICE: [Proxy][Xdebug] Listening for new connections on '127.0.0.1:9002'... array ( ) array ( )
Из лога видно, что по умолчанию proxy:
127.0.0.1:9001
для подключений регистраций IDE;127.0.0.1:9002
для подключений Xdebug;127.0.0.1:9000
как IDE по умолчанию и предустановленную IDE с ключом idekey.Если есть желание настроить прослушиваемые порты и т. д., то можно указать путь до папки с настройками. Достаточно скопировать папку config [9]:
cp -r /path/to/php-xdebug-proxy/config /your/custom/path
В папке с настройками три файла:
config.php
[10]:
<?php
return [
'xdebugServer' => [
// host:port для прослушивания подключений Xdebug
'listen' => '127.0.0.1:9002',
],
'ideServer' => [
// Если proxy не может найти IDE, то он будет использовать IDE по умолчанию,
// если нужно отключить IDE по умолчанию, то нужно передать пустую строку.
// IDE по умолчанию полезна, когда proxy пользуется только один человек.
'defaultIde' => '127.0.0.1:9000',
// Предопределённые IDE указываются в формате 'idekey' => 'host:port',
// если предопределённые IDE не нужны, то можно указать пустой массив.
// Предопределённые IDE полезны, когда пользователи proxy меняются нечасто,
// так им не нужно будет заново регистрироваться при каждом перезапуске proxy.
'predefinedIdeList' => [
'idekey' => '127.0.0.1:9000',
],
],
'ideRegistrationServer' => [
// host:port для прослушивания подключений регистраций IDE,
// если требуется отключить регистрации IDE, то нужно передать пустую строку.
'listen' => '127.0.0.1:9001',
],
];
logger.php
[11]: можно настроить логгер; файл должен возвращать объект, который является экземпляром PsrLogLoggerInterface
, по умолчанию используется MonologLogger
с AmpLogStreamHandler
(для неблокирующей записи), выводит логи в stdout;factory.php
[12]: можно настроить классы, которые используются в proxy; файл должен возвращать объект, который является экземпляром FactoryFactory
[13], по умолчанию используется FactoryDefaultFactory
[14].После копирования файлы можно отредактировать и запустить proxy:
bin/xdebug-proxy --configs=/your/custom/path/config
О том, как отлаживать код с помощью Xdebug, написано много статей. Отмечу основные моменты.
В php.ini должны быть следующие настройки в секции [xdebug]
(исправьте их, если они отличаются от стандартных):
Дальше можно запускать отлаживаемый PHP-код:
php /path/to/your/script.php
Если вы всё правильно сделали, то отладка начнётся с первого breakpoint в IDE. Отладка в режиме php-fpm несколькими разработчиками выходит за рамки данной статьи, но описана, например, здесь.
Всё, что мы рассмотрели выше, в той или иной степени умеет и pydbgpproxy.
Теперь поговорим о самом интересном в PHP Xdebug proxy. Прокси можно расширять, используя свою фабрику (создаётся в конфиге factory.php
, см. выше). Фабрика должна реализовывать интерфейс FactoryFactory
[13].
Наиболее мощными являются так называемые подготовители запросов (request preparers). Они могут изменять запросы от Xdebug к IDE и обратные. Чтобы добавить подготовитель запроса, нужно переопределить метод FactoryDefaultFactory::createRequestPreparers()
. Метод возвращает массив объектов, которые реализовывают интерфейс RequestPreparerRequestPreparer
[15]. При проксировании запроса от Xdebug к IDE они выполняются в прямом порядке, при проксировании запроса от IDE к Xdebug — в обратном.
Подготовители запросов могут использоваться, например, для изменения путей до файлов (в breakpoints и выполняемых файлах).
Для того, чтобы привести пример подготовителя, сделаю небольшое отступление. В unit-тестах мы используем soft-mocks [16] (GitHub [17]). Soft-mocks позволяет подменять функции, статические методы, константы и т. д. в тестах, является альтернативой для runkit [18] и uopz [19]. Работает это за счет переписывания PHP-файлов на лету. Подобным образом ещё работает AspectMock [20].
Но стандартные возможности Xdebug и IDE позволяют отлаживать переписанные (имеющие другой путь), а не оригинальные файлы.
Рассмотрим подробнее проблему отладки с использованием soft-mocks в тестах. Для начала возьмём случай, когда PHP-код выполняется локально.
Первые сложности появляются на этапе установки точек останова (breakpoints). В IDE они устанавливаются в оригинальные файлы, а не в переписанные. Чтобы поставить breakpoint через IDE, нужно найти актуальный переписанный файл. Проблема усугубляется тем, что при каждом изменении оригинального файла создаётся новый переписанный файл, то есть для каждого уникального содержимого файла будет уникальный переписанный файл.
Эту проблему можно решить вызовом функции xdebug_break()
, которая аналогична выставлению точки останова. Необходимость поиска переписанного файла в этом случае отпадает.
Теперь рассмотрим ситуацию посложнее: приложение выполняется на удалённой машине.
В этом случае можно примонтировать папку с переписанными файлами, например, через SSHFS. Если локальный и удалённый пути до папки различаются, то ещё нужно прописать mappings в IDE.
Так или иначе, этот способ немного отличается от привычного и позволяет отлаживать только переписанные файлы, но не оригинальные. Но всё же хочется редактировать и отлаживать одни и те же оригинальные файлы.
В AspectMock обошли проблему включением режима дебага [21] без возможности его отключить:
public function init(array $options = [])
{
if (!isset($options['excludePaths'])) {
$options['excludePaths'] = [];
}
$options['debug'] = true;
$options['excludePaths'][] = __DIR__;
parent::init($options);
}
В простом примере теста режим дебага навскидку медленнее процентов на 20. Но у меня нет достаточного количества тестов на AspectMock, чтобы дать более точную оценку того, насколько он медленнее. Если у вас есть много тестов на AspectMock, я буду рад, если вы поделитесь сравнением в комментариях.
Теперь, когда понятна проблема, рассмотрим, как можно её решить с использованием PHP Xdebug proxy. Основная часть находится в классе RequestPreparerSoftMocksRequestPreparer
[22].
В конструкторе класса определяем путь до скрипта инициализации soft-mocks и запускаем его (предполагается, что soft-mocks подключён как зависимость, но в конструктор можно передать любой путь):
public function __construct(LoggerInterface $logger, string $initScript = '')
{
$this->logger = $logger;
if (!$initScript) {
$possibleInitScriptPaths = [
// proxy установлен как проект, soft-mocks — как зависимость проекта
__DIR__.'/../../vendor/badoo/soft-mocks/src/init_with_composer.php',
// proxy и soft-mocks установлены как зависимости
__DIR__.'/../../../../badoo/soft-mocks/src/init_with_composer.php',
];
foreach ($possibleInitScriptPaths as $possiblInitScriptPath) {
if (file_exists($possiblInitScriptPath)) {
$initScript = $possiblInitScriptPath;
break;
}
}
}
if (!$initScript) {
throw new Error("Can't find soft-mocks init script");
}
// инициализируем soft-mocks (путь до папки с переписанными файлами и т.д.)
require $initScript;
}
Для подготовки запроса от Xdebug к IDE нужно заменить путь до переписанного файла путём оригинального файла:
public function prepareRequestToIde(XmlDocument $xmlRequest, string $rawRequest): void
{
$context = [
'request' => $rawRequest,
];
$root = $xmlRequest->getRoot();
if (!$root) {
return;
}
foreach ($root->getChildren() as $child) {
// путь до переписанного файла лежит в одном из тегов:
// - 'stack': https://xdebug.org/docs-dbgp.php#stack-get
// - 'xdebug:message': https://xdebug.org/docs-dbgp.php#error-notification
if (!in_array($child->getName(), ['stack', 'xdebug:message'], true)) {
continue;
}
$attributes = $child->getAttributes();
if (isset($attributes['filename'])) {
// если в атрибутах тега есть путь до переписанного файла, то заменяем его оригинальным путём
$filename = $this->getOriginalFilePath($attributes['filename'], $context);
if ($attributes['filename'] !== $filename) {
$this->logger->info("Change '{$attributes['filename']}' to '{$filename}'", $context);
$child->addAttribute('filename', $filename);
}
}
}
}
Для подготовки запроса от IDE к Xdebug нужно заменить путь до оригинального файла путём до переписанного:
public function prepareRequestToXdebug(string $request, CommandToXdebugParser $commandToXdebugParser): string
{
// разбираем запрос на команду и аргументы
[$command, $arguments] = $commandToXdebugParser->parseCommand($request);
$context = [
'request' => $request,
'arguments' => $arguments,
];
if ($command === 'breakpoint_set') {
// если есть аргумент -f, то заменяем путь до оригинального файла путём до переписанного
// см. https://xdebug.org/docs-dbgp.php#id3
if (isset($arguments['-f'])) {
$file = $this->getRewrittenFilePath($arguments['-f'], $context);
if ($file) {
$this->logger->info("Change '{$arguments['-f']}' to '{$file}'", $context);
$arguments['-f'] = $file;
// собираем обратно запрос
$request = $commandToXdebugParser->buildCommand($command, $arguments);
}
} else {
$this->logger->error("Command {$command} is without argument '-f'", $context);
}
}
return $request;
}
Чтобы подготовитель запроса заработал, нужно создать свой класс фабрики и либо наследовать его от FactoryDefaultFactory
[23], либо имплементировать интерфейс FactoryFactory
[13]. Для soft-mocks фабрика FactorySoftMocksFactory
[24] выглядит так:
class SoftMocksFactory extends DefaultFactory
{
public function createConfig(array $config): Config
{
// здесь создаём объект своего класса конфига
return new SoftMocksConfig($config);
}
public function createRequestPreparers(LoggerInterface $logger, Config $config): array
{
$requestPreparers = parent::createRequestPreparers($logger, $config);
return array_merge($requestPreparers, [$this->createSoftMocksRequestPreparer($logger, $config)]);
}
public function createSoftMocksRequestPreparer(LoggerInterface $logger, SoftMocksConfig $config): SoftMocksRequestPreparer
{
// здесь передаём путь до init-скрипта из конфига
return new SoftMocksRequestPreparer($logger, $config->getSoftMocks()->getInitScript());
}
}
Свой класс конфига здесь нужен, чтобы можно было указать путь init-скрипта soft-mocks. Что он из себя представляет, посмотреть можно в ConfigSoftMocksConfig [25].
Осталась самая малость: создать новую фабрику и указать путь до init-скрипта soft-mocks. Как это делается, можно посмотреть в softMocksConfig
[26].
Как я уже писал выше, PHP Xdebug proxy под капотом использует amphp, а значит, для работы с I/O [27] должен использоваться неблокирующий API. В apmphp уже есть немало компонентов, которые реализуют этот неблокирующий API. Если вы собираетесь расширять PHP Xdebug proxy и использовать его в многопользовательском режиме, то обязательно используйте неблокирующие API.
PHP Xdebug proxy — ещё довольно молодой проект, но в Badoo он уже активно используется для отладки тестов с использованием soft-mocks.
PHP Xdebug proxy:
Если вы используете Xdebug proxy для чего-то, кроме multiuser debugging, то поделитесь своим кейсом и Xdebug proxy, которым пользуетесь, в комментариях.
Если вы используете pydbgpproxy или какой-то другой Xdebug proxy, то попробуйте PHP Xdebug proxy, расскажите о своих проблемах, поделитесь pull requests. Давайте развивать проект вместе! :)
P. S. Спасибо моему коллеге Евгению Махрову aka eZH [28] за идею proxy smdbgpproxy [29]!
Спасибо за внимание!
Буду рад комментариям и предложениям.
Ринат Ахмадеев, Sr. PHP developer
Автор: mougrim
Источник [30]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/programmirovanie/331974
Ссылки в тексте:
[1] Xdebug: https://xdebug.org/
[2] PHP Xdebug proxy: https://github.com/mougrim/php-xdebug-proxy
[3] multiuser debugging: https://www.jetbrains.com/help/phpstorm/multiuser-debugging-via-xdebug-proxies.html
[4] здесь: http://code.activestate.com/komodo/remotedebugging/
[5] amphp: https://amphp.org/
[6] composer: https://getcomposer.org/
[7] ext-dom: https://secure.php.net/manual/ru/book.dom.php
[8] amphp/log: https://github.com/amphp/log
[9] config: https://github.com/mougrim/php-xdebug-proxy/tree/0.4.0/config
[10] config.php
: https://github.com/mougrim/php-xdebug-proxy/tree/0.4.0/config/config.php
[11] logger.php
: https://github.com/mougrim/php-xdebug-proxy/tree/0.4.0/config/logger.php
[12] factory.php
: https://github.com/mougrim/php-xdebug-proxy/tree/0.4.0/config/factory.php
[13] FactoryFactory
: https://github.com/mougrim/php-xdebug-proxy/tree/0.4.0/src/Factory/Factory.php
[14] FactoryDefaultFactory
: https://github.com/mougrim/php-xdebug-proxy/blob/0.4.0/src/Factory/DefaultFactory.php
[15] RequestPreparerRequestPreparer
: https://github.com/mougrim/php-xdebug-proxy/tree/0.4.0/src/RequestPreparer/RequestPreparer.php
[16] soft-mocks: https://habr.com/ru/company/badoo/blog/279617/
[17] GitHub: https://github.com/badoo/soft-mocks
[18] runkit: http://php.net/manual/ru/book.runkit.php
[19] uopz: http://php.net/manual/ru/book.uopz.php
[20] AspectMock: https://github.com/Codeception/AspectMock
[21] включением режима дебага: https://github.com/Codeception/AspectMock/blob/3.0.2/src/AspectMock/Kernel.php#L23
[22] RequestPreparerSoftMocksRequestPreparer
: https://github.com/mougrim/php-xdebug-proxy/tree/0.4.0/src/RequestPreparer/SoftMocksRequestPreparer.php
[23] FactoryDefaultFactory
: https://github.com/mougrim/php-xdebug-proxy/tree/0.4.0/src/Factory/DefaultFactory.php
[24] FactorySoftMocksFactory
: https://github.com/mougrim/php-xdebug-proxy/tree/0.4.0/src/Factory/SoftMocksFactory.php
[25] ConfigSoftMocksConfig: https://github.com/mougrim/php-xdebug-proxy/blob/0.4.0/src/Config/SoftMocksConfig.php
[26] softMocksConfig
: https://github.com/mougrim/php-xdebug-proxy/tree/0.4.0/softMocksConfig
[27] I/O: https://ru.wikipedia.org/wiki/%D0%92%D0%B2%D0%BE%D0%B4-%D0%B2%D1%8B%D0%B2%D0%BE%D0%B4
[28] eZH: https://habr.com/ru/users/ezh/
[29] smdbgpproxy: https://github.com/eelf/smdbgpproxy
[30] Источник: https://habr.com/ru/post/442504/?utm_campaign=442504&utm_source=habrahabr&utm_medium=rss
Нажмите здесь для печати.