Prerequisite
- xdebug
- I installed xdebug with PECL
- vscode-php-debug
- I installed it in my home directory
git clone https://github.com/xdebug/vscode-php-debug.git && \ cd vscode-php-debug && \ npm install && npm run build
Plugins
- mfussenegger/nvim-dap → Debug Adapter Protocol client implementation for Neovim.
- theHamsta/nvim-dap-virtual-text → adds virtual text support to nvim-dap
- rcarriga/nvim-dap-ui → A UI for nvim-dap which provides a good out of the box configuration.
Those plugins have dependencies, so please go to the page and read the installation instruction. I’ll only share some config that relates directly with these plugins.
Configs
I use packer to maintain my neovim plugins (you can see my full config here), so to install the plugins mentioned above, here are the line that I included:
use {
"mfussenegger/nvim-dap",
}
use {
"theHamsta/nvim-dap-virtual-text",
requires = {
"mfussenegger/nvim-dap"
},
}
use {
"rcarriga/nvim-dap-ui",
requires = {
"mfussenegger/nvim-dap"
},
}
After installation is done, we need to configure the dap plugin,
-- just want to make sure that we have dap and dapui
local has_dap, dap = pcall(require, "dap")
if not has_dap then
return
end
local has_dap_ui, dapui = pcall(require, "dapui")
if not has_dap_ui then
return
end
dap.adapters.php = {
type = 'executable',
command = 'node',
-- change this to where you build vscode-php-debug
args = { os.getenv("HOME") .. "/vscode-php-debug/out/phpDebug.js" },
}
dap.configurations.php = {
-- to run php right from the editor
{
name = "run current script",
type = "php",
request = "launch",
port = 9003,
cwd = "${fileDirname}",
program = "${file}",
runtimeExecutable = "php"
},
-- to listen to any php call
{
name = "listen for Xdebug local",
type = "php",
request = "launch",
port = 9003,
},
-- to listen to php call in docker container
{
name = "listen for Xdebug docker",
type = "php",
request = "launch",
port = 9003,
-- this is where your file is in the container
pathMappings = {
["/opt/project"] = "${workspaceFolder}"
}
}
}
-- toggle the UI elements after certain events
dap.listeners.after.event_initialized["dapui_config"] = function()
dapui.open()
end
dap.listeners.before.event_terminated["dapui_config"] = function()
dapui.close()
end
dap.listeners.before.event_exited["dapui_config"] = function()
dapui.close()
end
dapui.setup()
Besides that, I also created key mappings to make life easier:
local function map(mode, lhs, rhs, opts)
local options = { noremap = true, silent = true }
if opts then
options = vim.tbl_extend('force', options, opts)
end
vim.keymap.set(mode, lhs, rhs, options)
end
map("n", "<F5>", require "dap".continue, {})
map("n", "<F10>", require "dap".step_over, {})
map("n", "<F11>", require "dap".step_into, {})
map("n", "<F12>", require "dap".step_out, {})
map("n", "<leader>b", require "dap".toggle_breakpoint, {})
map("n", "<leader>du", ":lua require'dapui'.toggle()<cr>", {})
-- you'll want this because we don't want xdebug to start automatically everytime
function insert_xdebug()
local pos = vim.api.nvim_win_get_cursor(0)[2]
local line = vim.api.nvim_get_current_line()
local nline = line:sub(0, pos) .. 'xdebug_break();' .. line:sub(pos + 1)
vim.api.nvim_set_current_line(nline)
end
map("n", "<leader>ds", "<cmd>lua insert_xdebug()<cr>")
Other than vim configs, we need to change some php configs as well.
I added this into /usr/local/etc/php/8.2/php.ini, but your config might be in different location depending on your php installation location. Sometimes installing xdebug will automatically add this below line to php.ini, so you might not need to add it.
zend_extension="xdebug.so"
I also added another configs related to xdebug in /usr/local/etc/php/8.2/conf.d/debug.ini (I created this file). If you want debugger to start immediately when you call php (read: if you’re too lazy to write xdebug_break()), then you should give yes value to xdebug.start_with_request (remove comment from below code). I prefer not to.
xdebug.mode=debug,trace
;; xdebug.start_with_request=yes
How to
Run the current script from editor
Let’s start with a very simple php file
<?php
$a = 20;
$b = 30;
function add($a, $b) {
return $a + $b;
}
$sum = add($a, $b);
echo $sum;
We can add breakpoint with <leader>b to tell the debugger where we want it to stop later on. Let’s say we insert breakpoint in these places:
Now we can start the debugging process, <F5> to start the debugging process. We’ll get this prompt, choose 1:
Then we’ll see the debugger ui
We can play around the UI. To continue press <F5>. And you can go over the process until all is done.
To make it clear, B on the gutter line is where our breakpoints are and -> indicates where the process is currently at.
Run the current script from terminal
Add xdebug_break() anywhere before your first breakpoint. We had mapped a key for this to make our life easier, <leader>ds
<F5> to start debugger, but this time, instead of 1 we’ll choose 2.
Notice the different in the UI. Debugging hasn’t started, so the only info that we have in the UI is only the breakpoint locations.
Then we can run the php script from our terminal:
Notice that it doesn’t directly return sum. It’ll wait until we finish debugging. Same way to continue, <F5>.
Too lazy to set it up, any other way?
Yes, of course, we can log it as usual. Here’s my favorite template!
fwrite(STDERR, print_r(PHP_EOL, true));
fwrite(STDERR, print_r('I am heeeeeeeeere! Notice me senpai <3!', true));
fwrite(STDERR, print_r(PHP_EOL, true));
fwrite(STDERR, print_r($params, true));
// for prettier print
// fwrite(STDERR, print_r(json_encode($params, JSON_PRETTY_PRINT)));
fwrite(STDERR, print_r(PHP_EOL, true));
// I usually add much more PHP_EOL at start and end :D
If we want to see trace of the error, we can also do something like this:
try {
$var = $your_function(
$params_1,
$params_2
);
} catch (Exception $e) {
error_log($e);
}