Les commandes Artisan sont souvent utilisées pour effectuer des opérations longues et consommatrices en ressources. Parfois, face à une commande Artisan qui ne répond plus, ou qui ne se termine pas dans des délais raisonnables, nous pouvons être tentés de la "kill" à l'aide d'un simple Ctrl+C.
Cependant, cette action est agressive et pourrait provoquer des comportements inattendus. Par exemple, que se passe-t-il si vous stoppez la commande alors qu'elle effectue des modifications en base de données ?
Voyons ce que propose Laravel pour gérer proprement ce genre de situation !
Qu'est-ce que les signaux ?
Avant de continuer ce tutoriel, il est important de comprendre ce qu'est un signal.
Un signal est un message envoyé à un processus en cours d'exécution, par exemple, quand vous appuyez sur Ctrl+C pendant l'exécution d'une commande, vous envoyez un signal SIGINT
au processus lui indiquant que l'utilisateur souhaite l'arreter.
Pour profiter de ces signaux, il sera nécessaire d'installer l'extension pcntl
à votre PHP. Cette extension était courante il est fort probable qu'elle soit déjà disponible dans votre projet, vous le déterminerez en vérifiant les extensions disponibles avec la commande suivante :
1php -m | grep pcntl
pcntl (Process Control) est une extension PHP permettant de manipuler les processus et de gérer les signaux système
Un signal n'est donc pas une notion propre à Laravel, ni même à PHP, Il existe de nombreux autres signaux que vous pourrez retrouver sur cette liste, certains peuvent provenir d'une action manuelle de l'utilisateur, de l'application ou directement de votre système d'exploitation !
Ces signaux sont extrêmement pratiques car ils permettent d’interagir avec une commande en cours d’exécution, chose auparavant impossible avec PHP seul.
Dans ce tutoriel, nous allons capturer le signal SIGINT
pour exécuter des opérations avant l’arrêt de notre commande Artisan.
Cas concret d'utilisation des signaux
Imaginons que notre application possède une commande effectuant une modification sur l'ensemble de nos utilisateurs. Cette commande étant longue et critique, nous avons placé des transactions afin de garantir le bon fonctionnement des modifications :
1namespace App\Console\Commands; 2 3use App\Models\User; 4use Illuminate\Console\Command; 5use Illuminate\Support\Facades\DB; 6 7class Test extends Command 8{ 9 protected $signature = 'app:test';10 11 public function handle()12 {13 DB::beginTransaction();14 15 $users = User::all();16 17 $users->each(function (User $user) {18 $user->updateHeavyAndLongOperation();19 });20 21 DB::commit();22 }23}
Dans cette situation, en cas de Ctrl+C, la transaction pourrait rester ouverte et consommer des ressources inutiles, voire même laisser des verrous sur les tables nécessitant par la suite une intervention manuelle.
Pour éviter cela, nous allons utiliser la classe SignalableCommandInterface
, présente depuis Laravel 10, pour gérer proprement les signaux et les interruptions !
1namespace App\Console\Commands; 2 3use App\Models\User; 4use Illuminate\Console\Command; 5use Illuminate\Support\Facades\DB; 6use Symfony\Component\Console\Command\SignalableCommandInterface; 7 8class Test extends Command implements SignalableCommandInterface 9{10 protected $signature = 'app:test';11 12 public function handle()13 {14 DB::beginTransaction();15 16 $users = User::all();17 18 $users->each(function (User $user) {19 $user->updateHeavyAndLongOperation();20 });21 22 DB::commit();23 }24 25 public function getSubscribedSignals(): array 26 {27 return [];28 }29 30 public function handleSignal(int $signal, int|false $previousExitCode = 0): int|false 31 {32 return $previousExitCode;33 }34}
L'ajout de cette interface nécessite l'implémentation des deux méthodes suivantes.
Pour commencer, la méthode getSubscribedSignals
permet de préciser les signaux que la commande souhaite recevoir. Dans notre cas, nous souhaitons manipuler le signal SIGINT
correspondant à notre Ctrl+C pour annuler les modifications en base de données.
Dans un second temps, la méthode handleSignal
permet de préciser le comportement de la commande en cas de réception d'un signal configuré.
1<?php 2 3namespace App\Console\Commands; 4 5use App\Models\User; 6use Illuminate\Console\Command; 7use Illuminate\Support\Facades\DB; 8use Symfony\Component\Console\Command\SignalableCommandInterface; 9 10class Test extends Command implements SignalableCommandInterface11{12 protected $signature = 'app:test';13 14 public function handle()15 {16 DB::beginTransaction();17 18 $users = User::all();19 20 $users->each(function (User $user) {21 $user->updateHeavyAndLongOperation();22 });23 24 DB::commit();25 }26 27 public function getSubscribedSignals(): array28 {29 return [SIGINT]; 30 }31 32 public function handleSignal(int $signal, int|false $previousExitCode = 0): int|false33 {34 if ($signal === SIGINT) { 35 36 $this->info('SIGINT received, rollback transaction !');37 38 DB::rollBack();39 40 return 0;41 }42 43 return $previousExitCode;44 }45}
Une fois le signal réceptionné dans la méthode handleSignal
, nous pouvons effectuer les opérations nécessaires, ici, nous annulerons la transaction en cours.
Voilà, vous savez désormais comment manipuler des signaux dans une commande Artisan ! Pour nous simplifier la vie, Spatie propose également un package facilitant l'utilisation des signaux dans les commandes.
Après avoir installé le package et ajouté l'interface SignalAwareCommand
à notre commande, il suffira d'ajouter la méthode onSigint
pour effectuer les opérations nécessaires en cas de réception du signal SIGINT
:
1 2namespace App\Console\Commands; 3 4use App\Models\User; 5use Illuminate\Console\Command; 6use Illuminate\Support\Facades\DB; 7use Spatie\SignalAwareCommand\SignalAwareCommand; 8 9class Test extends Command implements SignalAwareCommand10{11 protected $signature = 'app:test';12 13 public function handle()14 {15 DB::beginTransaction();16 17 $users = User::all();18 19 $users->each(function (User $user) {20 $user->updateHeavyAndLongOperation();21 });22 23 DB::commit();24 }25 26 public function onSigint() 27 {28 $this->info('SIGINT received, rollback transaction !');29 30 DB::rollBack();31 }32}
En dehors de Laravel
La gestion des signaux est tout à fait possible en dehors de Laravel, dans un simple script PHP.
Apres avoir activé l'écoute des signaux à l'aide de la fonction pcntl_async_signals
, nous pouvons ensuite écrire un callback associé à un signal depuis la fonction pcntl_signal
:
1pcntl_async_signals(true);2 3pcntl_signal(SIGINT, function () {4 echo "SIGINT";5 exit(0);6});7 8sleep(100);
Rien de très compliqué, l'association de ces deux méthodes vous permettra de gérer les signaux dans n'importe quel script PHP !
Conclusion
La gestion des signaux n'est pas une chose courante en PHP mais elle peut parfois s'avérer très utile dans certaines situations.
En réceptionnant les bons signaux au bon moment, vous pourrez désormais effectuer des opérations sur des processus en cours d'exécution et vous reprendrez davantage de contrôle sur la vie de vos commandes !
A lire
Autres articles de la même catégorie

La notion de Manager dans Laravel
Introduction à un élément incontournable au cœur de Laravel, le Manager. Ce dernier est au commande d’un bon nombre des services.

William Suppo

Installer une beta de PHP en CLI
Que ce soit pour tester une nouvelle alpha ou s’amuser sur une vieille version, voyons comment installer en quelques minutes une version spécifique de php en CLI.

Mathieu De Gracia

Inversion de dépendance en Laravel
Améliorons l'architecture de nos projets en implémentant une dépendance externe à travers 3 approches différentes.

Mathieu De Gracia