Handling signal with Symfony Command
A few years ago, we wrote an article (in french) about how POSIX signals work in PHP.
Today, we want to share with you how to handle signals with Symfony Command.
⚠ This works only as of Symfony 6.3. Symfony 6.3 will be released in May 2023.
By default, Symfony Command does not handle signals. So when you start a command, and you hit CTRL+C it will immediately stop the process.
Most of the time it’s safe, but sometimes you want to handle signals to do some cleanup before stopping the command. For example if your command dialogs with a remote payment API, you want to write an atomic transaction: either the payment (remotely + locally) is done, or it is not.
Section intitulée how-to-handle-signalsHow to handle signals
To handle signals, you need to implement the 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;
}
}
As soon as the process is interrupted with SIGINT
or SIGTERM
, the handleSignal()
method is called. This method will toggle the shouldStop
property to true
. Then, the execute()
method is resumed and stops the loop, but only after the current payment has been fully processed fully.
The return false;
line allows to return a specific exit code when the command is signaled, or to not exit at all. In our case, we do not want to automatically exit on SIGINT
or SIGTERM
, so we return false
.
Section intitulée how-to-use-event-to-handle-signalsHow to use Event to handle signals
Symfony emits an event when a signal is received. By default the following signals are handled:
-
SIGINT
(Ctrl+C) -
SIGTERM
(kill
) -
SIGUSR1
(kill -USR1
) -
SIGUSR2
(kill -USR2
)
You can listen to the ConsoleEvents::SIGNAL
event to handle signals. The following example shows what you can do in a subscriber:
<?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);
}
}
If you want to dispatch more events when a signal is received, you can use the Application::setSignalsToDispatchEvent()
method:
// bin/console
$application = new Application($kernel);
$application->setSignalsToDispatchEvent([SIGINT, SIGTERM, SIGUSR1, SIGUSR2, SIGALRM]);
Or you can configure it in the command itself:
class MyCommand extends Command
{
protected function execute(InputInterface $input, OutputInterface $output): int
{
$this->getApplication()->setSignalsToDispatchEvent([SIGINT, SIGTERM, SIGUSR1, SIGUSR2, SIGALRM]);
}
Section intitulée conclusionConclusion
Handling signals is very important when you want to write a long-running command, or when the command handles critical data like payments.
As you can see, as of Symfony 6.3 it is very easy to handle signals with Symfony Command. We invite you to always write this kind of code to ensure your application is safe.
Commentaires et discussions
Nos formations sur ce sujet
Notre expertise est aussi disponible sous forme de formations professionnelles !
Symfony avancée
Découvrez les fonctionnalités et concepts avancés de Symfony
Ces clients ont profité de notre expertise
Nous avons réalisé la refonte du site de l’agence Beautiful Monday en utilisant nos compétences HTML5/CSS3 côté front-end, et le framework Symfony2 côté back-end. Afin de s’afficher correctement sur n’importe quel appareil, le site est entièrement responsive. La partie intégration a été effectuée avec un grand soin, en respectant parfaitement la maquette…
Afin de poursuivre son déploiement sur le Web, Arte a souhaité être accompagné dans le développement de son API REST “OPA” (API destinée à exposer les programmes et le catalogue vidéo de la chaine). En collaboration avec l’équipe technique Arte, JoliCode a mené un travail spécifique à l’amélioration des performances et de la fiabilité de l’API. Ces…
Nous avons développé une plateforme de site génériques autour de l’API Phraseanet. À l’aide de Silex de composants Symfony2, nous avons accompagné Alchemy dans la réalisation d’un site déclinable pour leurs clients. Le produit est intégralement configurable et supporte de nombreux systèmes d’authentification (Ldap, OAuth2, Doctrine ou anonyme).