La mise au point de programmes PHP est parfois perçue comme archaïque, car la configuration d’un environnement efficace de debugging peut s’avérer déroutante. Voici comment paramétrer une confortable installation pour profiter pleinement d’outils professionnels et maîtriser le développement local ou distant.
PHP est un langage qui a considérablement évolué depuis sa naissance il y a plus de 25 ans, tant dans sa philosophie que dans l’écosystème de travail. Si la recherche de problèmes dans un programme PHP a longtemps été synonyme d’affichage de var_dump() et d’interminables essais par tâtonnement, les IDE (Integrated Development Environment) disposent aujourd’hui de tout le nécessaire pour effectuer du pas à pas, poser des points d’arrêt conditionnels, examiner et modifier des variables en cours d’exécution et même intercepter des requêtes choisies sur des applicatifs déployés.
1. Extensions de débogage
Le langage, conçu dans un esprit de scripting, ne prévoyait pas à l’origine de moyens de mise au point. Le debugging nécessite une extension, à l’instar de beaucoup d’autres fonctionnalités modulaires pour PHP.
Il existe deux principales extensions de débogage : Zend et Xdebug. La première étant propriétaire et livrée exclusivement sous la forme d’un binaire, nous l’écarterons ici pour nous concentrer sur la seconde. D’autres tentatives de modules de débogage ont vu le jour au fil des années, mais elles n’ont pas survécu. Proposé et maintenu par Derick Rethans, l’auteur du désassembleur VLD [3], le projet Xdebug [1] est très actif et s’est imposé par sa grande qualité. Tous les principaux IDE le prennent en charge.
2. Utilisation de Xdebug
Xdebug fonctionne en mode client-serveur, où le programme PHP en cours d’exécution joue le rôle du client, et l’IDE est le serveur. Lorsque l’extension est installée et activée, le moteur PHP peut initier une connexion vers votre environnement de développement pour démarrer une session de débogage. Les deux parties communiquent en réseau, selon un protocole ouvert dénommé DBGp [2]. Plusieurs configurations sont disponibles pour l’établissement de la connexion pour couvrir tous les cas d’utilisation, ainsi que nous le verrons dans la suite.
Une fois la session démarrée, le client (le processus PHP qui exécute le script à examiner) obtient de la part du serveur (l’IDE) la liste des points d’arrêt et autres informations de mise au point. L’interaction commence alors ; l’environnement de développement peut exécuter des instructions pas à pas, inspecter types et contenus de variables, consulter et remonter la pile d’appels, le tout au moyen de discussions selon le protocole DBGp. À la fin de l’exécution du script (ou sur demande manuelle du développeur), la session se termine. Une autre connexion sera initiée lors d’une nouvelle exécution de script par le moteur PHP.
Client-serveur
Vous remarquerez en lisant la documentation officielle que les termes « client » et « serveur » y sont inversés, et désignent respectivement l’IDE et le moteur PHP. Cette terminologie me semble avoir été choisie pour s’accorder avec le schéma web selon lequel le moteur PHP joue le rôle de serveur et le poste de travail, celui de client. Mais, contrairement à d’autres langages pour lesquels le runtime est effectivement en écoute de connexions entrantes de débogage, le cas de PHP est différent et c’est bien l’IDE qui se place en écoute, et le serveur PHP qui initie, le cas échéant, une connexion vers le serveur de débogage qui se trouve être la station de travail.
2.1 Installation et activation
Plusieurs méthodes existent. Sur Debian Buster, l’extension php-xdebug peut s’installer directement par :
On vérifie aussitôt par :
Si cette méthode ne fonctionne pas pour vous, Xdebug est livré sous la forme d’un module PECL. Votre système doit posséder les dépendances nécessaires pour la compilation d’extensions PHP, que vous pouvez installer grâce au paquet php-dev (Debian) ou php-devel (CentOS) :
Vous disposerez ensuite de la commande pecl, et vous pourrez installer Xdebug de la façon suivante :
Ceci a pour effet de démarrer le téléchargement des sources et la compilation du module.
Une fois la construction terminée, on obtient le message suivant :
Il convient donc de suivre l’indication. Créez un fichier xdebug.ini comprenant la directive :
Il s’agit d’une zend_extension, en raison de son cycle de vie particulier [4] auprès du moteur PHP. Mais ceci n’a pas d’importance pour nous.
Il est inutile de préfixer le chemin complet du module xdebug.so, car PHP 7+ le déduit tout seul en fonction de ses paramètres de compilation.
Nous augmenterons le fichier xdebug.ini avec plus de directives dans la section 2.2 « Configuration ».
Placez ensuite ce fichier dans le répertoire de configuration de votre installation PHP, que vous pouvez identifier par la commande php -i en cherchant l’indication suivante :
Attention toutefois à ne pas oublier de placer également ce fichier si nécessaire dans les autres répertoires de configuration pour d’autres SAPI : la commande ci-dessus renvoie les informations pour CLI, elles seront différentes pour le module Apache notamment. Il s’agira généralement de liens symboliques vers des fichiers d’un répertoire /etc/php/7.3/mods-available ou similaire, selon les distributions. Il faudra ensuite penser à redémarrer ou recharger le service.
2.2 Configuration
Dès lors que l’extension Xdebug est chargée au niveau ini, le module est actif et est capable d’initier les connexions vers le terminal d’écoute, conformément aux paramètres que nous allons étudier maintenant.
Il est important de noter que les performances générales de PHP sont sensiblement dégradées lorsque Xdebug est activé, et il convient donc de réserver cette extension à un environnement de développement. En outre, un debugger installé sur un système en production peut constituer une porte d’attaque, comme nous allons le voir.
Voici les principales directives que nous allons détailler :
- xdebug.remote_enable ;
- xdebug.remote_port ;
- xdebug.remote_connect_back ;
- xdebug.remote_host ;
- xdebug.remote_mode ;
- xdebug.remote_autostart.
Ces paramètres permettent de contrôler trois situations bien différentes. Mais avant de les étudier, allumons l’aptitude de debugging par xdebug.remote_enable = true. Tant que ce paramètre n’est pas ouvert, l’extension est certes chargée par le moteur, mais elle n’est pas mise en œuvre (le serveur PHP n’essaie de contacter aucun terminal de débogage). Sur un système en production, il faudrait bien sûr veiller à ce qu’elle soit éteinte (ou mieux : que l’extension ne soit pas installée).
Nous allumons également pour le moment xdebug.remote_autostart = true, que nous aborderons dans la section 2.2.4 « Filtre ».
Gardez à l’esprit que, pour qu’elle soit prise en compte, toute modification de configuration ini nécessite le redémarrage / rechargement du service web.
2.2.1 Intercepter ses propres requêtes
Le premier cas est celui du débogage d’une requête sur le serveur de travail (local ou distant).
On place son IDE en écoute sur un port qu’on spécifie dans le paramètre xdebug.remote_port dans la configuration du serveur (par défaut 9000), on pointe le navigateur vers l’URL à attraper, et on attend que le serveur PHP tape à la porte de l’IDE en retour. Pour ce faire, le module Xdebug doit savoir que l’outil de débogage se trouve à la même adresse que l’origine de la requête HTTP : on configure donc xdebug.remote_connect_back = true.
Si le serveur web tourne en local, Xdebug n’aura pas de problème à atteindre le port ouvert par l’IDE. Mais il peut aussi être hébergé à l’extérieur et dans ce cas, l’hôte à contacter automatiquement par remote_connect_back est l’adresse IP de votre box internet (pensez à configurer le Port Forwarding sur votre routeur). Ce mode automatique résout le problème d’une adresse IP changeante, et permet également à plusieurs développeurs de faire des sessions de débogage sur ce même serveur distant, chacun depuis sa propre adresse (voir figure 1).
2.2.2 Requêtes d’un visiteur tiers
Il n’est pas toujours possible d’initier soi-même la requête qu’on souhaite déboguer ni de la lancer depuis le poste de l’IDE. C’est par exemple le cas si le scénario à examiner ne se déroule que sur un appareil mobile, ou encore si on doit attraper un point d’arrêt dans la visite d’un utilisateur dont on ne sait pas reproduire soi-même le problème rencontré.
Dans ce cas, Xdebug doit initier une connexion non pas vers l’adresse automatique du navigateur de la visite, mais vers l’adresse IP (fixe) de l’IDE, comme indiqué en figure 2. Ceci se configure grâce à xdebug.remote_host = <1.2.3.4> (en remettant bien sûr xdebug.remote_connect_back = false).
2.2.3 Mode de déclenchement
Par défaut, lorsque Xdebug est activé, toute requête HTTP entrante provoquera une tentative de connexion vers le serveur de débogage (sans échec si connexion impossible). Dès la connexion établie, l’IDE indique au moteur PHP où se trouvent les points d’arrêt souhaités. Le script invoqué se déroule normalement jusqu’au premier point d’arrêt, à l’occasion duquel le module Xdebug contacte de nouveau l’IDE pour prise en main interactive.
Ce mode de déclenchement est celui par défaut, et correspond au réglage xdebug.remote_mode = req. Toute requête démarre automatiquement une session DBGp.
Mais parfois le scénario ne devient intéressant que lorsque le programme rencontre une erreur (incompatibilité de types, constante non définie, dépassement d’index…) ou lève une exception. Dans ce cas, il n’est pas possible de savoir à l’avance où placer un breakpoint. C’est le rôle du mode xdebug.remote_mode = jit (Just In Time), dans lequel Xdebug ne démarre une session qu’à la survenance de l’erreur, et directement en point d’arrêt sur le lieu de l’accident.
Il existe une troisième façon de déclencher une session de pas à pas : c’est l’arrêt manuel, par un appel à la fonction xdebug_break() en dur dans le code. Cette méthode, qui peut sembler contre-intuitive, s’avère parfois pratique notamment quand il faut intercepter l’exécution dans du code généré (par Smarty par exemple).
2.2.4 Filtre de déclenchement
Nous avons vu pour le moment comment initier systématiquement une interaction de débogage, que ce soit dès le démarrage du script par le moteur, ou à la survenance d’une exception.
Pourtant, ce mécanisme n’est pas toujours souhaité : nous voudrions pouvoir décider quand débuguer et quand laisser le serveur tranquille, même sans modifier les paramètres ini (et donc sans devoir redémarrer le service).
Ceci peut se faire au moyen du réglage xdebug.remote_autostart, que nous avions allumé à true auparavant, et que nous pouvons maintenant passer à false.
Dans ce mode, une requête entrante ne déclenchera une connexion que si le navigateur se trouve au sein d’une Xdebug Session (à ne pas confondre avec la session PHP). Pour commencer cette session, nous passons le paramètre de requête (sur l’URL ou en POST) : XDEBUG_SESSION_START=<nomQuelconque>.
Si ce paramètre est présent, le module Xdebug initiera la communication avec l’IDE, et va également émettre pour le navigateur un cookie XDEBUG_SESSION, de façon à ce que toutes les requêtes suivantes soient également interceptées, tant que la session Xdebug perdure. Elle s’arrête si l’on supprime le cookie, ou s’il expire seul (par défaut une heure, réglable [5]), ou si l’on passe explicitement un autre paramètre de requête nommé XDEBUG_SESSION_STOP.
2.3 Un mot sur la sécurité
Il est important d’insister encore sur le mode de fonctionnement du debugger en PHP : c’est le moteur (serveur Apache, etc.) qui lance une connexion vers un poste de débogage.
Si Xdebug est activé sur un serveur, et configuré en mode Remote Connect Back, tout attaquant qui envoie une requête HTTP pourra espérer recevoir en retour une connexion entrante sur un port de debugging (pas spécialement difficile à deviner ou à sniffer). Or, il n’est pas nécessaire de disposer des fichiers sources pour examiner en détail un script PHP distant : une fois le debugger connecté et à l’arrêt sur le démarrage du script, le terminal DBGp peut scruter toutes les variables du scope en cours, connaître le chemin distant complet de tous les fichiers inclus, et même exécuter des instructions PHP arbitraires à distance sur le serveur.
En conséquence, il est important de bien configurer la machine Xdebug, et de ne pas laisser l’extension sur un serveur sensible en production.
2.4 Utilisation avec les IDE
Détaillons l’intégration dans deux des principaux IDE pour PHP, que sont PhpStorm et VS Code.
Dans ces deux logiciels, on peut poser des points d’arrêt en cliquant dans la gouttière de marge. Un clic droit sur le breakpoint permet d’y adosser une condition d’arrêt (de type : $variable == ‘valeur’, afin que l’interception n’ait lieu sur cette ligne que lorsque la condition est vérifiée au runtime). On pourra se référer à la documentation de l’éditeur, pour ce qui est de la manipulation des contrôles et menus en eux-mêmes. Voyons comment brancher Xdebug.
2.4.1 PhpStorm
La prise en charge de Xdebug est native. Il suffira de sélectionner le port et d’activer l’interception dans les Préférences, sous Languages & Frameworks > PHP > Debug comme en figure 3. En particulier, on indiquera s’il faut ou non stopper l’exécution dès le démarrage du script.
En outre, dans Languages & Frameworks > PHP > Servers (figure 4) il faut indiquer le mapping des fichiers : sous quel chemin du serveur se trouvent les fichiers dont on édite les sources localement. Ceci est nécessaire, car lors de la communication entre le serveur PHP et l’IDE, le module Xdebug voit les fichiers sous leur chemin côté serveur, et ne connaît pas l’arborescence sur la station de développement. En conséquence, tous les chemins concernés par la liste des breakpoints, la pile d’appels, etc. doivent subir une transposition de correspondance.
Notez que les mappings ne doivent pas obligatoirement suivre une arborescence directe : on peut notamment faire correspondre un répertoire distant en dehors de la racine du projet, avec un autre répertoire local. Ce cas peut se présenter par exemple si le projet utilise une bibliothèque partagée sur le serveur, dont on a aussi une copie locale, ou encore pour débuguer des scripts générés par un moteur de templates.
De plus, comme on peut être amené à travailler avec plusieurs serveurs (sachant que des containers Docker locaux sont assimilables à des serveurs distants), il y a la possibilité de définir des mappings différents pour chaque serveur.
Une fois ces réglages effectués, on place PhpStorm en mode écoute, par le bouton indiqué en figure 5.
2.4.2 VS Code
La prise en charge de Xdebug n’est pas préconstruite dans VS Code : il faut installer une extension. Nous utiliserons ici PHP Debug, de Felix Becker.
Une fois le plugin installé, on édite (ou crée) le fichier .vscode/launch.js à la racine du projet (on peut aussi utiliser l’assistant graphique comme en figure 6) pour y mettre le contenu suivant :
On retrouve ici essentiellement le port d’écoute et la déclaration de correspondance d’arborescences de fichiers entre le serveur et la station de travail (voir 2.4.1).
Il faut ensuite relancer VS Code, puis, en se plaçant sur le mode Debugger dans la barre de gauche, on peut cliquer sur l’icône « Start » verte en face de la configuration Listen for Xdebug, pour placer l’IDE en écoute sur le port choisi.
3. Profiling et couverture de tests
Xdebug offre une possibilité de profiling du code : il s’agit de constituer un rapport, à l’issue de l’exécution d’une requête, pour y analyser le temps passé dans chaque fonction et la consommation mémoire à chaque étape.
Cet examen est extrêmement gourmand (les fichiers temporaires utilisés peuvent facilement atteindre plusieurs centaines de Mio) et ralentit perceptiblement l’exécution du programme. Aussi le réglage xdebug.profiler_enable est false par défaut.
La documentation [5] indique l’ensemble des réglages qui contrôlent le répertoire des fichiers temporaires de rapports, et comment activer le profiling sélectivement pour une requête donnée de façon similaire au 2.2.4.
Les rapports sont au format Cachegrind [7] et peuvent donc être visionnés avec les outils standard tels que KCacheGrind (exemple en figure 7).
Xdebug sait aussi pister la couverture de code, utile pour déterminer les zones de code non traversées pendant les tests [9]. L’activation se contrôle par xdebug.coverage_enable et elle est même allumée par défaut. Cependant, les performances de Xdebug pour le code coverage ne sont pas les meilleures, et ce mode peut même ralentir considérablement les campagnes de test.
Il est préférable d’éteindre xdebug.coverage_enable = false, et d’utiliser Pcov [8], outil qui se spécialise dans cette voie, sans aptitude au debugging. Attention, les deux extensions sont mutuellement exclusives.
Conclusion
La mise en œuvre d’un environnement confortable et professionnel pour le debugging en PHP peut s’avérer décourageante au premier abord. Pourtant une fois correctement configuré, cet excellent outil permet aussi bien la détection de problèmes pendant le développement que le dépistage de situations incongrues sur un serveur en ligne.
Références
[1] Site du projet Xdebug : https://xdebug.org
[2] Protocole ouvert de débogage DBGp : https://xdebug.org/docs/dbgp
[3] G. ZERBIB, « Plongée dans l'OPcache », GNU/Linux Magazine no224, mars 2019 :
https://connect.ed-diamond.com/GNU-Linux-Magazine/GLMF-224/Plongee-dans-l-OPcache
[4] Extension vs Zend Extension : https://wiki.php.net/internals/extensions
[5] Paramètres de configuration Xdebug : https://xdebug.org/docs/all_settings
[6] Extension VS Code PHP Debug :
https://marketplace.visualstudio.com/items?itemName=felixfbecker.php-debug
[7] Cachegrind : https://valgrind.org/docs/manual/cg-manual.html
[8] PCov : https://github.com/krakjoe/pcov
[9] PHPUnit Code Coverage : https://phpunit.readthedocs.io/en/9.3/code-coverage-analysis.html