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

WARNING

The command will not work to debug the Symfony's container building: in that phase, in fact, the listeners are not still active.

If you need to debug the configuration of a bundle or any other part of Symfony that is before the container is built and services configured, then you still need to go the old way, with full declaration of env variables directly in the command call in the command line.

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)