Skip to content

How I Debug php with Neovim

Posted on:June 12, 2023 at 10:00 PM

Prerequisite

Plugins

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);
}