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 d’une refonte complète de son architecture Web, le journal en ligne Mediapart a sollicité l’expertise de JoliCode afin d’accompagner ses équipes. Mediapart.fr est un des rares journaux 100% en ligne qui n’appartient qu’à ses lecteurs qui amène un fort traffic authentifiés et donc difficilement cachable. Pour effectuer cette migration, …

La nouvelle version du site naissance.fr développée s’appuie sur Symfony 2 et Elasticsearch. Cette refonte propose un tunnel d’achat spécialement développé pour l’application. Aujourd’hui, le site est équipé d’une gestion d’un mode d’envoi des faire-parts différé, de modification des compositions après paiement et de prise en charge de codes promotionnels…

Après avoir monté une nouvelle équipe de développement, nous avons procédé à la migration de toute l’infrastructure technique sur une nouvelle architecture fortement dynamique à base de Symfony2, RabbitMQ, Elasticsearch et Chef. Les gains en performance, en stabilité et en capacité de développement permettent à l’entreprise d’engager de nouveaux marchés…