Debugging php cli-scripts from a docker container in NetBeans

Этот пост был опубликован мной более года назад. Информация, описанная ниже, уже могла потерять актуальность, но всё ещё может быть полезна.

Hello. Recently I made a script to replace my real php interpreter in NetBeans settings.

As you probably know NB can’t run dockerized php cli scripts. here I will show you how I solved this problem. Now instead of using terminal I just press Run or Debug button in NB GUI to start my session. AFAICS this solution is pretty good.

I started from something really simple:

#!/bin/bash
docker exec test-php php \
    -dxdebug.mode=debug \
    -dxdebug.start_with_request=1 \
    `basename ${BASH_ARGV[0]}` \
    "${@:1:$#-1}"

But this is not universal and does not take into account all the parameters I can set in ‘Project Properties’ > ‘Run Configurations’:

After some time I rewrote this script and new helper was born. NB call it like it is real local php interpreter but my helper builds and calls a docker exec command. I wrote it in bash to be *nix-agnostic. I could make a plugin for NB but I’m too lazy to learn its structure, ecosystem and things.

How to setup this helper?

Let’s assume that you already have a container with php and xdebug running right now. If not, please refer to google, I won’t cover this here.

The helper file is called php for a reason: NB does not allow you to specify a different path to an interpreter in its settings. Since mine is needed to replace the real with dockerized, then its name must be appropriate. Keep this in mind and don’t get confused.

Below I have described these steps in great detail, but you should not be afraid — everything is very easy. There’s just a lot of text.

Variant 1: local

  1. Save the helper file inside nbproject directory of your project. For example:

    /home/user/bla/nbproject/php

    where php is helper file you downloaded.
  2. Make sure it is executable. If not, then run:

    chmod a+x /home/user/bla/nbproject/php
  3. Open NetBeans. Select ‘Customize…’ in the ‘Set project configuration’ dropdown on the ‘Run’ toolbar.
  4. Set ‘Run As’ to ‘Script (run in command line)’.
  5. Opt out the ‘Use default PHP Interpreter’ checkbox.
  6. In ‘PHP Interpreter’ field type the path from step 1 without using the ‘Browse’ button (NB hides nbproject in its ‘Browse files’ dialog).

Note that next steps 7-9 are not necessary to perform if you’re customizing <default> because then these parameters will cover all configurations, not only one currently you customizing. Configurations differs only in parameters described in steps 4-6. Create new configuration to set your own options per script.

  1. In ‘PHP Options’ add the following argument:

    --container=<name>

    where <name> is name of a running container with php and xdebug. If there are some other options were typed in this field earlier then just add this new one.

Yes, equal sign is required. Everything between it and closest space or till the end will become a container name. You can find this name in output of docker ps or in docker-compose.yml of your project.

No, this is not real php argument. This arg is only processed only by my helper to build correct docker exec command. It is required.

  1. To enforce xdebug connection from container to your IDE just add these argument in step 7:

    -dxdebug.mode=debug
    -dxdebug.start_with_request=1

    All xdebug settings are listed here.
  2. If your project contains subdirectory with php sources and this dir is mounted to your container then you should also add this argument:

    --map=<local_path>:<docker_path>

    where:
    <local_path> is an absolute path to you local directory with php sources;
    <docker_path> is an absolute path to the same directory inside of a container.

Yes, equal sign is required. Everything between it and closest space or till the end will become a value.

Yes, colon sign is required. It separates two paths inside a value. If a value, colon, left or right part is missed an error will occur.

No, this is not real php argument. This arg is only processed only by my helper to build correct docker exec command. It is NOT required.

  1. When called, helper prints some useful info before php being executed. To hide this messages you should add this arg:

    --quiet

No, errors will not be muted.

No, this is not real php argument. This arg is only processed only by my helper. It is NOT required and controls helper’s output.

  1. Provide other settings of your need in this window and save configuration.

Variant 2: global

  1. Save the helper file anywhere you want. For example:

    /home/user/php

    where php is helper file you downloaded.
  2. Make sure it is executable. If not, then run:

    chmod +x /home/user/php
  3. Open ‘Tools’ > ‘Settings’ > ‘PHP’ > ‘General’.
  4. In ‘PHP Interpreter’ field type the path from step 1.
  5. Follow steps 3-4 and 7-11 from variant 1.

How to catch xdebug breakpoints?

  1. Make sure you completed step 8 from variant 1.
  2. Choose a run configuration you customized for debugging.
  3. Set a breakpoint in any php file that will perform when your script will be executed.

I’m ready, what’s next?

