Debugging PHP in VSCode like a PRO!

Nikita Pavlovski
7 min readJul 29, 2023

Hi! I am Nikita Pavlovski. I’m a software engineering enthusiast.
Telegram & Github: @nikitades

And today I’m bringing a way to easily enable fully fledged step debugging of PHP in Visual Studio Code.

The Single IDE to rule them all

Ever since, I loved the idea of a singe IDE for all cases, which makes total sense to me:

  • a single set of hotkeys
  • a single structure of control elements, editable fields and menus, zero cost of language switch
  • setup once, use forever

Unfortunately, it’s much more profitable for some companies when you purchase a whole new IDE for every language out there. Sometimes even a couple of IDEs for a single language. But no more.

Regardless of whether you engage into polyglot programming or not, today we’ll try to make the best use of VSCode debugger for PHP. Because to modern day, PHP is no language to make an excuse for, when it comes to PHP and VSCode compatibility.

Xdebug as the primary tool

Elon Musk knows a thing or two about the “X”, right?

Some people say that if you don’t use Xdebug and instead debug with var_dump, die or dd then you might not be delving deep enough. To me, this has the grain of truth, however it’s always up to you. I only know that to debug with Xdebug is always more visible and also just faster in long perspective. So, we are about to setup the ultimate debugging stand.

Environment

  • Mac OS ARM
  • Zsh
  • PHP 8.2.8
  • VSCode 1.80.1
  • xdebug.php-debug VSCode extension
  • Homebrew 4.1.2
  • XDebug 3.2.2

My PHP installation is brought to me by Homebrew package manager. Your case might be different (Linux, Windows, different package manager, whatever), but the core concept would still be the same, so I’ll try to cover other setups’ details eventually.

The further part of the article assumes that

  • PHP is installed on your machine
  • XDebug extension is enabled (php -m shows “xdebug” line)
  • {PHP_ini_folder}/conf.d/99-xdebug.inifile exists (create one if missing, see “Starting Local PHP server with CLI and XDebug”, paragraph 2 for more details)
  • 99-xdebug.ini contains the following lines
xdebug.mode=debug
xdebug.client_host=0.0.0.0
xdebug.client_port=9003
  • php.ini configured in the fashion that ini files from conf.d directory are included
  • and this is the example PHP project to use here:
.vscode
launch.json
src
index.php

The content of index.php file could be literally anything that could be compiled, just make sure there is more than 1 line, so you can put the breakpoint on the line before the last one.

Starting Local PHP server with CLI and XDebug

This seems to be the simplest and the most handy way to start debugging with XDebug.

  1. We need to first find where .ini files live and create a comfortable way to attend this directory. Add these lines to the bottom of your .bashrc or .zshrc:

🍎 Mac OS

export PHP_VER_SHORT=$(php -r "echo substr(PHP_VERSION, 0, 3);")
export PHP_CONF_D=$HOMEBREW_PREFIX/etc/php/$PHP_VER_SHORT/conf.d

As you can notice, I refer to Homebrew PHP installation. If this is not your case, please check php --ini output files and pick the folder that contains your main php.ini file. The only required thing is the reliable path to the location of arbitrary ini files of your PHP version ($PHP_CONF_D).

🐧 Linux

Typical Ubuntu PHP installation brings one more layer of folders under the version slug: cli, php-fpm and possibly some others. Since this tutorial involves the local PHP server usage, we’ll stick with CLI:

/etc/php/8.1/cli/conf.d/99-xdebug.ini

Hence, .bashrc variables export will look like:

export PHP_VER_SHORT=$(php -r "echo substr(PHP_VERSION, 0, 3);")
export PHP_CONF_D=/etc/php/$PHP_VER_SHORT/cli/conf.d

Also, please note that VSCode has to be restarted in order to be aware of new shell profile variables. It’s not enough to just source ~/.bashrc .

2. Now let’s think of how to let PHP know whether it is time to add XDebug magic to the request or not.

ℹ️ What in fact are VSCode tasks? 🤔

Just another way to automate actions sequences. Simply putting, these are just CLI commands that are issued on a command from VSCode.

There are two types of tasks in VSCode: workspace-related and global ones. Having these actions in the global task would simplify the initial setup of every future PHP project, so let’s stick with global tasks.

