Depuis sa version 10, Laravel dispose d’une class Process permettant d’exécuter des commandes externes directement depuis votre application.
Cette nouvelle class supplante l’utilisation de fonction natives de PHP telles que exec, passthru ou bien dans notre cas proc_open tout en apportant une surcouche de sécurité et de sucre syntaxique pour en simplifier l’utilisation.
1$result = Process::run('ls -la');2 3$result->output();
La class contient tout un lot d’options pour configurer plus précisément l’exécution de votre commande :
1$result = Process::path('/var/www/sites/laravel-france')2 ->timeout(10)3 ->idleTimeout(5)4 ->run('php artisan down');5 6$result->output();
Étant directement intégrée à l'écosystème du framework, cette class Process profitera de toutes les méthodes habituelles, à l’instar d’un mail ou d’un job, pour simplifier vos cas de tests :
1Process::fake();2 3Process::run('ls -la');4 5Process::assertRan('ls -la');
Vous retrouverez l’intégralité de la documentation sur la class Process à cette URL.
Comment ca fonctionne
Dans les entrailles du framework, la class Process de Laravel est un wrapper de la class Process de Symfony, ce wrapper fournit une interface simplifiée pour interagir avec ses fonctionnalités et se focaliser sur des utilisations plus courantes.
La façade Process
de Laravel donne accès à une class Process\Factory
qui est chargée de créer une instance de la classe PendingProcess
laquelle encapsule à son tour une instance de la class Process de Symfony.
Apres avoir exécuté la commande, la factory vous retournera une instance de ProcessResult
manipulant l’output et le résultat de la commande.
Gardons en tête que la class Process de Symfony reste accessible dans votre application Laravel, libre à vous de l’utiliser si votre besoin est complexe et exige une configuration particulière dépassant le cadre de la class process du framework :
1use Symfony\Component\Process\Process; 2 3$process = Process::fromShellCommandline('ls -la'); 4 5$process->setWorkingDirectory(getcwd()); 6$process->setTimeout(60); 7 8$process->run(); 9 10$process->getOutput();
Cas concret
Désormais, imaginons un cas concret d’utilisation de notre nouvelle class Process.
Imaginons que votre application possède un système de newsletters envoyant des emails à plusieurs millions d'utilisateurs.
Vous pourriez créer une simple commande envoyant un email à la fois mais cela prendrait beaucoup de temps pour parcourir toute la table utilisateurs.
Voyons comment notre nouvelle class Process peut nous permettre de paralléliser l'exécution de cette commande et de grandement accélérer l'envoi de nos newsletters !
Pour commencer, ajoutons un million d’utilisateurs à notre BDD :
1User::factory()->count(1_000_000)->create();
Ensuite, ajoutons une nouvelle commande SendNewsletter
ayant la responsabilité d’envoyer le mail à une liste d’utilisateurs :
1<?php 2 3namespace App\Console\Commands; 4 5use Illuminate\Console\Command; 6 7class SendNewsletter extends Command 8{ 9 /**10 * The name and signature of the console command.11 *12 * @var string13 */14 protected $signature = 'send-newsletter {--ids=}';15 16 /**17 * The console command description.18 *19 * @var string20 */21 protected $description = 'Launch users newsletters.';22 23 /**24 * Execute the console command.25 */26 public function handle(): void27 {28 $ids = $this->option('ids');29 30 $userIds = explode(',', $ids);31 32 foreach($userIds as $id) {33 // launch newsletters34 }35 }36}
Vous pouvez utilisez la commande php artisan make:command SendNewsletter
pour facilement créer la commande.
Nous allons maintenant ajouter une seconde commande SendNewsletterPool
, cette dernière aura pour responsabilité de manipuler la classe Process pour exécuter plusieurs instances de notre première commande SendNewsletter
en parallèle :
1<?php 2 3namespace App\Console\Commands; 4 5use Illuminate\Console\Command; 6 7class SendNewsletterPool extends Command 8{ 9 /**10 * The name and signature of the console command.11 *12 * @var string13 */14 protected $signature = 'send-newsletter-pool';15 16 /**17 * The console command description.18 *19 * @var string20 */21 protected $description = 'Launch users newsletters simultaneously.';22 23 /**24 * Execute the console command.25 */26 public function handle(): void27 {28 //29 }30}
Dans cette commande SendNewsletterPool
, commençons par ajouter un chunkById
pour parcourir la table users
par lots de 10'000 utilisateurs.
1public function handle(): void2{3 User::chunkById(10_000, function ($users) {4 //5 });6}
Si vous souhaitez plus de détails sur la méthode chunkById
, nous vous avons récemment décrit le fonctionnement de ces méthodes dans cet article.
À l’intérieur du chunkById
, découpons cette variable $user
en plusieurs sous-lots de 2'000 items.
Comme vous pouvez le voir à l’aide d’un dump, nous ne retrouvons avec 5 arrays contenant chacun 2'000 utilisateurs :
1public function handle(): void 2{ 3 User::chunkById(10_000, function ($users) { 4 5 $chunked = $users->chunk(2000); 6 7 dd( 8 $chunked->count(), // 5 9 $chunked->get(0)->count(), // 200010 $chunked->get(1)->count(), // 200011 $chunked->get(2)->count(), // 200012 $chunked->get(3)->count(), // 200013 $chunked->get(4)->count(), // 200014 );15 });16}
C'est l'heure d'utiliser notre class Process !
En utilisant un Process::pool, il devient possible d'exécuter plusieurs fois en parallèle la commande send-newsletter
depuis send-newsletter-pool
, dans notre cas 5 fois, chaque exécution ayant un lot de 2'000 utilisateurs à parcourir :
1public function handle(): void 2{ 3 User::chunkById(10_000, function ($users) { 4 5 $chunked = $users->chunk(2000); 6 7 $command = 'php artisan send-newsletter --ids='; 8 9 $pool = Process::pool(function (Pool $pool) use ($command, $chunked) {10 $pool->command($command . $chunked->get(0)->pluck('id'));11 $pool->command($command . $chunked->get(1)->pluck('id'));12 $pool->command($command . $chunked->get(2)->pluck('id'));13 $pool->command($command . $chunked->get(3)->pluck('id'));14 $pool->command($command . $chunked->get(4)->pluck('id'));15 })->start();16 17 while ($pool->running()->isNotEmpty()) {18 usleep(10000); // 0,01s19 }20 });21}
Le while
de 0.01s est nécessaire pour attendre la fin d’exécution des différentes pools avant de reprendre un nouveau lot de 10'000 utilisateurs.
Pour finir, ajoutons une progress bar pour visualiser l’avancé de la commande :
1public function handle(): void 2{ 3 $chunkSize = 10_000; 4 5 $bar = $this->output->createProgressBar(User::count() / $chunkSize); 6 7 User::chunkById($chunkSize, function ($users) use ($bar) { 8 9 $chunked = $users->chunk(2000);10 11 $command = 'php artisan send-newsletter --ids=';12 13 $pool = Process::pool(function (Pool $pool) use ($command, $chunked) {14 $pool->command($command . $chunked->get(0)->pluck('id'));15 $pool->command($command . $chunked->get(1)->pluck('id'));16 $pool->command($command . $chunked->get(2)->pluck('id'));17 $pool->command($command . $chunked->get(3)->pluck('id'));18 $pool->command($command . $chunked->get(4)->pluck('id'));19 })->start();20 21 while ($pool->running()->isNotEmpty()) {22 usleep(10000); // 0,01s23 }24 25 $bar->advance(); 26 });27 28 $bar->finish(); 29}
Notre commande SendNewsletterPool
est désormais fonctionnelle, elle ne nous reste plus qu'à la lancer :
1php artisan send-newsletter-pool
Lors de son exécution, vous pouvez utiliser un ps aux
pour visualiser les différentes pools en cours de traitement.
1ps aux | grep "artisan send-newsletter"
C’est terminé, grâce à la class Process, nous venons d'accélérer le lancement de nos newsletters en exécutant 5 fois en parallèle la commande SendNewsletter
!
Si vous voulez le tester par vous-même, tout le code de ce tutoriel est disponible via le lien du code source en bas de page !
Source : https://github.com/laravel-fr/support-process/tree/master/app/Console/Commands
A lire
Autres articles de la même catégorie
Notifier ses utilisateurs via les websockets
Dans ce tutoriel, on va s'appuyer sur un outil puissant de Laravel pour notifier facilement nos utilisateurs via websocket.
Envoyer des notifications sur Discord
A travers ce tutoriel, nous allons découvrir comment envoyer une notification sur Discord lorsqu’un événement ce produit.
Les bases 2/6 : Création du contrôleur
Découverte des contrôleurs au sein d'une application Laravel