Несколько лет назад мы написали статью о том, как работают сигналы POSIX в PHP.
Сегодня мы хотим поделиться с вами тем, как работать с сигналами с помощью Symfony Command.
⚠ Это работает только начиная с Symfony 6.3. Эта версия будет выпущена в мае 2023 года.
По умолчанию Symfony Command не обрабатывает сигналы. Поэтому, когда вы запускаете команду и нажимаете CTRL+C, она немедленно остановит процесс.
В большинстве случаев это безопасно, но иногда вы хотите обрабатывать сигналы, чтобы выполнить некоторую очистку перед остановкой команды. Например, если ваша команда ведет диалог с API удаленных платежей, вы хотите написать атомарную транзакцию: либо платеж (удаленно + локально) выполнен, либо нет.
Для обработки сигналов вам необходимо реализовать интерфейс SignalableCommandInterface:
<?php
namespace App\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\SignalableCommand\SignalableCommandInterface;
class PaymentCommand extends Command implements SignalableCommandInterface
{
private bool $shouldStop = false;
protected function execute(InputInterface $input, OutputInterface $output): int
{
foreach ($this->getPayments() as $payment) {
if ($this->shouldStop) {
break;
}
$this->processPayment($payment);
}
return Command::SUCCESS;
}
public function getSubscribedSignals(): array
{
return [
SIGINT,
SIGTERM,
];
}
public function handleSignal(int $signal, int|false $previousExitCode = 0): int|false
{
$this->logger->info('Signal received, stopping the command...', [
'signal' => $signal,
]);
$this->shouldStop = true;
return false;
}
}
Как только процесс прерывается сигналом SIGINT или SIGTERM, вызывается метод handleSignal(). Этот метод переключает свойство shouldStop в true. Затем метод execute() возобновляется и останавливает цикл, но только после того, как текущий платеж будет полностью обработан.
Строка "return false;" позволяет вернуть определенный код выхода при подаче сигнала о выполнении команды или не выходить вообще. В нашем случае мы не хотим автоматически выходить по сигналам SIGINT или SIGTERM, поэтому возвращаем false.
Symfony генерирует событие при получении сигнала. По умолчанию обрабатываются следующие сигналы:
Для обработки сигналов вы можете прослушать событие ConsoleEvents::SIGNAL. Следующий пример показывает, что можно сделать в подписчике:
<?php
namespace App\Somewhere\In\Your\Application;
use Symfony\Component\Console\ConsoleEvents;
use Symfony\Component\Console\Event\ConsoleSignalEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class SignalSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
ConsoleEvents::SIGNAL => 'handleSignal',
];
}
public function handleSignal(ConsoleSignalEvent $event): void
{
$signal = $event->getSignal();
// #1 Some log
$this->logger->info('Signal received'., [
'signal' => $signal,
]);
// #2 Stop the command if it implements StoppableCommandInterface
// StoppableCommandInterface is not part of the Symfony Console component
// It's up to you to implement it in your application
if (
$event->getCommand() instanceof StoppableCommandInterface)
&& in_array($signal, [SIGINT, SIGTERM], true)
) {
$event->getCommand()->stop();
$event->abortExit();
}
// #3 Do not stop on SIGUSR1 or SIGUSR2
// By default, PHP will stop on SIGUSR1, or SIGUSR2, let's change that
if (in_array($signal, [SIGINT, SIGTERM], true)) {
$event->setExitCode(null);
}
// #4 Set a custom status code on other signals
$event->setExitCode(128 + $signal);
}
}
Если вы хотите диспетчеризировать больше событий при получении сигнала, вы можете использовать метод Application::setSignalsToDispatchEvent():
// bin/console
$application = new Application($kernel);
$application->setSignalsToDispatchEvent([SIGINT, SIGTERM, SIGUSR1, SIGUSR2, SIGALRM]);
Или вы можете настроить ее в самой команде:
class MyCommand extends Command
{
protected function execute(InputInterface $input, OutputInterface $output): int
{
$this->getApplication()->setSignalsToDispatchEvent([SIGINT, SIGTERM, SIGUSR1, SIGUSR2, SIGALRM]);
}
Обработка сигналов очень важна, когда вы хотите написать долго выполняющуюся команду, или когда команда обрабатывает критические данные, например, платежи.
Как вы можете видеть, начиная с Symfony 6.3 очень легко обрабатывать сигналы с помощью Symfony Command. Мы предлагаем вам всегда писать такой код, чтобы обеспечить безопасность вашего приложения.
Symfony 6.2 - новые возможности
14405
753
Новое в Symfony 6.2: Компонент Clock
14606
730
Новое в Symfony 6.1: Более простое расширение и настройка бандлов
15610
780
Новое в Symfony 6.1: динамический переключатель локали (Locale Switcher)
12747
637
Новое в Symfony 6.1: компонент HtmlSanitizer
13106
655
Для Symfony 6.1 потребуется PHP версии 8.1 из-за предзагрузки классов
8597
429
Внедрение поддержки Docker в Symfony
14320
716
Приложение на Symfony и Twitter Bootsrap 4 [видео]
10498
524