Open the command palette of VSCode and type: Tasks: Open User Tasks . In the resulting json file, create two more tasks (root level objects) with the following content:

        {
"label": "Enable XDebug",
"type": "shell",
"command": "echo 'xdebug.start_with_request=yes' >> $PHP_CONF_D/99-xdebug.ini",
"presentation": {
"reveal": "never",
"clear":true,
"close": true
}
},
{
"label": "Disable XDebug",
"type": "shell",
"command": "sed -i.bu '/xdebug.start_with_request=yes/d' $PHP_CONF_D/99-xdebug.ini",
"presentation": {
"reveal": "never",
"clear": true,
"close": true
}
}

Please note that by default both launch.json and tasks.json are just JSON files that contain a fairly simple object, that mostly consists of an array of another objects: tasks or launch profiles.

Now these tasks are available at every project open with VSCode.

What do these tasks do? One thing basically, both directions. The first task adds xdebug.start_with_request=yes line to 99-xdebug.ini, and the next one just removes it.

ℹ️ Why 99?
PHP loads ini files consequently, alphabetically. Numbers are an explicit way to tell PHP which extensions should be loaded last. For instance, XDebug conflicts with OPcache, if loaded before. The “99” guarantees that XDebug is loaded in the end.

This way of managing a single setting turns XDebug on and off for every request that is served with your system installation of PHP.

3. Creating the launch pattern at VSCode

There’s a way to quickly enter and leave the debugging mode in VSCode: F5 hotkey by default. In case if there’s only one launch config then this config is about to be chosen by default. Otherwise, make sure that the proper launch config is chosen on the left:

Paste this code block to launch.json file of your .vscode directory in the project. This would create “Listen for XDebug” launch profile at VSCode debug panel.

    {
"name": "Listen for Xdebug",
"type": "php",
"request": "launch",
"preLaunchTask": "Enable XDebug",
"postDebugTask": "Disable XDebug",
"cwd": "${workspaceRoot}/src"
},

!Important! Make sure “cwd” parameter matches the directory of index.php file. This is the way for VSCode to understand that the certain bytes sequence is in fact a particular file.

Please notice that this launch pattern has “preLaunchTask” and “postDebugTask” keys. These keys are to be filled with the desired local or global task name. In the example these fields contain names of the tasks created at step 2.

4. Testing

Now, if everything was configured properly, you should have the following:

  • The basic PHP project with the launch profile and a simplistic index script
  • Global tasks to enable and disable XDebug in PHP

Next steps:

  1. Place the debugging breakpoint to any line of the code
  2. Launch the local server with php -S 0.0.0.0:8080 -t ./src (because the index script is located in src folder of the project)
  3. Press F5 to actually enter the debug mode at VSCode, and also to execute “Enable XDebug” task before it (remember to start the server before the task is executed)
  4. Open 0.0.0.0:8080 and see the breakpoint is caught by the debugger. Done! 🎉

Bonus: how to make VSCode start the server for us?

This is 90% the same as the Mode 1. The only difference is that we don’t start the server manually and instead VSCode takes care of it for us.

ℹ️ Is there any reason to not always enable XDebug?

Yes. XDebug adds a remarkable load of additional complexity to every PHP request. This results in a visible performance drop. So, if you just need to launch some project and not debug one, you might consider not turning XDebug on at every occasion.

It’s out of scope of current tutorial, but simply speaking there are two options: (1) to keep controlling XDebug mode manually and not use VSCode autostart, (2) to create one more VSCode debug launch profile which does not involve XDebug.

So, no more manual issuing of the command. At launch.json, add the following launch pattern:

    {
"name": "Launch Built-in web server",
"type": "php",
"request": "launch",
"runtimeArgs": [
"-dxdebug.mode=debug",
"-dxdebug.start_with_request=yes",
"-S",
"localhost:8080"
],
"program": "",
"cwd": "${workspaceRoot}/src",
"port": 9003,
"serverReadyAction": {
"pattern": "Development Server \\(http://localhost:([0-9]+)\\) started",
"uriFormat": "http://localhost:%s",
"action": "openExternally"
}
}

And don’t forget to pick the right launch patter at the debug pane:

And now it’s really simple: just press F5. The new tab would open by itself, and the debugger would instantly jump to the next step breakpoint.
Voila! ⭐️

The good thing about such mode is that every stdOut logging that would occur in your application will be displayed in the appropriate debug console tab. For instance, this line:

results in the following log line:

The same thing would happen with Monolog and other logging libraries that support stdOut logging.

Bright, isn’t it?

— — — — —

Please see the next part: Step debugging PHP: VSCode + PHP in Docker

See the example project to play yourself: https://github.com/nikitades/php-xdebug-example

--

--