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
Using the listener below, instead, you can debug any Symfony command this way:
bin/console my:command --an-option --an argument -x
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!
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}";
}
}
Top comments (0)