The ‘Debug Project’ button on ‘Run’ toolbar is waiting for you. NB will immediately launch a script that initiates an xdebug connection from the container with the script, and will start listening to the xdebug port, waiting for this incoming connection.


How this helper works?

Here is a screenshot of configuration properties window:

In their order:

  1. Type of execution.
  2. When the checkbox is unchecked you can provide another interpreter.
  3. Absolute path to custom interpreter. Must be correct and ended with php. That’s why my helper is called ‘the same name php. Same constraint applies in NB settings.
  4. Path to php script inside of you project.
  5. Arguments for this script.
  6. Working directory. Normally it will become as current pwd for php process. But in out context it is unnecessary, useless, doesn’t count and doesn’t matter.
  7. Arguments for php (not script!). There you can override some of php.ini settings for example.

When you run this configuration NB will build a such string (I splited it here for our comfort):

"/home/anthony/projects/nb-test/nbproject/php" \
     "--container=test-php" \
     "--map=/home/anthony/projects/nb-test/app:/var/www" \
     "-dxdebug.mode=debug" \
     "-dxdebug.start_with_request=1" \
     "/home/anthony/projects/nb-test/app/index.php" \
     "arg1" \
     "arg2"

Let’s TACL on this parts separately:

  1. Path to my helper as it were real php interpreter. This path wil be taken from this configuration or NB settings depending on ‘Use Default PHP Interpreter’ checkbox.
  2. Helper’s arg with name of my docker container.
  3. Helper’s arg with directory mapping. This is how helper understand that container contains not full project but its subdir with sources. So, helper must call script using mapped path, not container’s current WORKDIR.
  4. xdebug arg that makes breakpoints available for us to catch.
  5. xdebug arg that forces xdebug to connect to our IDE.
  6. Corrected absolute path to script file inside of my container.
  7. Test arg for php script.
  8. Test arg for php script.

Note the order of parts of command: interpreter => its args => file => its args.

This string is compiled and called by NB. But I need completely different one:

docker exec \
    <container_name> \
    php \
    [<php_args>] \
    <script_file> \
    [<script_args>]

I decided to make such transformations in a simple way. Taking this command as an space-splited array of substrings, I went through them in a loop. Collecting all the pieces of the command along the way, I check whether the next piece is a file existing on the disk. If yes, then I transform its path (as described below) and collect the remaining flags at the end. Along the way, I also attach the name of the container and the path map, which directly affects the conversion of the path to the script.

Nuance 0. IDE already has ready-to-use window where we already can set every thing to start docker process. This window is on screenshots above. We will use it as is.

Nuance 1. We can’t just take and run any script in some container. Technically, it is possible, but the container must first be (created and) launched.

I assume that the php project user is working on is already dockerized. Therefore, the container must exist and be running. To specify its name, I entered the --container argument to enter in ‘PHP Options’.

Nuance 2. The php script itself must be accessible inside the container: either being inside the volume, or being volume-mounted from the host. So we need to convert the path to the php script: the absolute (host) path inside the container will be incorrect. We need a relative one.

By default, it is assumed that this path is relative to the current WORKDIR container. So I cut a piece from the root to the current project directory which is defined as the absolute path to the helper minus two pieces on the right. Thus,

/home/anthony/nb-test/nbproject/php

becomes

/home/anthony/nb-test

and this is what I cut from an absolute script path.

Another case is when you specify --map in ‘PHP Options’. Then the left part of the map is cut from the absolute script path and concatenated with the right. As a result, the absolute path to the file is already inside the container.

Nuance 3. The interpreter in NB can be configured both within a single ‘Run Configuration’ and in global settings. Hence, two ways of using the helper were naturally born: global and local. With a global helper, it lies anywhere on the host, with a local one — in the nbproject directory of the project. This directory is the only indicator by which the helper can find the source folder.

Nuance 4. I’ve already mentioned the working directory here. By default, this is the project directory, but it can be explicitly overrided in the ‘Run Configuration’. So, before starting the script, the specified path will become a pwd for php.

As my research has shown, it’s better to ignore and never look or touch.


And now, when all permutations and transformations were made, we have such command:

docker exec \
    test-php \
    php \
    -dxdebug.mode=debug \
    -dxdebug.start_with_request=1 \
    /var/www/index.php \
    arg1 \
    arg2

I think the code of this helper turned out to be pretty good. Here is a result of its execution:

Some info for Windows users. I think it will be easier for you to rewrite this helper by yourself using cmd or powershell or any compiled language. Sorry, but I don’t want to dive into this rabbit hole, because first of all I was solving my own problem.

I would be glad if this helper will be useful to someone. Aloha.

Leave a comment

Your email address will not be published. Required fields are marked *