Explorons la nouvelle class Process

Publié le 14 mars 2023 par Mathieu De Gracia
Couverture de l'article Explorons la nouvelle class Process

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 string
13 */
14 protected $signature = 'send-newsletter {--ids=}';
15 
16 /**
17 * The console command description.
18 *
19 * @var string
20 */
21 protected $description = 'Launch users newsletters.';
22 
23 /**
24 * Execute the console command.
25 */
26 public function handle(): void
27 {
28 $ids = $this->option('ids');
29 
30 $userIds = explode(',', $ids);
31 
32 foreach($userIds as $id) {
33 // launch newsletters
34 }
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 string
13 */
14 protected $signature = 'send-newsletter-pool';
15 
16 /**
17 * The console command description.
18 *
19 * @var string
20 */
21 protected $description = 'Launch users newsletters simultaneously.';
22 
23 /**
24 * Execute the console command.
25 */
26 public function handle(): void
27 {
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(): void
2{
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(), // 2000
10 $chunked->get(1)->count(), // 2000
11 $chunked->get(2)->count(), // 2000
12 $chunked->get(3)->count(), // 2000
13 $chunked->get(4)->count(), // 2000
14 );
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,01s
19 }
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,01s
23 }
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
Mathieu De Gracia avatar
Mathieu De Gracia
Des fois, mon chat code à ma place

A lire

Autres articles de la même catégorie