Debugging PHP App in NeoVim with Launch Configuration
By: AjiYakin | #Development #Documentation
Sometimes I need to debug PHP code in two different situations: a CLI (command-line) app and a web server app. While the way to run each one is slightly different, the steps are subtle, and I often forget the correct order for each case.
NeoVim Plugin
- mfussenegger/nvim-dap
- kristijanhusak/vim-dadbod-ui
Project Directory Structure
Assuming we have this project directory structure:
src
└── App.php -- This is the entry point for CLI app
index.php   -- This is the entry point for Server app
composer.json
flake.nix   -- Nix flake file (NixOS)
.user.ini   -- PHP ini configuration
.vscode
└── launch.json -- Launch configuration
I am currently using Nix package manager to setup environment for each project that I have. With nix it is much more convenient to setup PHP environment since I can enable and include necessary PHP extension that I need to have, in this case I need to have xdebug extension.
flake.nix:
{
  description = "debugphp";
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
    nix-shell.url = "github:loophp/nix-shell";
    systems.url = "github:nix-systems/default";
  };
  outputs =
    inputs@{
      self,
      flake-parts,
      systems,
      ...
    }:
    flake-parts.lib.mkFlake { inherit inputs; } {
      systems = import systems;
      perSystem =
        {
          config,
          self',
          inputs',
          pkgs,
          system,
          lib,
          ...
        }:
        let
          php = pkgs.api.buildPhpFromComposer {
            src = inputs.self;
            php = pkgs.php83; # Change to php56, php70, ..., php81, php82, php83 etc.
          };
        in
        {
          _module.args.pkgs = import self.inputs.nixpkgs {
            inherit system;
            overlays = [ inputs.nix-shell.overlays.default ];
            config.allowUnfree = true;
          };
          devShells.default = pkgs.mkShellNoCC {
            name = "php-devshell";
            buildInputs = [
              php
              php.packages.composer
              pkgs.phpunit
            ];
          };
          apps = {
            # nix run .#composer -- --version
            composer = {
              type = "app";
              program = lib.getExe (
                pkgs.writeShellApplication {
                  name = "composer";
                  runtimeInputs = [
                    php
                    php.packages.composer
                  ];
                  text = ''
                    ${lib.getExe php.packages.composer} "$@"
                  '';
                }
              );
            };
            # nix run .#phpunit -- --version
            phpunit = {
              type = "app";
              program = lib.getExe (
                pkgs.writeShellApplication {
                  name = "phpunit";
                  runtimeInputs = [ php ];
                  text = ''
                    ${lib.getExe pkgs.phpunit} "$@"
                  '';
                }
              );
            };
          };
        };
    };
}
composer.json:
{
    "name": "ajiyakin/debugphp",
    "description": "Sample project to demonstrate how to debug PHP in NeoVim",
    "type": "project",
    "require": {
        "guzzlehttp/guzzle": "^7.9"
    },
    "require-dev": {
        "phpunit/phpunit": "^8.5",
        "ext-xdebug": "*",
        "phpunit/php-code-coverage": "^7.0"
    },
    "license": "MIT",
    "autoload": {
        "psr-4": {
            "Ajiyakin\\Debugphp\\": "src/"
        }
    },
    "authors": [
        {
            "name": "AjiYakin",
            "email": "ajiyakin91@gmail.com"
        }
    ]
}
.vscode/launch.json:
{
  "$schema": "https://raw.githubusercontent.com/mfussenegger/dapconfig-schema/master/dapconfig-schema.json",
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Listen for Xdebug",
      "type": "php",
      "request": "launch",
      "port": 9003
    },
    {
      "name": "Built-in Server with Xdebug",
      "type": "php",
      "request": "launch",
      "runtimeArgs": [
        "-S", "localhost:8080"
      ],
      "port": 9003
    }
  ]
}
.user.ini (per-directory php ini configuration file):
xdebug.mode=debug
xdebug.client_host=0.0.0.0
xdebug.client_port=9003
xdebug.start_with_request=yes
xdebug.idekey=NEOVIM
For CLI App
Here is running order for CLI App:
- Add breakpoint in App.php
- Run the Listen for Xdebug from launch configuration
- Run the cli with command: XDEBUG_CONFIG="idekey=NEOVIM" php -c .user.ini ./vendor/bin/phpunit src/App.php
For Server App
It is much more simple to run server app:
- Add breakpoint in index.php
- Run Built-in Server with Xdebug
- Trigger breakpoint by sending request to the corresponding routing/endpoint.
Notes
I am not sure why the server is not shutting down when I stop the debugger for server app, I will need to figure out later, but a workaround for this is to manually kill the server with this command:
sudo kill -9 $(lsof -t -i tcp:8080)