Console Events
Console-компонент генерирует события на каждом этапе выполнения команды. Это позволяет перехватывать и модифицировать поведение любой команды.
Список событий
| Событие | Когда вызывается |
|---|---|
ConsoleEvents::COMMAND |
Перед выполнением команды |
ConsoleEvents::TERMINATE |
После выполнения команды |
ConsoleEvents::ERROR |
При возникновении исключения |
ConsoleEvents::SIGNAL |
При получении OS-сигнала (SIGINT, SIGTERM) |
ConsoleEvents::COMMAND
Вызывается перед execute(). Позволяет модифицировать ввод, вывод или полностью отменить выполнение.
<?php
declare(strict_types=1);
namespace App\EventSubscriber;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\ConsoleEvents;
use Symfony\Component\Console\Event\ConsoleCommandEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
final class CommandLoggingSubscriber implements EventSubscriberInterface
{
public function __construct(
private readonly LoggerInterface $logger,
) {
}
public static function getSubscribedEvents(): array
{
return [
ConsoleEvents::COMMAND => 'onCommand',
];
}
public function onCommand(ConsoleCommandEvent $event): void
{
$command = $event->getCommand();
$input = $event->getInput();
$this->logger->info('Executing command: {name}', [
'name' => $command?->getName(),
'arguments' => $input->getArguments(),
'options' => $input->getOptions(),
]);
// Optionally disable the command
// $event->disableCommand();
// When disabled, execute() is NOT called, return code = COMMAND::FAILURE
}
}
ConsoleEvents::TERMINATE
Вызывается после завершения команды (после execute()). Позволяет выполнить cleanup или изменить код возврата.
<?php
declare(strict_types=1);
namespace App\EventSubscriber;
use Symfony\Component\Console\ConsoleEvents;
use Symfony\Component\Console\Event\ConsoleTerminateEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
final class CommandTimingSubscriber implements EventSubscriberInterface
{
private float $startTime = 0.0;
public static function getSubscribedEvents(): array
{
return [
ConsoleEvents::COMMAND => ['onCommand', 255],
ConsoleEvents::TERMINATE => ['onTerminate', -255],
];
}
public function onCommand(): void
{
$this->startTime = microtime(true);
}
public function onTerminate(ConsoleTerminateEvent $event): void
{
$duration = microtime(true) - $this->startTime;
$command = $event->getCommand();
$exitCode = $event->getExitCode();
$output = $event->getOutput();
$output->writeln('');
$output->writeln(sprintf(
'<comment>Command "%s" finished in %.2fs (exit code: %d)</comment>',
$command?->getName() ?? 'unknown',
$duration,
$exitCode,
));
// Optionally change exit code
// $event->setExitCode(0);
}
}
ConsoleEvents::ERROR
Вызывается при исключении в execute(). Позволяет логировать ошибку, изменить исключение или код возврата.
<?php
declare(strict_types=1);
namespace App\EventSubscriber;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\ConsoleEvents;
use Symfony\Component\Console\Event\ConsoleErrorEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
final class CommandErrorSubscriber implements EventSubscriberInterface
{
public function __construct(
private readonly LoggerInterface $logger,
) {
}
public static function getSubscribedEvents(): array
{
return [
ConsoleEvents::ERROR => 'onError',
];
}
public function onError(ConsoleErrorEvent $event): void
{
$error = $event->getError();
$command = $event->getCommand();
$this->logger->error('Command "{name}" threw exception: {message}', [
'name' => $command?->getName(),
'message' => $error->getMessage(),
'trace' => $error->getTraceAsString(),
]);
// Change the exit code
$event->setExitCode(1);
// Replace the exception (optional)
// $event->setError(new \RuntimeException('Wrapped: ' . $error->getMessage(), 0, $error));
}
}
ConsoleEvents::SIGNAL
Обработка OS-сигналов (SIGINT при Ctrl+C, SIGTERM при остановке).
<?php
declare(strict_types=1);
namespace App\Command;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Command\SignalableCommandInterface;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
#[AsCommand(name: 'app:long-task')]
final class LongTaskCommand extends Command implements SignalableCommandInterface
{
private bool $shouldStop = false;
/**
* List of signals this command handles
* @return list<int>
*/
public function getSubscribedSignals(): array
{
return [\SIGINT, \SIGTERM];
}
/**
* Handle the received signal
*/
public function handleSignal(int $signal, int|false $previousExitCode = 0): int|false
{
match ($signal) {
\SIGINT => $this->shouldStop = true,
\SIGTERM => $this->shouldStop = true,
default => null,
};
// Return false to continue, or exit code to stop
return false; // Continue running, but flag is set
}
protected function execute(
InputInterface $input,
OutputInterface $output,
): int {
$output->writeln('Processing... Press Ctrl+C to stop gracefully.');
for ($i = 0; $i < 1000; $i++) {
if ($this->shouldStop) {
$output->writeln('<comment>Graceful shutdown requested</comment>');
// Cleanup resources
break;
}
$output->write('.');
usleep(100000);
}
$output->writeln('');
$output->writeln('<info>Done!</info>');
return Command::SUCCESS;
}
}
Verbosity Levels
Уровни verbosity контролируют, сколько информации выводит команда.
Уровни
| Уровень | Значение | Флаг | Использование |
|---|---|---|---|
QUIET |
16 | -q |
Без вывода |
NORMAL |
32 | (по умолчанию) | Стандартный вывод |
VERBOSE |
64 | -v |
Подробности |
VERY_VERBOSE |
128 | -vv |
Ещё больше подробностей |
DEBUG |
256 | -vvv |
Отладочная информация |
Использование verbosity
<?php
declare(strict_types=1);
namespace App\Command;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
#[AsCommand(name: 'app:verbosity-demo')]
final class VerbosityDemoCommand extends Command
{
protected function execute(
InputInterface $input,
OutputInterface $output,
): int {
// Method 1: Check verbosity level
if ($output->isQuiet()) {
// -q: no output at all
return Command::SUCCESS;
}
// Always shown (NORMAL and above)
$output->writeln('Processing started...');
if ($output->isVerbose()) {
// -v: additional details
$output->writeln('Loading configuration...');
$output->writeln('Connecting to database...');
}
if ($output->isVeryVerbose()) {
// -vv: even more details
$output->writeln('SQL: SELECT * FROM users WHERE active = 1');
$output->writeln('Found 42 users');
}
if ($output->isDebug()) {
// -vvv: debug information
$output->writeln('Memory usage: ' . memory_get_usage(true));
$output->writeln('Time elapsed: 0.42s');
}
$output->writeln('Done!');
return Command::SUCCESS;
}
}
Verbosity с OutputInterface constants
<?php
declare(strict_types=1);
use Symfony\Component\Console\Output\OutputInterface;
// Method 2: writeln with verbosity parameter
$output->writeln('Always visible');
$output->writeln('Verbose info', OutputInterface::VERBOSITY_VERBOSE);
$output->writeln('Very verbose', OutputInterface::VERBOSITY_VERY_VERBOSE);
$output->writeln('Debug info', OutputInterface::VERBOSITY_DEBUG);
Verbosity с SymfonyStyle
<?php
declare(strict_types=1);
$io = new SymfonyStyle($input, $output);
// SymfonyStyle checks verbosity automatically for some methods:
// - note(), caution() shown at VERBOSE and above
// - writeln() always shown unless QUIET
// Explicit check
if ($io->isVerbose()) {
$io->note('Detailed processing information');
}
if ($io->isDebug()) {
$io->text('Debug: memory=' . memory_get_usage());
}
Полный пример: команда с событиями и verbosity
<?php
declare(strict_types=1);
namespace App\Command;
use App\Repository\UserRepository;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Command\SignalableCommandInterface;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(
name: 'app:cleanup-users',
description: 'Remove inactive users from the database',
)]
final class CleanupUsersCommand extends Command implements SignalableCommandInterface
{
private bool $shouldStop = false;
public function __construct(
private readonly UserRepository $userRepository,
) {
parent::__construct();
}
protected function configure(): void
{
$this
->addOption('dry-run', null, InputOption::VALUE_NONE, 'Do not actually delete')
->addOption('days', 'd', InputOption::VALUE_REQUIRED, 'Days of inactivity', '90');
}
public function getSubscribedSignals(): array
{
return [\SIGINT, \SIGTERM];
}
public function handleSignal(int $signal, int|false $previousExitCode = 0): int|false
{
$this->shouldStop = true;
return false;
}
protected function execute(
InputInterface $input,
OutputInterface $output,
): int {
$io = new SymfonyStyle($input, $output);
$dryRun = $input->getOption('dry-run');
$days = (int) $input->getOption('days');
$io->title('User Cleanup');
if ($dryRun) {
$io->warning('DRY RUN MODE -- no changes will be made');
}
$users = $this->userRepository->findInactiveUsers($days);
$count = count($users);
if ($count === 0) {
$io->success('No inactive users found');
return Command::SUCCESS;
}
$io->text(sprintf('Found %d inactive users (>%d days)', $count, $days));
$io->progressStart($count);
$deleted = 0;
foreach ($users as $user) {
if ($this->shouldStop) {
$io->progressFinish();
$io->warning("Interrupted! Deleted $deleted/$count users");
return Command::SUCCESS;
}
if ($output->isVerbose()) {
$io->text(sprintf(
'Deleting: %s (last login: %s)',
$user->getEmail(),
$user->getLastLoginAt()?->format('Y-m-d') ?? 'never',
));
}
if (!$dryRun) {
$this->userRepository->remove($user);
$deleted++;
}
$io->progressAdvance();
}
$io->progressFinish();
if ($dryRun) {
$io->note("Would have deleted $count users");
} else {
$io->success("Deleted $deleted users");
}
if ($output->isVeryVerbose()) {
$io->text(sprintf(
'Memory: %s, Time: %.2fs',
number_format(memory_get_peak_usage(true) / 1024 / 1024, 1) . 'MB',
microtime(true) - $_SERVER['REQUEST_TIME_FLOAT'],
));
}
return Command::SUCCESS;
}
}
Итоги
ConsoleEvents::COMMAND-- перед выполнением (можно отменить)ConsoleEvents::TERMINATE-- после выполнения (можно изменить exit code)ConsoleEvents::ERROR-- при исключении (логирование, замена ошибки)ConsoleEvents::SIGNAL/SignalableCommandInterface-- OS-сигналы (Ctrl+C)- Verbosity: QUIET < NORMAL < VERBOSE < VERY_VERBOSE < DEBUG
isVerbose(),isVeryVerbose(),isDebug()-- проверки уровня-q,-v,-vv,-vvv-- флаги командной строки