Tip of the day: how to debug Composer scripts with XDebug and PhpStorm

The problem: XDebug doesn't work for Composer scripts

PhpStorm is quite convenient to debug scripts with XDebug (do you support Derick for giving us XDebug ?): just add a "Run/Debug configuration", choosing the "PHP Script" type, give a few parameters, and you can start debugging your PHP CLI scripts, using breakpoints, evaluations, etc.

Wonderful. So now, let's define such a configuration to debug a Composer script, say a Behat configuration generator from site settings for some current Drupal 8 project. Apply the configuration, run it in debug mode, and ....

...PhpStorm doesn't stop, the script runs and ends, and all breakpoints were ignored. How to actually use breakpoints in the IDE ?

Broken configuration to debug a Composer script

The diagnostic

Let's see how Composer actually starts:
<?php
if (PHP_SAPI !== 'cli') {
    echo
'Warning: Composer should be invoked via the CLI version of PHP, not the '.PHP_SAPI.' SAPI'.PHP_EOL;
}

require
__DIR__.'/../src/bootstrap.php';

use
Composer\Factory;
use
Composer\XdebugHandler;
use
Composer\Console\Application;

error_reporting(-1);

// Create output for XdebugHandler and Application
$output = Factory::createOutput();

$xdebug = new XdebugHandler($output);
$xdebug->check();
unset(
$xdebug);

// [...more...]
?>

Now, we can step through this, and notice the Composer script actually runs during the $xdebug->check(); line. What's going on ?

<?php
// In vendor/composer/composer/src/Composer/XdebugHandler.php
   
public function check()
    {
       
$args = explode('|', strval(getenv(self::ENV_ALLOW)), 2);

        if (
$this->needsRestart($args[0])) {
            if (
$this->prepareRestart()) {
               
$command = $this->getCommand();
               
$this->restart($command);
            }

            return;
        }
    
// [...more...]
?>

Here's the crux of the problem: in order to alleviate the extreme slowdown caused by XDebug when running Composer commands, any time Composer is run, it checks for the presence of Xdebug in the running PHP configuration, and if it finds it (the needsRestart() check), it rebuilds a command line with a different PHP configuration ($command = $this->getCommand();), which does not include the xdebug extension, and runs it in the $this->restart($command); call, which actually runs the new command using the passthru mechanism.

<?php
   
protected function restart($command)
    {
       
passthru($command, $exitCode);
   
// [...more...]
?>

Now, since this command no longer runs with Xdebug, there is no way a debugging tool can use it. So how does one fix the problem ?

The solution

Let's go back to the beginning of this check() method:

<?php
class XdebugHandler
{
    const
ENV_ALLOW = 'COMPOSER_ALLOW_XDEBUG';

   
// [...snip...]

   
public function check()
    {
       
$args = explode('|', strval(getenv(self::ENV_ALLOW)), 2);

        if (
$this->needsRestart($args[0])) {
   
// [...more...]
?>

So the call to needsRestart($args[0] does actually depend on the value of the XdebugHandler::ENV_ALLOW constant, i.e. COMPOSER_ALLOW_XDEBUG. Let's see.

<?php
   
private function needsRestart($allow)
    {
        if (
PHP_SAPI !== 'cli' || !defined('PHP_BINARY')) {
            return
false;
        }

        return empty(
$allow) && $this->loaded;
    }
?>

So, if COMPOSER_ALLOW_XDEBUG is not empty, needsRestart($allow) will return FALSE. In which case, as shown above, it won't cause the new command to be built, and Composer will just proceed with our script and allow our debugging to work.

In practice, this means all we need it to pass a COMPOSER_ALLOW_XDEBUG environment variable with a non-empty value in the PhpStorm run configuration, like this.

Debug configuration for Composer command in PhpStorm

Debugging a Composer script in PhpStorm

Problem solved_! BTW, did you notice this was explained in the Composer vendor/composer/composer/doc/articles/troubleshooting.md documentation ?

[...snip...]
## Xdebug impact on Composer

To improve performance when the xdebug extension is enabled, Composer automatically restarts PHP without it.
You can override this behavior by using an environment variable: `COMPOSER_ALLOW_XDEBUG=1`.
[...more...]

Yeah, me neither, until I solved the problem as described. So let's all just keep in mind to RTFM.