DEV Community

Cover image for How to debug ANY Symfony command simply passing `-x`
Adamo Crespi for Serendipity HQ

Posted on • Updated on

How to debug ANY Symfony command simply passing `-x`

Debugging a Symfony console command requires setting some environment variables (depending on your actual configuration of xDebug):

  • XDEBUG_SESSION=1 (Docs)
  • XDEBUG_MODE=debug
  • XDEBUG_ACTIVATED=1

You can check the purpose of these environment variables on the xDebug's Docs.

TL;DR

So, launching a Symfony console command in debug mode would look like this:

XDEBUG_SESSION=1 XDEBUG_MODE=debug XDEBUG_ACTIVATED=1 php bin/console my:command --an-option --an argument
Enter fullscreen mode Exit fullscreen mode

Using the listener below, instead, you can debug any Symfony command this way:

bin/console my:command --an-option --an argument -x
Enter fullscreen mode Exit fullscreen mode

Really shorter and faster, also to debug a command on the fly, without having to move the cursor to the beginning of the command (that is so boring to do on the CLI! 🤬).

The -x option starts the "magic" and the listener actually performs the trick.

Using this listener, you can actually debug ANY Symfony command, also if it doesn't belong to your app, but belongs to, for example, Doctrine, a third party bundle or even Symfony itself.

It's really like magic!

Image description

The RunCommandInDebugModeEventListener listener

The listener works thanks to the ConsoleEvents::COMMAND event.

It simply searches for the flag -x (or --xdebug) and, if it finds it, then restarts the command setting the environment variables required by xDebug to work.

The restart of the command is done through the PHP function passthru().

The rest of the code is sufficiently self explanatory, so I'm not going to explain it.

This is the listener: happy Symfony commands debugging!

<?php

declare(strict_types=1);

namespace App\EventListener;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Command\HelpCommand;
use Symfony\Component\Console\ConsoleEvents;
use Symfony\Component\Console\Event\ConsoleCommandEvent;
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;

#[AsEventListener(ConsoleEvents::COMMAND, 'configure')]
class RunCommandInDebugModeEventListener
{
    public function configure(ConsoleCommandEvent $event): void
    {
        $command = $event->getCommand();

        if (false === $command instanceof Command) {
            throw new \RuntimeException('The command must be an instance of ' . Command::class);
        }

        if ($command instanceof HelpCommand) {
            $command = $this->getActualCommandFromHelpCommand($command);
        }

        $command->addOption(
            name: 'xdebug',
            shortcut: 'x',
            mode: InputOption::VALUE_NONE,
            description: 'If passed, the command is re-run setting env variables required by xDebug.',
        );

        if ($command instanceof HelpCommand) {
            return;
        }

        $input = $event->getInput();
        if (false === $input instanceof ArgvInput) {
            return;
        }

        if (false === $this->isInDebugMode($input)) {
            return;
        }

        if ('1' === getenv('XDEBUG_SESSION')) {
            return;
        }

        $output = $event->getOutput();
        $output->writeln('<comment>Relaunching the command with xDebug...</comment>');

        $cmd = $this->buildCommandWithXDebugActivated();

        \passthru($cmd);
        exit;
    }

    private function getActualCommandFromHelpCommand(HelpCommand $command): Command
    {
        $reflection    = new \ReflectionClass($command);
        $property      = $reflection->getProperty('command');
        $actualCommand = $property->getValue($command);

        if (false === $actualCommand instanceof Command) {
            throw new \RuntimeException('The command must be an instance of ' . Command::class);
        }

        return $actualCommand;
    }

    private function isInDebugMode(ArgvInput $input): bool
    {
        $tokens = $this->getTokensFromArgvInput($input);

        foreach ($tokens as $token) {
            if ('--xdebug' === $token || '-x' === $token) {
                return true;
            }
        }

        return false;
    }

    /**
     * @return array<string>
     */
    private function getTokensFromArgvInput(ArgvInput $input): array
    {
        $reflection     = new \ReflectionClass($input);
        $tokensProperty = $reflection->getProperty('tokens');
        $tokens         = $tokensProperty->getValue($input);

        if (false === is_array($tokens)) {
            throw new \RuntimeException('Impossible to get the arguments and options from the command.');
        }

        return $tokens;
    }

    private function buildCommandWithXDebugActivated(): string
    {
        $serverArgv = $_SERVER['argv'] ?? null;
        if (null === $serverArgv) {
            throw new \RuntimeException('Impossible to get the arguments and options from the command: the command cannot be relaunched with xDebug.');
        }

        $script = $_SERVER['SCRIPT_NAME'] ?? null;
        if (null === $script) {
            throw new \RuntimeException('Impossible to get the name of the command: the command cannot be relaunched with xDebug.');
        }

        $phpBinary = PHP_BINARY;
        $args      = implode(' ', array_slice($serverArgv, 1));

        return "XDEBUG_SESSION=1 XDEBUG_MODE=debug XDEBUG_ACTIVATED=1 {$phpBinary} {$script} {$args}";
    }
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)