Lots de jobs dans Laravel

Quand est-il utile d’utiliser des lots de “jobs” dans Laravel ?

Nous avons abordé la question des files d’attente dans un article précédent. Les files d’attente sont un outil utile dans Laravel, qui te permettent de gérer des jobs complexes ou longues en arrière-plan. Il y aura des moments où tu devras exécuter des dizaines, voire des centaines d’instances d’un job — par exemple, pour créer des rapports individuels pour un grand nombre de clients ou pour importer un dossier de fichiers CSV.

Pour y parvenir, tu pourrais envisager d’exécuter une seule jobs contenant une boucle pour appliquer la logique nécessaire. Toutefois, cette approche pose un problème d’évolutivité. Elle pourrait bien fonctionner si tu as un petit nombre de rapports à générer ou de fichiers à importer, mais à un moment donné, le temps d’exécution et la consommation de mémoire de la tâche pourraient excéder les limites que tu as établies dans la configuration de ton PHP et de ton serveur.

Bien que tu puisses augmenter ces limites, une méthode plus efficace serait de traiter chaque rapport comme une tâche séparée – dont le temps d’exécution et la consommation de mémoire restent relativement faibles – et de générer un fichier .zip une fois toutes les tâches achevées. Pour réaliser cela, tu peux utiliser la mise en lots des jobs.

Qu’est-ce que le “Job Batching” ?

La mise en lots des jobs est une fonctionnalité de Laravel. Comme son nom l’indique, elle te permet d’exécuter un lot de jobs. Les données relatives au lot sont stockées dans une table appelée job_batches; elle contient des informations utiles sur le lot, telles que le nombre total de jobs, le nombre de jobs ayant échoué, le nombre de jobs encore en cours, etc. Cette table est créée à l’aide des commandes suivantes :

php artisan queue:batches-table
php artisan migrate

En outre, tu peux définir un certain nombre de fonctions de rappel qui s’exécutent lorsque certains événements se produisent. Il s’agit notamment de :

  • Progress – s’exécute lorsqu’un seul travail est terminé avec succès
  • Then – s’exécute lorsque tous les travaux sont terminés avec succès
  • Catch – s’exécute la première fois qu’un échec de travail est détecté
  • Finally – s’exécute lorsque tous les travaux ont été exécutés (avec ou sans succès)

Création d’un lot de jobs

Afin de démontrer ce que tu peux accomplir avec les lots, nous allons utiliser l’exemple de la création d’une facture mensuelle au format PDF pour chacun des clients d’un système de facturation hypothétique. Pour nos besoins, les factures seront sauvegardées dans un dossier .zip afin d’être examinées avant d’être envoyées. Le flux de travail ressemblera à ceci :

  1. Trouver tous les clients qui ont besoin d’une facture.
  2. Passer en revue chacun des clients, générer la facture et enregistrer le PDF sur le serveur.
  3. Zipper les factures dans un seul fichier.
  4. Envoyer un courriel à l’utilisateur qui a demandé les factures avec un lien vers le fichier .zip.
  5. Si l’un des PDF échoue, annuler et informer l’utilisateur qu’il y a eu un problème.

Nous allons garder les choses simples et supposer qu’il existe un modèle de facture qui possède des méthodes pour trouver la liste des clients et générer la facture. La gestion des erreurs sera également minimale et nous nous contenterons d’envoyer un courriel si l’une des factures échoue ; nous parlerons plus tard des améliorations possibles.

Pour commencer, nous allons créer la job permettant de générer une seule facture au format PDF. Nous transmettrons l’identifiant de la facture et, dans le gestionnaire, nous trouverons la facture appropriée et appellerons la méthode pour générer et enregistrer le PDF de la facture. Afin d’en faire un travail par lots, nous devrons ajouter le trait Illuminate\Bus\Batchable à la classe du job. Ce trait donne accès à une méthode batch qui nous permettra de récupérer le lot auquel le job appartient.

Nous allons commencer par créer un job appelé CreateInvoice à l’aide d’une commande artisanale.

php artisan make:job CreateInvoice

Après avoir ajouté le trait Illuminate\Bus\Batchable et le code pour générer la facture, la classe de job ressemble à ceci :

<?php

namespace App\Jobs;

use Illuminate\Bus\Batchable;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use App\Models\Invoice;

