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

# Queue-jobs in packages en compatibiliteit bij updates

> Leest hoe Laravel 13 jobs opslaat en herstelt en behandelt de compatibiliteit van klassenamen en properties, het opnieuw ophalen van modellen, het testen van oude payloads en de volgorde waarin je workers bijwerkt.

Zorg dat jobs die vóór een package-update in de queue zijn gezet, daarna nog verwerkt kunnen worden. De queue slaat niet de uit te voeren code op, maar de naam van de jobklasse en haar toestand. Naast unittests voor de nieuwe code heb je **tests nodig die een door de oude code opgeslagen toestand met de nieuwe code herstellen**.

> [!NOTE]
>
> Deze pagina richt zich op Laravel 13 en controleert de implementatie van Framework `v13.42.0`. Ze gaat uit van de [basis van queues](https://kawax.biz/nl/queues.md) en het [testen van packages](https://kawax.biz/nl/advanced/package-testing.md). Het compatibiliteitsontwerp hieronder is beleid aan de kant van het package. Laravel heeft geen functie die je eigen payloadformaten automatisch migreert.

## De code verandert tussen opslaan en uitvoeren

Bij objectjobs voert `Queue::createObjectPayload()` `serialize(clone $job)` uit en zet het resultaat in `data.command` van de payload. Bij versleuteling vindt dezelfde serialisatie daarvoor plaats.

Aan de workerkant voert `CallQueuedHandler::getCommand()` `unserialize()` uit op de opgeslagen data en geeft de herstelde job door aan de Dispatcher. Een gewone `handle()` draait met de code en de service container die de worker op dat moment heeft.

```mermaid theme={null}
sequenceDiagram
    participant P as Dispatchende kant<br>oude packageversie
    participant Q as Queue
    participant W as Worker<br>nieuwe packageversie

    P->>Q: Klassenaam en properties opslaan
    Note over P,W: Deploy werkt de code bij
    Q->>W: Opgeslagen payload ophalen
    W->>W: Herstellen in de huidige klasse
    W->>W: Huidige handle() uitvoeren
```

Als je langlopende workers niet herstart, blijft na de update de oude code draaien. Houd bij rolling deploys niet alleen rekening met "oude dispatcher met nieuwe worker", maar ook met "nieuwe dispatcher met oude worker".

| Wat je wijzigt | Effect op opgeslagen jobs |
| - | - |
| Volledig gekwalificeerde klassenaam van de job | Kan de klasse niet onder de oude naam worden geladen, dan wordt de job niet correct hersteld |
| Naam en zichtbaarheid van properties | De sleutels bij opslaan en herstellen komen mogelijk niet meer overeen |
| Type van properties | Oude waarden zijn mogelijk niet toewijsbaar, waardoor herstellen kan mislukken |
| Gedeclareerde standaardwaarde van properties | De betekenis van waarden waarvan het opslaan is overgeslagen kan veranderen |
| `handle()` of de services waarvan het afhangt | Ook als herstellen lukt, kan het resultaat van het verwerken van oude data veranderen |

Beoordeel het hernoemen of verwijderen van een klasse niet alleen op de vraag of gebruikers die klasse direct aanroepen. Wachtende, uitgestelde en op een nieuwe poging wachtende jobs, en de bewaartermijn van mislukte jobs, horen ook bij compatibiliteit.

## Wat SerializesModels opslaat

`Illuminate\Foundation\Queue\Queueable` gebruikt intern `SerializesModels`. Naast het vervangen van modellen levert het `__serialize()`, dat de properties van de job zelf opsomt, en `__unserialize()`, dat de waarden terugzet.

### Waarden gelijk aan de gedeclareerde standaardwaarde worden niet opgeslagen

`__serialize()` sluit statische en niet-geïnitialiseerde properties uit en **slaat het opslaan ook over wanneer de huidige waarde strikt gelijk is aan de gedeclareerde standaardwaarde van de property**.

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

Blijft deze property `'csv'`, dan bevat de opgeslagen array geen `format`. Verandert een latere release de gedeclareerde standaardwaarde in `'json'`, dan kan `'json'` ook voor oude jobs gelden. Dat is geen cosmetische wijziging van de standaardwaarde, maar een wijziging in hoe oude data wordt gelezen.

### De constructor wordt bij herstellen niet opnieuw aangeroepen

`__unserialize()` wijst waarden toe aan properties die in de huidige klasse bestaan en waarvoor de opgeslagen array een sleutel heeft. Aanvulling of validatie van invoer via de constructor vindt niet plaats.

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

De `'csv'` in dit voorbeeld is **de standaardwaarde van een constructorargument**. Het is niet de gedeclareerde standaardwaarde van de property zelf, dus vult het de waarde niet aan wanneer je een oude payload zonder `format` herstelt. Blijft een toegevoegde getypeerde property niet-geïnitialiseerd, dan mislukt een latere toegang.

Verder is de sleutel van een public property haar naam, die van een protected property heeft een prefix voor de zichtbaarheid en die van een private property bevat ook de klassenaam. Controleer ook een wijziging van public naar private met oude payloads.

> [!WARNING]
>
> `SerializesModels` somt de properties van de job op. Ga er niet van uit dat dezelfde omzetting recursief plaatsvindt binnen willekeurige DTO's of voor modellen diep in arrays. Geef je andere objecten dan modellen door, dan horen hun klassen en opslagformaten ook bij compatibiliteit.

## Identifiers en uitvoerende services scheiden

Sla de voor uitvoering benodigde identifiers en opties op in de job, en injecteer uitvoerende services zoals API-clients of exporters in `handle()`. Geef je een service aan de constructor door en bewaar je die, dan kan ook haar interne toestand worden opgeslagen.

Het volgende voorbeeld voegt een formaatkeuze en een payloadversie toe aan een job met dezelfde naam die in de oude versie alleen `reportId` had. `ReportExporter` is een contract dat het package levert en in de container bindt, met `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);
    }
}
```

Dit ontwerp scheidt de betekenis van ontbrekende waarden van de waarden die nieuw worden gedispatcht.

* Heeft een oude payload geen `format`, dan wordt de gedeclareerde standaardwaarde `'csv'` gebruikt.
* Heeft een oude payload geen `payloadVersion`, dan wordt die behandeld als de gedeclareerde standaardwaarde `1`.
* Bij nieuw aangemaakte jobs zet de constructor `payloadVersion` op `2`. Omdat die afwijkt van de gedeclareerde standaardwaarde, wordt deze waarde opgeslagen.
* Is de `format` van een nieuwe job `'json'`, dan wordt ook die waarde opgeslagen. `'csv'` wordt overgeslagen, dus houd de gedeclareerde standaardwaarde `'csv'` voortaan aan.

Wil je later alleen het standaardformaat van nieuwe jobs wijzigen, dan kun je de standaardwaarde van het constructorargument en de toewijzing aanpassen en die los bekijken van de propertystandaardwaarde voor oude data. Beheer de payloadversie niet als releasenummer van het package, maar als **generatie van het opslagformaat en de interpretatie ervan**.

> [!TIP]
>
> Alleen een versienummer toevoegen levert geen compatibiliteit op. De nieuwe versie in dit voorbeeld kan ook het oude formaat verwerken, maar niets garandeert dat oude workers de nieuwe `format` of `payloadVersion` begrijpen. Je hebt ook de hieronder beschreven volgorde voor het bijwerken van dispatcher en workers nodig.

## Doorgegeven modellen onderscheiden van snapshots

Sla je een model direct op in een property, dan vervangt `SerializesAndRestoresModelIdentifiers` het door een `ModelIdentifier` met klasse, ID, verbindingsnaam, namen van geladen relaties enzovoort. Bij het herstellen van één model haalt het dit opnieuw uit de database met `firstOrFail()` en laadt het de betreffende relaties.

Dit is geen mechanisme dat attributen of gerelateerde data vastlegt op het moment van dispatchen.

* Wordt het record vóór de uitvoering bijgewerkt, dan ziet het herstelde model de bijgewerkte inhoud.
* Vooraf op relaties toegepaste beperkingen worden niet opgeslagen, dus pas de benodigde voorwaarden bij uitvoering opnieuw toe.
* Sluit overbodige relaties uit van opslag met `withoutRelations()` of `#[WithoutRelations]`.
* Is het model verwijderd, dan kan de job al bij het herstellen mislukken, vóór zijn `handle()`.

Mogen jobs met een verwijderd model worden weggegooid, dan kun je het officiële `#[DeleteWhenMissingModels]` overwegen. Het package moet weggooien echter niet op eigen houtje de standaard maken, maar het faalbeleid aan gebruikers tonen. Geef je alleen een ID door en zoek je in `handle()`, dan bepaal je zelf hoe die zoekactie faalt of wordt overgeslagen.

Heb je de inhoud op het moment van dispatchen nodig, sla de benodigde waarden dan expliciet op, of leg bijvoorbeeld een onveranderlijk rapportverzoek vast in de database en geef het ID daarvan door. Onderscheid het simpelweg doorgeven van een model van het opslaan van een snapshot van bedrijfsdata.

### Dispatchen na de commit is een apart probleem

Verwerk je records die binnen een transactie zijn aangemaakt, zorg dan dat workers ze niet vóór de commit gaan lezen.

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

DB::transaction(function () use ($reportId) {
    // Maak of werk het doelrecord bij in deze transactie.

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

`afterCommit()` wacht met dispatchen tot de open bovenliggende transactie is gecommit en gooit de dispatch weg bij een rollback. Het lost de compatibiliteit van klassenamen en properties of het dubbel uitvoeren van externe verwerking niet op.

## Fakes en tests van oude payloads scheiden

Een gewone `Queue::fake()` registreert dispatches. Een geslaagde `assertPushed()` bevestigt op zich niet het opslaan en herstellen, of dat de nieuwe versie oude payloads verwerkt.

Een round trip met de huidige klasse controleer je zo.

```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()` voert `serialize()` en `unserialize()` uit binnen de fake. Het controleert echter niet het dispatchen naar een echte backend, de uitvoering door een worker of het genereren door oude code.

### Met de oude versie gegenereerde fixtures bewaren

Zolang de oude `GenerateReport` alleen `reportId` heeft, genereer je een vaste job zonder echte data. De volgende voorbereidingscode voer je één keer uit in de testomgeving van de oude versie. Maak `tests/fixtures/` vooraf aan.

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

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

Zet de fixture onder versiebeheer en laad haar vanuit de tests van de nieuwe versie. Genereer je haar bij elke testrun opnieuw met de nieuwe versie, dan test je geen achterwaartse compatibiliteit meer, dus houd haar vast.

```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);
    }
}
```

De `TestCase` in dit voorbeeld is de basisklasse die je in de [testomgeving voor packages](https://kawax.biz/nl/advanced/package-testing.md) opzet. Geef aan `unserialize()` alleen fixtures die je zelf beheert, nooit externe invoer.

Voer daarnaast `handle()` van de herstelde oude job uit met een test-exporter en controleer de uitvoer en de opgeslagen resultaten. Geef je modellen door, controleer dan apart bijwerken, verwijderen, verbinding en het opnieuw ophalen van relaties, en op de queueverbinding die je echt gebruikt ook dispatchen na de commit en nieuwe pogingen.

## De lezende kant bijwerken voordat je het nieuwe formaat dispatcht

**1. Een release voorbereiden die het oude formaat kan lezen**

Lever eerst code die de oude klassenaam en de interpretatie van oude waarden behoudt en ook het nieuwe formaat kan lezen. Controleer met fixtures niet alleen het herstellen, maar ook het verwerkingsresultaat.

**2. Controleren dat er geen oude workers overblijven**

Neem het vervangen van workers op in de deployprocedure van de gebruikende app. Bij een gewone `queue:work` vraagt `php artisan queue:restart` om te stoppen na de huidige job, en je controleert dat de procesmanager de nieuwe versie start. Voor het herstartsignaal wordt de cache gebruikt.

**3. Het dispatchen van het nieuwe formaat inschakelen**

Schakel pas over op het dispatchen van het nieuwe formaat wanneer de lezende kant het ondersteunt, bijvoorbeeld met een feature flag. Begint het uitrollen van dezelfde code al met het genereren van het nieuwe formaat, dan heb je een procedure nodig die het bij oude workers weghoudt, zoals het dispatchen pauzeren of oude en nieuwe queues met bijbehorende workers scheiden.

**4. Ondersteuning behouden tijdens de periode voor nieuwe pogingen en rollback**

Controleer ook nieuwe pogingen van mislukte jobs en jobs die later binnen chains worden gedispatcht. Ga je na het opslaan van het nieuwe formaat terug naar oude code, controleer dan of de oude code het kan lezen. Verwijder ondersteuning voor oude klassen of formaten niet alleen omdat de queue een keer leeg was.

`queue:restart` is geen commando dat opgeslagen payloads herschrijft. Vermeld in de update-instructies van het package de ondersteunde formaten, de benodigde vervanging van workers en de voorwaarden om het oude formaat af te schaffen. Leg dezelfde wijziging ook vast in [versiecompatibiliteitsbeheer](https://kawax.biz/nl/advanced/package-versioning.md) en [changelogbeheer](https://kawax.biz/nl/advanced/package-changelog.md).

## Geraadpleegde primaire bronnen

* [Officiële Laravel-documentatie: structuur van jobs en dependency injection](https://github.com/laravel/docs/blob/13.x/queues.md#class-structure)
* [Officiële Laravel-documentatie: dispatchen na de commit](https://github.com/laravel/docs/blob/13.x/queues.md#jobs-and-database-transactions)
* [Officiële Laravel-documentatie: workers en deployment](https://github.com/laravel/docs/blob/13.x/queues.md#queue-workers-and-deployment)
* [Officiële Laravel-documentatie: omgaan met verwijderde modellen](https://github.com/laravel/docs/blob/13.x/queues.md#ignoring-missing-models)
* [Queue: objectpayloads aanmaken](https://github.com/laravel/framework/blob/v13.42.0/src/Illuminate/Queue/Queue.php)
* [Queueable: opbouw van de jobtrait](https://github.com/laravel/framework/blob/v13.42.0/src/Illuminate/Foundation/Queue/Queueable.php)
* [SerializesModels: properties opslaan en herstellen](https://github.com/laravel/framework/blob/v13.42.0/src/Illuminate/Queue/SerializesModels.php)
* [SerializesAndRestoresModelIdentifiers: modellen vervangen en opnieuw ophalen](https://github.com/laravel/framework/blob/v13.42.0/src/Illuminate/Queue/SerializesAndRestoresModelIdentifiers.php)
* [CallQueuedHandler: herstellen en uitvoeren](https://github.com/laravel/framework/blob/v13.42.0/src/Illuminate/Queue/CallQueuedHandler.php)
* [QueueFake: opslaan en herstellen in de fake](https://github.com/laravel/framework/blob/v13.42.0/src/Illuminate/Support/Testing/Fakes/QueueFake.php)
* [Officiële Laravel-tests: serialisatie van modellen](https://github.com/laravel/framework/blob/v13.42.0/tests/Integration/Queue/ModelSerializationTest.php)


## Related topics

- [Laravel-packages testen met Orchestra Testbench](https://kawax.biz/nl/advanced/package-testing.md)
- [Geavanceerde onderwerpen](https://kawax.biz/nl/advanced/index.md)
- [Praktische technieken voor Laravel Telescope](https://kawax.biz/nl/blog/telescope-introduction.md)
- [Praktische use-cases voor Laravel Pennant](https://kawax.biz/nl/blog/laravel-pennant.md)
- [Uitvoeringscontrole van queue-jobs](https://kawax.biz/nl/advanced/queue-job-control.md)


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