Why var_dump() Falls Short for Real PHP Development
Most PHP developers begin their debugging journey with var_dump(). You locate the section where something appears wrong, insert a few var_dump() calls, reload the page, interpret the output, remove the statements once you believe the issue is resolved, and continue forward. This approach works adequately for straightforward problems where the output fits on a single screen and the bug sits close to where you are examining. It breaks down for everything else: nested arrays generating unreadable scrolling output, bugs that appear only under specific requests and not others, issues in code paths triggered by cron jobs or API calls rather than page loads, and logical errors where the script runs without crashing but produces incorrect results.
The fundamental issue with var_dump() debugging is that it requires modifying the code while you are investigating the problem. Adding and removing statements alters the file, which can introduce whitespace inconsistencies, syntax errors from hasty removal, or new logical bugs from incomplete edits. You are also limited to whatever information you explicitly dump, rather than having access to the full runtime state of the application.
Xdebug is a PHP extension that provides a proper debugger with step-through execution, variable inspection, watches, conditional breakpoints, and stack traces. Once you have debugged a PHP application with Xdebug and VS Code — setting breakpoints at the exact point where something goes wrong, stepping through execution line by line, watching variables change as you advance — the constraints of var_dump() become immediately apparent. You do not return to it for anything beyond the most trivial check.
How Xdebug Communicates With the Debugging Client
Xdebug operates as a client-server pair. The PHP script runs with Xdebug active, which listens on a configured port for connections from a debugging client. VS Code functions as the debugging client, connecting to Xdebug on the configured host and port. When a debugging session is triggered, Xdebug pauses the PHP execution and sends the current execution state to VS Code over this connection.
The communication happens over DBGp, a debugging protocol specifically designed for this purpose. Xdebug listens on client_host:client_port (default: localhost:9003) and waits for a debugger client to connect. When the client connects, Xdebug takes control of the PHP execution and reports the current call stack, variables, and breakpoints to the client.
The three main Xdebug modes are coverage, develop, and debug. Coverage instruments code to measure which lines are executed and is used by tools like PHPUnit for code coverage reports. Develop provides enhanced error messages and variable dumps without stopping execution. Debug pauses execution and communicates with a debugging client. You can also enable profiler output mode, which writes profiling data that tools like KCacheGrind can visualise for performance analysis.
The mode is set in php.ini. For development, use mode=debug. For production, never enable Xdebug — the performance impact is severe and the security risk of exposing the debugging interface to unauthorised users is significant.
Installing Xdebug on Ubuntu with PHP-FPM
On Ubuntu with PHP-FPM, install Xdebug via apt.
sudo apt update
sudo apt install php-xdebug
sudo systemctl restart php-fpm
On Ubuntu, the package installs Xdebug and enables it automatically in /etc/php/8.1/fpm/conf.d/20-xdebug.ini (the version number varies by PHP version). The module is loaded but the mode may not be set correctly. Check the configuration file to verify the settings.
cat /etc/php/8.1/fpm/conf.d/20-xdebug.ini
# Usually contains: zend_extension=xdebug.so without the mode settings
Add the configuration to set the correct mode and connection parameters. For debugging with VS Code, you need mode=debug, client_host set to the IP address where VS Code is running (localhost if on the same machine), and client_port set to 9003.
zend_extension=xdebug.so
xdebug.mode=debug
xdebug.client_host=127.0.0.1
xdebug.client_port=9003
xdebug.start_with_request=trigger
xdebug.idekey=VSCODE
The start_with_request=trigger setting means debugging only starts when a specific trigger is present in the request. This prevents every page load from launching a debugging session, which would quickly become overwhelming. The trigger is the XDEBUG_SESSION cookie or parameter. Without the trigger, Xdebug runs in develop mode and provides enhanced error output only.
If VS Code and PHP-FPM are on the same machine, client_host=127.0.0.1 is correct. If PHP-FPM runs in a Docker container and VS Code runs on the host, client_host should be the IP address of the host on the Docker bridge network, not localhost — Docker containers cannot connect to localhost on the host because localhost refers to the container itself. Find the host IP from inside the container with hostname -I | awk '{print $1}'.
# In the container, find host IP
hostname -I | awk '{print $1}'
# Use that IP as xdebug.client_host in the container's php.ini
After changing the configuration, restart PHP-FPM to apply the new settings, then verify Xdebug is loaded.
sudo systemctl restart php8.1-fpm
# Verify Xdebug is loaded
php -m | grep -i xdebug
Installing Xdebug on macOS with Homebrew PHP
Homebrew PHP installations include Xdebug as a tap. If you are using Homebrew's PHP, install Xdebug via the Homebrew php tap.
brew install php
brew install xdebug
Homebrew does not automatically configure Xdebug. You need to add the configuration to your php.ini. Find the php.ini location with php -i | grep "Loaded Configuration File".
php -i | grep "Loaded Configuration File"
# Output: /opt/homebrew/etc/php/8.3/php.ini
Add the Xdebug configuration to that file.
xdebug.mode=debug
xdebug.client_host=127.0.0.1
xdebug.client_port=9003
xdebug.start_with_request=trigger
Restart PHP if it is running as a service. For Homebrew, this is brew services restart php.
brew services restart php
php -m | grep xdebug
Installing Xdebug on Windows with XAMPP
XAMPP ships with Xdebug included but not enabled by default. Find the php.ini location in the XAMPP control panel (PHP Settings -> Edit php.ini). Look for the Xdebug section and uncomment or add the configuration.
zend_extension = C:\xampp\php\ext\php_xdebug.dll
xdebug.mode=debug
xdebug.client_host=127.0.0.1
xdebug.client_port=9003
xdebug.start_with_request=trigger
If XAMPP does not include the Xdebug DLL, download the correct version from xdebug.org. The download page asks for your PHP version. Use php -v to find your PHP version and download the matching DLL. Place it in C:\xampp\php\ext\ and reference it in the zend_extension path.
Restart Apache from the XAMPP control panel. Verify with phpinfo() or php -m from the XAMPP shell.
Configuring VS Code to Listen for Xdebug Connections
Install the PHP Debug extension by Felix Becker in VS Code. This extension implements the DBGp protocol that Xdebug uses to communicate with the debugging client. Search for "PHP Debug" in the Extensions panel and click Install.
After installation, create a launch.json file in the .vscode directory of your project. Open the Debug panel (Ctrl+Shift+D on Windows/Linux, Cmd+Shift+D on macOS), click "create a launch.json file", and select PHP. VS Code generates the basic configuration for you.
{
"version": "0.2.0",
"configurations": [
{
"name": "Listen for Xdebug",
"type": "php",
"request": "launch",
"port": 9003,
"pathMappings": {
"/var/www/html": "${workspaceFolder}"
}
}
]
}
The pathMappings entry is critical when your web server document root is on a different path than your VS Code workspace. If your project is at /home/user/project on the local machine but the web server serves from /var/www/html, the pathMappings maps /var/www/html (the remote path PHP sees) to ${workspaceFolder} (the local path VS Code works with). Without this mapping, VS Code cannot find the source files to display them when a breakpoint is hit.
If PHP runs inside Docker, the pathMappings should point to the container's filesystem path, not the host path. If your container maps /home/user/project to /var/www/html inside the container, the pathMappings entry should be "/var/www/html": "/home/user/project". When setting up development environments with containers, ensuring the paths align correctly between your IDE and the container prevents many common debugging frustration points.
Click the green play icon next to "Listen for Xdebug" to start listening. VS Code shows "Listening for Xdebug" in the debug toolbar. When a PHP request triggers a debugging session, VS Code activates and shows the paused execution with full access to variables, the call stack, and debugging controls.
Triggering a Debugging Session
With start_with_request=trigger, the debugging session does not start automatically on every request. You need to pass the XDEBUG_SESSION trigger in the request. There are three ways to do this.
The first method is adding ?XDEBUG_SESSION_START=VSCODE as a query parameter to the URL. For example, http://localhost/index.php?XDEBUG_SESSION_START=VSCODE. The XDEBUG_SESSION_START parameter with any value triggers the debugging session. The idekey (VSCODE in our configuration) tells Xdebug which debugging profile to use.
The second method is setting a cookie with the same name in your browser. The Xdebug browser extension (available for Chrome and Firefox) adds this cookie when you click its icon to enable debugging for a site. Once the cookie is set, all requests to that domain trigger a debugging session without needing the query parameter.
The third method is using the Xdebug helper icon in your browser toolbar. Click it to enable debugging for the current site, which sets the cookie. Click it again to disable. This is more convenient than adding query parameters for every request when you are actively debugging a particular feature.
If VS Code is not listening when a debug session is triggered, Xdebug waits for a connection for xdebug.connect_timeout_ms milliseconds (default: 200ms) then times out and continues execution without debugging. Increase the timeout or ensure VS Code is listening before triggering the session to avoid unexpected timeouts.
Setting and Managing Breakpoints
Click in the margin next to any line number to set a breakpoint. A red circle indicates an active breakpoint. Breakpoints pause execution before the line executes, giving you the chance to inspect the application state at that precise moment.
Right-click on a breakpoint to set conditions: the debugger only pauses at that breakpoint when the condition is true. This is useful for debugging loops where you only want to pause when a specific iteration occurs or when a particular data state exists.
Conditional breakpoints accept PHP expressions. Set a condition of $user->id === 42 to only pause when the user ID is 42. Set a condition of $total > 1000 to only pause when the total exceeds a threshold. Conditional breakpoints are one of the most useful features that var_dump() debugging cannot replicate.
Logpoints are breakpoints that do not pause execution but instead output a message to the debug console. Use logpoints to log when a code path is executed without stopping. Set a logpoint with the message "User {$user->name} accessed resource {$id}" and it outputs every time the line is hit without pausing. This is particularly useful for tracing execution flow without interrupting the application.
Function breakpoints pause execution when a specific function is called. Set a function breakpoint on "my_function" and execution pauses whenever that function is invoked, regardless of where the call originates. This is useful for finding where a specific function is called from without searching through code manually.
Stepping Through Code and Using the Debug Controls
The VS Code debug toolbar provides five primary controls. Continue (F5) runs execution until the next breakpoint is hit or the script completes. Step Over (F10) executes the current line and pauses at the next line in the same function. If the current line calls another function, Step Over runs the entire function and pauses at the line after the function call.
Step Into (F11) does the same as Step Over except it enters the called function and pauses at the first line of that function. This lets you dive into function calls to understand their internal behaviour. Step Out (Shift+F11) runs the rest of the current function and pauses after the function returns, letting you exit a function you have finished examining. Restart (Shift+F5) ends the current debugging session and starts a new one. Stop (Shift+F5) ends the session and disconnects from Xdebug.
The most common workflow is to set a breakpoint at the entry point of the area you suspect contains the bug, trigger a debug session, step through the code with Step Over, and watch variable values change as you step. When you reach a function call that might be the source of the problem, Step Into to enter that function. When you have verified a function is working correctly, Step Out to return to the caller.
The Call Stack panel in VS Code shows the chain of function calls that led to the current execution point. Click any frame in the call stack to view the variables and execution context of that point in the code. This is useful for understanding how execution arrived at a specific point — what called what, with what arguments.
The Variables panel shows local variables and their current values at the paused execution point. Expand arrays and objects to inspect their contents in detail. Right-click on a variable and select "Copy Value" to use in the Watch panel or in a logpoint message.
Using Watch Expressions to Track Variables Across Steps
The Watch panel lets you monitor specific variables or expressions throughout a debugging session. Add a variable by clicking the + icon and typing the variable name. Add an expression by typing an expression like $user->orders->total. The Watch panel evaluates the expression at each breakpoint and shows its current value.
Watch expressions persist across breakpoints, so you can track a variable's value as you step through many lines of code. This is particularly useful for understanding how a value changes across a loop or how a complex object accumulates state across multiple function calls.
Expressions in the Watch panel support PHP syntax. You can watch $result > 0 to see whether a condition is true at each step, or $items to watch the last element of an array as it changes. When debugging production issues, having a set of watch expressions ready can significantly reduce the time needed to diagnose complex problems.
Debugging CLI PHP Scripts
Xdebug works for command-line PHP scripts as well as web requests. For CLI scripts, the XDEBUG_SESSION environment variable triggers the debugging session. Export XDEBUG_SESSION before running the script.
export XDEBUG_SESSION=VSCODE
php my-script.php
Or use the -d flag to pass the Xdebug configuration directly to PHP without modifying your php.ini.
php -d xdebug.mode=debug -d xdebug.client_host=127.0.0.1 -d xdebug.start_with_request=trigger my-script.php
This is useful for debugging cron jobs, artisan commands, and PHP scripts that run from the command line rather than via a web server. Setting up CLI debugging is particularly valuable when you are building automated workflows or background processing tasks.
Common Xdebug Configuration Problems and Fixes
The most common Xdebug configuration problem is a connection timeout. If Xdebug cannot connect to the debugging client within the timeout period, it abandons the connection and execution continues. This happens when the client_host is wrong (pointing to the wrong machine), the port is blocked by a firewall, or the debugging client is not listening when the request is made.
Check that client_host in php.ini matches the IP address of the machine running VS Code. On macOS or Linux, if VS Code is on the same machine as PHP, 127.0.0.1 is correct. In Docker, use the host's IP on the Docker bridge network, not localhost. Use getenv('HOST_IP') in your Docker startup script to pass the host IP to PHP's environment.
Another common problem is multiple PHP installations. On Ubuntu with Apache and PHP-FPM, apt install php-xdebug installs the CLI version's Xdebug but not necessarily the FPM version. If you enable Xdebug in php.ini for the CLI but not for FPM, web debugging does not work even though CLI debugging does. Check which php.ini each SAPI reads.
# Check CLI php.ini location
php --ini
# Check FPM php.ini location
php-fpm8.1 -i | grep "Loaded Configuration File"
# Edit the correct file for each SAPI
A third common problem is that Xdebug 3 changed the configuration parameter names. If you are using xdebug.remote_host and xdebug.remote_port from older tutorials and debugging does not connect, those parameters were renamed in Xdebug 3. Use client_host and client_port instead. The old parameter names no longer work.
Debugging PHP-FPM Requests with VS Code
When PHP runs under PHP-FPM, the connection works slightly differently. The web server spawns a PHP-FPM worker process to handle the request. The Xdebug extension in that worker process tries to connect back to the debugging client. The debugging client must be listening on the configured host and port before the request is made.
If VS Code is on the host and PHP-FPM is in a Docker container, the container's PHP process connects to the host's IP, not to localhost. Set client_host in the container's php.ini to the host's IP address. This is the most common reason debugging fails for Docker-based PHP setups — the container cannot reach localhost on the host.
# From inside the container, find the host gateway
ip route | grep default | awk '{print $3}'
# Use that IP as xdebug.client_host in container php.ini
PHP-FPM also has a setting that can block Xdebug. The security.limit_extensions setting in php-fpm.conf restricts which file extensions PHP-FPM will process. If Xdebug is not loading for .php files, check that security.limit_extensions = .php is not restricting PHP to specific file extensions that exclude your files.
When to Use Xdebug Instead of var_dump()
Use var_dump() for one-off checks in known small contexts. When you want to quickly verify a variable's type or a single value, adding var_dump($variable) and reloading is faster than setting up a debugging session. This is appropriate for simple checks in development when you know exactly where to look.
Use Xdebug for everything else. Bugs that require understanding the execution flow, bugs that involve complex state spread across multiple variables, bugs that only appear in specific conditions, logical errors where the code runs without crashing but produces wrong results — all of these are solved properly with the debugger, not with var_dump(). The time invested in setting up Xdebug pays back immediately on the first complex bug it helps you solve.
Setting up a proper staging environment alongside your debugging workflow can help you reproduce issues more reliably. A staging server setup lets you test changes in an isolated environment before applying them to production, which pairs well with systematic debugging practices.
Building a Reliable PHP Debugging Workflow
Setting up Xdebug with VS Code is a worthwhile investment for any PHP developer. The ability to pause execution, inspect variables, and step through code transforms how you approach debugging. Rather than guessing what is happening based on scattered output, you see the actual state of your application at every step.
Once you have Xdebug configured and working, take time to explore the less obvious features. Conditional breakpoints save hours when debugging loops or repeated operations. Logpoints let you trace execution without modifying code. The Watch panel helps you track complex object state across function calls. These features become natural once you understand them, and they make debugging significantly faster.
If you are working on PHP projects that involve error handling and monitoring in production, it is worth complementing your debugging setup with proper logging practices. Understanding both local debugging and production error logging and monitoring gives you a complete picture of how your application behaves at every stage.
For projects that involve automated testing and deployment, combining Xdebug with a CI/CD pipeline creates a robust development workflow. When you can debug issues locally and have tests run automatically on each commit, you catch problems earlier and deploy with more confidence. A properly configured GitHub Actions CI/CD pipeline for PHP supports this workflow well.