class CreateInvoice implements ShouldQueue
{
    use Batchable, Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    private $invoice_id;

    /**
     * Create a new job instance.
     */
    public function __construct($invoice_id)
    {
        $this->invoice_id = $invoice_id;
    }

    /**
     * Execute the job.
     */
    public function handle(): void
    {
        if ($this->batch()->cancelled()) {
            // Determine if the batch has been cancelled...

            return;
        }
        
        $invoice = Invoice::find($this->invoice_id);
        $invoice->createPDF();
    }
}

Le gestionnaire se compose maintenant de 3 parties principales :

  1. Une instruction if qui détecte si le lot a été annulé – cela se produit le plus souvent si un job échoue. Cela permet d’éviter que d’autres factures soient générées en cas d’échec de l’une d’entre elles.
  2. Création d’un objet facture à l’aide de l’identifiant transmis dans le job
  3. Appeler la méthode createPDF dans le modèle de facture pour créer une version PDF de la facture et l’enregistrer sur le disque.

Pour exécuter notre nouveau job par lots, nous devons appeler la méthode batch de la façade Illuminate\Bus\Batch . Nous allons commencer par un appel basique, en créant simplement une poignée de factures à titre d’exemple :

use App\Jobs\CreateInvoice;
use Illuminate\Bus\Batch;
use Illuminate\Support\Facades\Bus;
use Throwable;

$batch = Bus::batch([
    new CreateInvoice(1),
    new CreateInvoice(2),
    new CreateInvoice(3),
    new CreateInvoice(4),
])->then(function (Batch $batch) {
    // All jobs completed successfully...
})->catch(function (Batch $batch, Throwable $e) {
    // First batch job failure detected...
})->dispatch();

Nous avons appelé la fonction batch, en passant quatre jobs CreateInvoice. Nous avons également défini la fonction then callback, qui nous permet de zipper les fichiers et d’envoyer un lien vers le fichier, ainsi que la fonction catch callback, qui nous permet d’envoyer un message d’erreur à l’utilisateur au cas où l’un des job échouerait.

Bien sûr, nous allons vouloir créer plus de quatre factures. Nous allons à nouveau abstraire les choses et supposer que nous avons une méthode statique dans le modèle de facture, appelée getInvoicesByMonth, qui renvoie une liste de factures pour un mois spécifique, ainsi qu’une méthode, appelée createZipFile, qui zippera les factures et renverra un lien vers le fichier résultant. Nous allons maintenant créer un nouveau job par lots appelé LoadInvoiceBatch:

<?php

namespace App\Jobs;

use App\Models\Invoice;
use Illuminate\Bus\Batchable;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

class LoadInvoiceBatch implements ShouldQueue
{
    use Batchable, Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    private $month;

    /**
     * Create a new job instance.
     */
    public function __construct($month)
    {
        $this->$month = $month;
    }

    /**
     * Execute the job.
     */
    public function handle(): void
    {
        if ($this->batch()->cancelled()) {
            return;
        }

        $invoice_list = Invoice::getInvoicesByMonth($this->month);
        foreach ($invoice_list as $invoice) {
            $this->batch()->add(new CreateInvoice($invoice->id));
        }
    }
}

Avec ceci en place, nous pouvons exécuter notre nouveau job et définir nos rappels :

use App\Jobs\LoadInvoiceBatch;
use App\Models\Invoice;
use App\Notifications\InvoiceError;
use App\Notifications\InvoicesCompleted;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Bus;

$batch = Bus::batch([
    new LoadInvoiceBatch($month)
])->then(function() {
    $link = Invoice::createZipFile();
    Auth::user()->notify(new InvoicesCompleted($link));
})->catch(function() {
    Auth::user()->notify(new InvoiceError());
})->dispatch();

Lors de la création du lot, nous l’avons stocké dans une variable appelée $batch; celle-ci possède un certain nombre de propriétés et de méthodes utiles qui nous donnent des informations sur le lot, notamment l’ID du lot, le nombre total de job dans le lot, le nombre de job en attente, le nombre de job échoués, le pourcentage de job terminés, et bien plus encore. Il existe également une méthode pour annuler le job manuellement (si tu en as besoin). Cet exemple était assez simple ; tu peux faire plus avec les lots, comme essayer de relancer les job qui ont échoué dans le lot. Tu trouveras plus d’informations dans la documentation de Laravel.