> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# Jobs en file d'attente d'un package et compatibilité lors des mises à jour

> Analyse la sauvegarde et la restauration des jobs dans Laravel 13 et explique la compatibilité des noms de classe et des propriétés, la récupération des modèles, les tests d'anciens payloads et l'ordre de mise à jour des workers.

Faites en sorte que les jobs mis en file avant une mise à jour du package puissent encore être traités après. Ce que la file d'attente stocke n'est pas le code à exécuter, mais le nom de la classe du job et son état. En plus des tests unitaires du nouveau code, il vous faut **des tests qui restaurent avec le nouveau code un état sauvegardé par l'ancien code**.

> [!NOTE]
>
> Cette page cible Laravel 13 et vérifie l'implémentation du Framework `v13.42.0`. Elle suppose les [bases des files d'attente](https://kawax.biz/fr/queues.md) et les [tests de package](https://kawax.biz/fr/advanced/package-testing.md). La conception de la compatibilité ci-dessous est une politique côté package. Laravel ne fournit aucune fonctionnalité qui migre automatiquement vos propres formats de payload.

## Le code change entre la sauvegarde et l'exécution

Pour les jobs objet, `Queue::createObjectPayload()` exécute `serialize(clone $job)` et place le résultat dans `data.command` du payload. Lorsque le chiffrement est activé, la même sérialisation a lieu avant.

Côté worker, `CallQueuedHandler::getCommand()` applique `unserialize()` aux données stockées et transmet le job restauré au Dispatcher. Un `handle()` classique s'exécute avec le code et le conteneur de services dont dispose le worker à ce moment-là.

```mermaid theme={null}
sequenceDiagram
    participant P as Côté émetteur<br>ancienne version du package
    participant Q as File d'attente
    participant W as Worker<br>nouvelle version du package

    P->>Q: Sauvegarder le nom de classe et les propriétés
    Note over P,W: Le déploiement met à jour le code
    Q->>W: Récupérer le payload sauvegardé
    W->>W: Restaurer dans la classe actuelle
    W->>W: Exécuter le handle() actuel
```

Si vous ne redémarrez pas les workers de longue durée, l'ancien code continue de tourner après la mise à jour. Avec un déploiement progressif, envisagez non seulement « ancien émetteur avec nouveau worker », mais aussi « nouvel émetteur avec ancien worker ».

| Ce que vous changez | Effet sur les jobs sauvegardés |
| - | - |
| Nom de classe complet du job | Si la classe ne peut pas être chargée sous l'ancien nom, le job ne peut pas être restauré correctement |
| Nom et visibilité des propriétés | Les clés à la sauvegarde et à la restauration peuvent ne plus correspondre |
| Type des propriétés | Les anciennes valeurs peuvent ne pas être assignables et la restauration peut échouer |
| Valeur par défaut déclarée des propriétés | Le sens des valeurs dont la sauvegarde a été omise peut changer |
| `handle()` ou les services dont il dépend | Même si la restauration réussit, le résultat du traitement des anciennes données peut changer |

Ne jugez pas le renommage ou la suppression d'une classe uniquement selon que les utilisateurs l'appellent directement. Les jobs en attente, différés ou en attente de nouvelle tentative, ainsi que la durée de conservation des jobs échoués, font aussi partie de la compatibilité.

## Ce que SerializesModels sauvegarde

`Illuminate\Foundation\Queue\Queueable` utilise `SerializesModels` en interne. En plus du remplacement des modèles, il fournit `__serialize()`, qui énumère les propriétés du job lui-même, et `__unserialize()`, qui remet les valeurs en place.

### Les valeurs égales à la valeur par défaut déclarée ne sont pas sauvegardées

`__serialize()` exclut les propriétés statiques et non initialisées, et **omet aussi la sauvegarde lorsque la valeur actuelle est strictement égale à la valeur par défaut déclarée de la propriété**.

```php theme={null}
public string $format = 'csv';
```

Si cette propriété vaut toujours `'csv'`, le tableau sauvegardé ne contient pas `format`. Si une version ultérieure change la valeur par défaut déclarée en `'json'`, `'json'` peut aussi s'appliquer aux anciens jobs. Ce n'est pas un changement cosmétique de valeur par défaut, mais un changement dans la lecture des anciennes données.

### Le constructeur n'est pas rappelé lors de la restauration

`__unserialize()` assigne des valeurs aux propriétés qui existent dans la classe actuelle et qui ont une clé dans le tableau sauvegardé. Il n'exécute aucun complément ni aucune validation des entrées via le constructeur.

```php theme={null}
public function __construct(
    public string $format = 'csv',
) {}
```

Le `'csv'` de cet exemple est **la valeur par défaut d'un argument du constructeur**. Ce n'est pas la valeur par défaut déclarée de la propriété elle-même, donc elle ne complète pas la valeur lorsque vous restaurez un ancien payload sans `format`. Si une propriété typée ajoutée reste non initialisée, l'accès ultérieur échoue.

Par ailleurs, la clé d'une propriété public est son nom, celle d'une propriété protected porte un préfixe de visibilité, et celle d'une propriété private inclut aussi le nom de la classe. Vérifiez aussi avec d'anciens payloads un passage de public à private.

> [!WARNING]
>
> `SerializesModels` énumère les propriétés du job. Ne supposez pas que la même conversion s'applique récursivement à l'intérieur de DTO arbitraires ou aux modèles enfouis dans des tableaux. Si vous transmettez des objets autres que des modèles, leurs classes et formats de sauvegarde font aussi partie de la compatibilité.

## Séparer les identifiants des services d'exécution

Stockez dans le job les identifiants et options nécessaires à l'exécution, et injectez dans `handle()` les services d'exécution tels que les clients d'API ou les exportateurs. Si vous passez un service au constructeur et le conservez, son état interne peut aussi être sauvegardé.

L'exemple suivant ajoute un choix de format et une version de payload à un job du même nom qui, dans l'ancienne version, ne contenait que `reportId`. `ReportExporter` est un contrat que le package fournit et lie dans le conteneur, avec `export(string $reportId, string $format): void`.

```php theme={null}
<?php

namespace Acme\Reports\Jobs;

use Acme\Reports\Contracts\ReportExporter;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use UnexpectedValueException;

class GenerateReport implements ShouldQueue
{
    use Queueable;

    public string $format = 'csv';

    public int $payloadVersion = 1;

    public function __construct(
        public string $reportId,
        string $format = 'csv',
    ) {
        $this->format = $format;
        $this->payloadVersion = 2;
    }

    public function handle(ReportExporter $exporter): void
    {
        if (! in_array($this->payloadVersion, [1, 2], true)) {
            throw new UnexpectedValueException('Unsupported report payload version.');
        }

        $exporter->export($this->reportId, $this->format);
    }
}
```

Cette conception sépare le sens des valeurs manquantes des valeurs nouvellement mises en file.

* Si un ancien payload n'a pas de `format`, la valeur par défaut déclarée `'csv'` est utilisée.
* Si un ancien payload n'a pas de `payloadVersion`, elle est traitée comme la valeur par défaut déclarée `1`.
* Pour les nouveaux jobs, le constructeur fixe `payloadVersion` à `2`. Comme elle diffère de la valeur par défaut déclarée, cette valeur est sauvegardée.
* Si le `format` d'un nouveau job est `'json'`, cette valeur est aussi sauvegardée. `'csv'` est omis, conservez donc à l'avenir la valeur par défaut déclarée `'csv'`.

Si vous voulez plus tard changer uniquement le format par défaut des nouveaux jobs, vous pouvez modifier la valeur par défaut de l'argument du constructeur et l'affectation, et les examiner séparément de la valeur par défaut de la propriété utilisée pour les anciennes données. Gérez la version de payload non comme le numéro de version du package, mais comme **la génération du format de sauvegarde et de son interprétation**.

> [!TIP]
>
> Ajouter un numéro de version ne suffit pas à assurer la compatibilité. La nouvelle version de cet exemple sait aussi traiter l'ancien format, mais rien ne garantit que les anciens workers comprennent le nouveau `format` ou `payloadVersion`. Il vous faut aussi l'ordre de mise à jour de l'émetteur et des workers décrit plus bas.

## Distinguer la transmission de modèles des instantanés

Si vous stockez un modèle directement dans une propriété, `SerializesAndRestoresModelIdentifiers` le remplace par un `ModelIdentifier` contenant la classe, l'ID, le nom de connexion, les noms des relations chargées, etc. Lors de la restauration d'un modèle unique, il le récupère à nouveau en base avec `firstOrFail()` et charge les relations concernées.

Ce n'est pas un mécanisme qui fige les attributs ou les données liées au moment de la mise en file.

* Si l'enregistrement est mis à jour avant l'exécution, le modèle restauré reflète le contenu mis à jour.
* Les contraintes appliquées au préalable aux relations ne sont pas sauvegardées, réappliquez donc les conditions nécessaires à l'exécution.
* Excluez de la sauvegarde les relations inutiles avec `withoutRelations()` ou `#[WithoutRelations]`.
* Si le modèle a été supprimé, le job peut échouer lors de la restauration, avant son `handle()`.

S'il est acceptable d'abandonner les jobs dont le modèle a été supprimé, vous pouvez envisager l'attribut officiel `#[DeleteWhenMissingModels]`. Toutefois, le package ne doit pas faire de l'abandon le comportement par défaut de lui-même, mais exposer la politique d'échec aux utilisateurs. Si vous ne transmettez qu'un ID et faites la recherche dans `handle()`, vous contrôlez vous-même l'échec ou l'omission de cette recherche.

Si vous avez besoin du contenu au moment de la mise en file, sauvegardez explicitement les valeurs nécessaires, ou enregistrez en base par exemple une demande de rapport immuable et transmettez son ID. Distinguez le simple passage d'un modèle de la sauvegarde d'un instantané des données métier.

### La mise en file après le commit est un autre problème

Lorsque vous traitez des enregistrements créés dans une transaction, empêchez les workers de les lire avant le commit.

```php theme={null}
use Acme\Reports\Jobs\GenerateReport;
use Illuminate\Support\Facades\DB;

DB::transaction(function () use ($reportId) {
    // Créer ou mettre à jour l'enregistrement cible dans cette transaction.

    GenerateReport::dispatch($reportId)->afterCommit();
});
```

`afterCommit()` attend le commit de la transaction parente ouverte avant de mettre en file, et abandonne la mise en file en cas de rollback. Il ne résout ni la compatibilité des noms de classe et des propriétés, ni l'exécution en double de traitements externes.

## Séparer les fakes des tests d'anciens payloads

Un `Queue::fake()` classique enregistre les mises en file. Un `assertPushed()` qui réussit ne confirme pas à lui seul la sauvegarde et la restauration, ni que la nouvelle version traite les anciens payloads.

Vous pouvez vérifier un aller-retour avec la classe actuelle ainsi.

```php theme={null}
use Acme\Reports\Jobs\GenerateReport;
use Illuminate\Support\Facades\Queue;

Queue::fake()->serializeAndRestore();

GenerateReport::dispatch('report-001', 'json');

Queue::assertPushed(GenerateReport::class, function (GenerateReport $job) {
    return $job->reportId === 'report-001'
        && $job->format === 'json'
        && $job->payloadVersion === 2;
});
```

`serializeAndRestore()` exécute `serialize()` et `unserialize()` dans le fake. Il ne vérifie cependant ni la mise en file vers un vrai backend, ni l'exécution par un worker, ni la génération par l'ancien code.

### Conserver des fixtures générées par l'ancienne version

Tant que l'ancien `GenerateReport` ne contient que `reportId`, générez un job fixe sans données réelles. Le code de préparation suivant s'exécute une fois dans l'environnement de test de l'ancienne version. Créez `tests/fixtures/` au préalable.

```php theme={null}
use Acme\Reports\Jobs\GenerateReport;

file_put_contents(
    __DIR__.'/fixtures/generate-report-v1.serialized',
    serialize(new GenerateReport('report-legacy')),
);
```

Placez la fixture sous contrôle de version et chargez-la depuis les tests de la nouvelle version. La régénérer avec la nouvelle version à chaque exécution ne vérifierait plus la rétrocompatibilité, gardez-la donc figée.

```php theme={null}
<?php

namespace Acme\Reports\Tests;

use Acme\Reports\Jobs\GenerateReport;

class JobCompatibilityTest extends TestCase
{
    public function test_new_payload_keeps_explicit_values(): void
    {
        $job = unserialize(serialize(new GenerateReport('report-001', 'json')));

        $this->assertInstanceOf(GenerateReport::class, $job);
        $this->assertSame('report-001', $job->reportId);
        $this->assertSame('json', $job->format);
        $this->assertSame(2, $job->payloadVersion);
    }

    public function test_default_format_is_omitted_but_new_version_is_saved(): void
    {
        $values = (new GenerateReport('report-001'))->__serialize();

        $this->assertArrayNotHasKey('format', $values);
        $this->assertSame(2, $values['payloadVersion']);
    }

    public function test_old_payload_uses_legacy_defaults(): void
    {
        $job = unserialize(file_get_contents(
            __DIR__.'/fixtures/generate-report-v1.serialized',
        ));

        $this->assertInstanceOf(GenerateReport::class, $job);
        $this->assertSame('report-legacy', $job->reportId);
        $this->assertSame('csv', $job->format);
        $this->assertSame(1, $job->payloadVersion);
    }
}
```

Le `TestCase` de cet exemple est la classe de base que vous préparez dans l'[environnement de test du package](https://kawax.biz/fr/advanced/package-testing.md). Ne passez à `unserialize()` que des fixtures que vous gérez vous-même, jamais d'entrées externes.

Exécutez aussi le `handle()` de l'ancien job restauré avec un exportateur de test et vérifiez la sortie et les résultats sauvegardés. Si vous transmettez des modèles, vérifiez séparément la mise à jour, la suppression, la connexion et la récupération des relations et, sur la connexion de file réellement utilisée, la mise en file après le commit et les nouvelles tentatives.

## Mettre à jour le côté lecteur avant de mettre en file le nouveau format

**1. Préparer une version capable de lire l'ancien format**

Distribuez d'abord un code qui conserve l'ancien nom de classe et l'interprétation des anciennes valeurs, tout en sachant lire le nouveau format. Avec les fixtures, vérifiez non seulement la restauration mais aussi le résultat du traitement.

**2. Vérifier qu'aucun ancien worker ne subsiste**

Intégrez le remplacement des workers à la procédure de déploiement de l'application utilisatrice. Avec un `queue:work` classique, `php artisan queue:restart` demande l'arrêt après le job en cours, et vous vérifiez que le gestionnaire de processus démarre la nouvelle version. Le signal de redémarrage passe par le cache.

**3. Activer la mise en file du nouveau format**

Une fois le côté lecteur prêt, basculez vers la mise en file du nouveau format, par exemple avec un feature flag. Si la simple distribution du même code commence aussi à générer le nouveau format, il faut une procédure qui ne le livre pas aux anciens workers, comme suspendre la mise en file ou séparer anciennes et nouvelles files avec les workers correspondants.

**4. Maintenir le support pendant la période de nouvelles tentatives et de rollback**

Vérifiez aussi les nouvelles tentatives de jobs échoués et les jobs mis en file plus tard au sein de chaînes. Si vous revenez à l'ancien code après avoir sauvegardé le nouveau format, vérifiez que l'ancien code peut le lire. Ne supprimez pas le support des anciennes classes ou formats simplement parce que la file a été vide une fois.

`queue:restart` n'est pas une commande qui réécrit les payloads sauvegardés. Dans les notes de mise à jour du package, indiquez les formats pris en charge, le remplacement de workers nécessaire et les conditions de retrait de l'ancien format. Consignez le même changement dans la [gestion de la compatibilité des versions](https://kawax.biz/fr/advanced/package-versioning.md) et la [gestion du changelog](https://kawax.biz/fr/advanced/package-changelog.md).

## Sources primaires consultées

* [Documentation officielle de Laravel : structure des jobs et injection de dépendances](https://github.com/laravel/docs/blob/13.x/queues.md#class-structure)
* [Documentation officielle de Laravel : mise en file après le commit](https://github.com/laravel/docs/blob/13.x/queues.md#jobs-and-database-transactions)
* [Documentation officielle de Laravel : workers et déploiement](https://github.com/laravel/docs/blob/13.x/queues.md#queue-workers-and-deployment)
* [Documentation officielle de Laravel : gestion des modèles supprimés](https://github.com/laravel/docs/blob/13.x/queues.md#ignoring-missing-models)
* [Queue : création des payloads d'objet](https://github.com/laravel/framework/blob/v13.42.0/src/Illuminate/Queue/Queue.php)
* [Queueable : composition du trait de job](https://github.com/laravel/framework/blob/v13.42.0/src/Illuminate/Foundation/Queue/Queueable.php)
* [SerializesModels : sauvegarde et restauration des propriétés](https://github.com/laravel/framework/blob/v13.42.0/src/Illuminate/Queue/SerializesModels.php)
* [SerializesAndRestoresModelIdentifiers : remplacement et récupération des modèles](https://github.com/laravel/framework/blob/v13.42.0/src/Illuminate/Queue/SerializesAndRestoresModelIdentifiers.php)
* [CallQueuedHandler : restauration et exécution](https://github.com/laravel/framework/blob/v13.42.0/src/Illuminate/Queue/CallQueuedHandler.php)
* [QueueFake : sauvegarde et restauration dans le fake](https://github.com/laravel/framework/blob/v13.42.0/src/Illuminate/Support/Testing/Fakes/QueueFake.php)
* [Tests officiels de Laravel : sérialisation des modèles](https://github.com/laravel/framework/blob/v13.42.0/tests/Integration/Queue/ModelSerializationTest.php)


## Related topics

- [Tester des packages Laravel avec Orchestra Testbench](https://kawax.biz/fr/advanced/package-testing.md)
- [Sujets avancés](https://kawax.biz/fr/advanced/index.md)
- [Morph maps d'un package et compatibilité des données stockées](https://kawax.biz/fr/advanced/package-morph-maps.md)
- [Conception du cycle de vie des services d'un package](https://kawax.biz/fr/advanced/package-service-lifetimes.md)
- [Enregistrement et mise en cache des écouteurs d'événements d'un package](https://kawax.biz/fr/advanced/package-events.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.