Why cannot you set up Xdebug?
Abstract
Most users experience hardships with setting up Xdebug in their development environments. This problem has deteriorated for two reasons:
- Insufficient understanding of Xdebug work
- An overload of ambiguous and inaccurate google-answers
As long as the second reason is hard to affect, this article focuses on reducing the impact of the first reason. So the goal is to explain how Xdebug works under the hood, how an IDE interacts with Xdebug, and how a web browser side has to function. The narrative format of the article will help systematize your knowledge in an orderly manner. As a result, the acquired knowledge will lead to reduction of efforts on setting Xdebug up in an unfamiliar environment.
In addition, you will gain insights into the following related questions: What is DBGp Proxy Tool, and why you should not use Xdebug Cloud. You won't gain any information about installing the Xdebug extension or using the debugger.
Problem area
Setting up Xdebug requires an interaction with several components in the development environment — a client, a server, an IDE. The number of components and their potential working environment lead to complications in a configuration process. Understanding how Xdebug works will reduce efforts on setting up Xdebug in any development environment.
Configuration examples are presented only for Xdebug version 3.
Operational principle
Let's start with PHP. In some perspective PHP applications could work in two modes:
-
The Web mode — handling incoming HTTP requests from a browser
Examples of stacks working with the mode: php cgi, nginx+fpm, nginx unit
-
The CLI mode — direct execution from a terminal
Examples of stacks working with the mode: roadrunner, phpunit, horizon
Xdebug works the same in these two modes. But we are going to examine them separately for clarification.
The Web mode
It is assumed that a web server has already started and ready to handle HTTP requests. An illustration of HTTP request path from a client to PHP looks like the one below:
...
— means the presence of intermediate nodes that take place in the Internet. They may not exist in a local development.
Xdebug injects to that flow (note that the extension tries to connect back to the client):
Let's examine this process step-by-step in more detail:
In that context, the client is a local host. It has a running browser that sends an HTTP request and an IDE that listens to incoming connections from Xdebug.
- The client sends the HTTP request with a trigger which means that this request must be debugged.
- The PHP script is started, then it receives and handles the request.
- The PHP engine during startup hooks runs the Xdebug extension.
- Checking the presence of the trigger is in progress.
- A client's address is being searched.
- Attempts are being made to establish a connection with the client.
- Debugging launch: transfering control to the client, communication, waiting commands from the client.
- The end of debugging due to a stop command from the client or a finish of script execution.
The trigger
The HTTP request must contain a special trigger to initialize the Xdebug session on the server side. The trigger is a simple key+value pair: XDEBUG_TRIGGER=<any value>
(the legacy version is still working XDEBUG_SESSION=<any value>
). This trigger must be a part of the HTTP request from one of the possible options:
- Cookie
- Query param
- Post payload
A popular browser extension Xdebug Helper uses the Cookie approach to transfer that trigger.
The xdebug.trigger_value
setting allows to configure the exact value for activation the debugging process. By default, Xdebug accepts any non-empty value.
The Xdebug hook and the trigger checking
The Xdebug extension integrates with the PHP lifecycle. So when the PHP engine starts, the Xdebug startup hook checks the presence of the trigger. If there is no trigger, the Xdebug extension doesn't attempt establishing a connection and PHP continues to work as usual.
The search for a client's address and establishing a connection
If there was a positive check, the Xdebug extension starts to search for the client's address and establish a connection with it. The success of this step is the established connection with a client's IDE.
The search logic is simple. Just iterate (by trying to connect) all potential clients' addresses until the first succeed connection has been found. Here is an example of the workflow:
- Xdebug tries to connect to the address from the environment variable
HTTP_X_FORWARDED_FOR
- Xdebug tries to connect to the address from the environment variable
REMOTE_ADDR
- (fallback) Xdebug tries to connect to the address from the Xdebug configuration (by default, it is
localhost
)
If all attempts have failed, PHP would go on as usual (without debugging).
By default, checking addresses from environment variables is disabled, and Xdebug only looks at the fallback address. Enabling this option frees you of hard coding the client's address.
The described workflow uses this configuration:
; This is overridden behavior. By default, it is disabled
xdebug.discover_client_host = true
; This is used as the fallback address
xdebug.client_host = localhost
; Xdebug always connects to an IDE on this port
xdebug.client_port = 9003
The communication with the client
DBGp protocol aims to form communication between the Xdebug extension and the IDE. As a result, the familiar features like breakpoints, stacktrace, variable inspection are worked in the IDE.
At that step, the interactive debugging process has started for the client.
CLI mode
CLI applications are run from a console. An illustration of injecting Xdebug in the CLI mode looks like the one below:
The communication is similar to the Web mode but with a few distinctions:
-
The trigger
XDEBUG_TRIGGER
must be passed as an environment variable to initialize the Xdebug session. -
Checking addresses from environment variables feature (from the Searching client's address section) won't work in that mode. The
xdebug.client_host
setting has to be overridden with the client's address.
Here is an example of starting PHP in the CLI mode with the enabled Xdebug extension:
IDE
The main task of the IDE is to accept the Xdebug connection and provide an interactive debug process for users. But also the IDE has to deal with two nontrivial issues — a file mapping and working with multiple projects.
File mapping
What is a file mapping issue? The absolute path to the file containing code is used in actions such as the 'Step over' command or when a user sets breakpoints. These paths will differ between the IDE and the Xdebug extension if the code is being executed in an isolated environment, like a virtual machine, a container or a remote server.
The Xdebug extension can only work with its paths and cannot control the file mapping itself. Instead, the IDE performs the correct file mapping between the local and remote paths.
Here's an example of how the IDE substitutes these paths to maintain proper communication with the Xdebug extension (based on user settings):
Local host path | /Users/macos/my-project/index.php |
Remote host path | /home/vagrant/my-project/index.php |
Multiple projects
Nowadays, developers often work on multiple projects at the same time. How to achieve automatic routing of incoming Xdebug requests to the right project?
This problem is fully solved in PhpStorm and partially in Visual Studio Code. In the next section, the description will be provided.
PhpStorm integration
PhpStorm's logic uses fuzzy criteria to determine the correct routing. In general, it consists of two steps:
-
Getting values from environment variables that has been set at start of the PHP script. PhpStorm iterates through them until the first non-empty has found:
PHP_IDE_CONFIG
SERVER_NAME
andSERVER_PORT
SSH_CONNECTION
The found value is used as an identifier of the project.
If all environment variables have empty values, an interactive debug process in not possible.
-
Searching for the right project from in the project settings
Settings
/PHP
/Servers
. If there is no match, PhpStorm adds a newServer
based on the calculated identifier.
As a result, created Server
will be used for automatic routing and correct file mapping.
Visual Studio Code integration
The popular PHP Debug Adapter for Visual Studio Code brings all usual debugging processes and file mapping via pathMappings
setting in the configurationlaunch.json
, except for working with multiple projects.
The current solution assumes that only one project at a time can listen to Xdebug connections. For that reason, a user has to manually change a project that will be debugged.
Proxy
This section is dedicated to the solutions (that collaborate with Xdebug) that can enhance a development environment. The content will be accompanied by examples of usage the solutions with PhpStorm.
The proxy allows the establishment of the connection from the debugger to the IDE through an intermediate node. That approach makes it possible to:
- Easily configure the Xdebug extension
- Bypass network restrictions
- Debug an application by multiple users
DBGp Proxy Tool
DBGp Proxy Tool is a utility that runs on a node. It provides two features: partially coping with NAT and allowing multiple users to debug an application simultaneously (on a shared stage, for example).
Setting up is divided into two parts: configuration on the server side and configuration on the client side. Here is the server side configuration:
-
Xdebug establishes a connection with DBGp Proxy Tool. So the Xdebug configuration must have the explicit address of DBGp Proxy Tool specified in the
xdebug.client_host
setting. -
DBGp Proxy Tool starts on the node and listens to new connections from the Xdebug extension and the IDE.
Here is the client side configuration:
-
The client's IDE sends a request to DBGp Proxy Tool. This request contains an IDE key — a unique identifier for DBGp Proxy Tool that used for routing the Xdebug session to the right client.
-
A user prepares the IDE key on the browser side and initiates debugging of an HTTP request. The trigger with the specific value must be set —
XDEBUG_TRIGGER=Bob
.Under the hood
XDEBUG_TRIGGER=Bob
will be sent as Cookie.
When the setup is completed, DBGp Proxy Tool will route the Xdebug session to the client's IDE using the specified IDE key from the client's HTTP request.
DBGp Proxy Tool establishes new connections to the IDE for each new incoming Xdebug request. This implies that the connection from DBGp Proxy Tool to the client must not be affected by network restrictions.
Xdebug Cloud
Xdebug Cloud is a commercial product. It simplifies significantly the installing process. Moreover, it affords to establish the Xdebug session between nodes behind NAT and firewalls. Multi-user usage is also supported.
The setting up workflow is small:
-
Get the Cloud ID (register + pay)
-
Put the Cloud ID into the Xdebug configuration:
-
Put the Cloud ID into the configuration of PhpStorm.
That's it. This is the easiest way to set up Xdebug in any environment! But...
Be aware!
The connection between the PHP engine and Xdebug Cloud is not secured. The DBGp protocol allows to execute any PHP code. So, in case of man-in-the-middle attack, an attacker can gain a full access to your system (issue link).
For that reason use Xdebug Cloud at your own risk!
Troubleshooting
This troubleshooting is focused on covering basic steps that can help in most cases. Comprehending the information posted above is highly necessary for following the troubleshooting.
How to diagnose a problem?
If Xdebug does not connect to an IDE, follow this checklist to diagnose a problem:
-
Check that the Xdebug extension is installed for both CLI and Web modes:
-
Check the actual configuration of Xdebug. The CLI and Web modes use their own configurations. This means that changes must be made twice, once for the CLI mode and once for the Web mode:
-
Pay attention on these options:
xdebug.mode = debug ; For Step Debugging mode, always use 'debug' xdebug.start_with_request = default ; For Step Debugging mode, always use 'default' xdebug.client_port = 9003 ; Use this port as the default xdebug.discover_client_host = ; Depending on your environment, it could be 'true' or 'false' xdebug.client_host = ; If the previous setting is 'false', then type the client's address here xdebug.idekey = ; Leave the empty value if you don't use DBGp Proxy Tool
-
The changes must be made in
.ini
files. Use these commands to detect their locations:Keep in mind to restart PHP-FPM after the changes have been made.
-
-
Check if the client's address is correct:
-
In a Docker environment on Mac or Windows, the only solution is:
-
In a Docker environment on Linux or a virtual machine in any operational system (with host only network), the auto discover feature works fine:
-
On localhost or on a remote server with an open SSH connection to it:
-
-
Inspect a log file of the Xdebug extension:
-
Inspect a log file of PhpStorm:
- Open: Help / Diagnostic Tools / Debug Log Settings
- Add line and save:
#com.jetbrains.php
- Checkout the log file: Help / Show Log in Finder/Explorer/Navigator
How to set up PhpStorm?
PhpStorm offers a variety of features for working with PHP, including debugging. However, there is a set of settings that have to be configured to achieve the full experience.
-
Ensure that one of the environment variables (from the PhpStorm integration section) is set on the server side.
- If
SERVER_NAME
orSSH_CONNECTION
are not set (as is often the case in a Docker environment), usingPHP_IDE_CONFIG
becomes the only appropriate solution. - The
PHP_IDE_CONFIG
variable must adhere to the following pattern:serverName=<SomeName>
.
- If
-
Add your PHP interpreter in following section:
Settings
/PHP
/CLI interpreter
. - Additionally, add file mapping in the same section.
- The
Server
record must be provided in theSettings
/PHP
/Server
section. PhpStorm creates it for you after the first attempt of the Xdebug session. - Additionally, add file mapping for the created
Server
.
As a result, you can run CLI scripts with debugging, execute tests with execute, always listen for PHP Debug connections and expect debugging in the correct project.
Debugging an SPA-application
Mostly, backend and frontend applications are located at different addresses. In that case, the browser extension Xdebug Helper doesn't send the debugging trigger to the backend. The main solution is to use the same domain for both applications.
-
Edit the configuration file of the frontend application's web-server. If webpack is used, then modify
webpack.config.js
: -
Update the backend address to match the frontend application address on the client (typically by modifying the
BACKEND_URL
variable in the.env.local
)
Now, all request with the /api
prefix will be proxied to the backend. the XDEBUG_TRIGGER
trigger will be sent as a Cookie.
Debugging in other environments
Check out Ultimate Guide from JetBrains. There are a few cases of debugging in nonstandard environments (like via SSH tunnel). Also, you can find more solutions to real problems there.
Check out Step Debugging on the official Xdebug website.