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
Dans le cadre de notre partenariat avec Carb0n, nous avons réalisé un examen complet de leur infrastructure, allant de l’environnement de production, jusqu’à au code de l’application. Nous avons fourni des recommandations stratégiques pour son amélioration. Notre intervention a notamment couvert : Évaluation et renforcement de la sécurité sur AWS, …
En tant que joaillier 100 % numérique, l’équipe de Courbet Paris a souhaité se doter d’une plateforme eCommerce, capable d’offrir une expérience moderne qui revalorise l’acte d’achat de produits de joaillerie sur internet. JoliCode a accompagné leur équipe en développant une plateforme robuste, mais aussi évolutive, afin de répondre aux enjeux business…
Les site e-commerces font face à de nombreuses problématiques : gestion de fort trafic, recherche parmi des milliers de références etc. JoliCode a accompagné l’équipe Smallable dans le choix des solutions pouvant répondre à ces enjeux : utilisation de briques asynchrones, conception d’index Elasticsearch pérenne.