> ## 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 Paketen und Kompatibilität bei Updates

> Analysiert das Speichern und Wiederherstellen von Jobs in Laravel 13 und erklärt die Kompatibilität von Klassennamen und Properties, das erneute Laden von Modellen, Tests mit alten Payloads und die Reihenfolge beim Aktualisieren der Worker.

Stellen Sie sicher, dass Jobs, die vor einem Paket-Update dispatcht wurden, auch danach verarbeitet werden können. Die Queue speichert nicht den auszuführenden Code, sondern den Namen der Job-Klasse und ihren Zustand. Neben Unit-Tests für den neuen Code brauchen Sie **Tests, die einen vom alten Code gespeicherten Zustand mit dem neuen Code wiederherstellen**.

> [!NOTE]
>
> Diese Seite richtet sich an Laravel 13 und prüft die Implementierung von Framework `v13.42.0`. Sie setzt die [Grundlagen der Queues](https://kawax.biz/de/queues.md) und das [Testen von Paketen](https://kawax.biz/de/advanced/package-testing.md) voraus. Das folgende Kompatibilitätsdesign ist eine Richtlinie auf Paketseite. Laravel bietet keine Funktion, die eigene Payload-Formate automatisch migriert.

## Der Code ändert sich zwischen Speichern und Ausführen

Bei Objekt-Jobs führt `Queue::createObjectPayload()` `serialize(clone $job)` aus und legt das Ergebnis in `data.command` der Payload ab. Bei aktivierter Verschlüsselung findet davor dieselbe Serialisierung statt.

Auf Worker-Seite führt `CallQueuedHandler::getCommand()` `unserialize()` auf die gespeicherten Daten aus und übergibt den wiederhergestellten Job an den Dispatcher. Ein normales `handle()` läuft mit dem Code und dem Service-Container, die der Worker zu diesem Zeitpunkt hat.

```mermaid theme={null}
sequenceDiagram
    participant P as Dispatch-Seite<br>alte Paketversion
    participant Q as Queue
    participant W as Worker<br>neue Paketversion

    P->>Q: Klassenname und Properties speichern
    Note over P,W: Deployment aktualisiert den Code
    Q->>W: Gespeicherte Payload abrufen
    W->>W: In die aktuelle Klasse wiederherstellen
    W->>W: Aktuelles handle() ausführen
```

Wenn Sie langlebige Worker nicht neu starten, läuft nach dem Update weiterhin der alte Code. Bei Rolling Deployments berücksichtigen Sie nicht nur „alte Dispatch-Seite mit neuem Worker“, sondern auch „neue Dispatch-Seite mit altem Worker“.

| Was Sie ändern | Auswirkung auf gespeicherte Jobs |
| - | - |
| Vollqualifizierter Klassenname des Jobs | Lässt sich die Klasse unter dem alten Namen nicht laden, kann der Job nicht korrekt wiederhergestellt werden |
| Namen und Sichtbarkeit von Properties | Schlüssel beim Speichern und beim Wiederherstellen stimmen möglicherweise nicht mehr überein |
| Typen von Properties | Alte Werte lassen sich eventuell nicht zuweisen, das Wiederherstellen kann fehlschlagen |
| Deklarierte Standardwerte von Properties | Die Bedeutung von Werten, deren Speicherung ausgelassen wurde, kann sich ändern |
| `handle()` oder abhängige Services | Auch wenn das Wiederherstellen gelingt, kann sich das Ergebnis der Verarbeitung alter Daten ändern |

Beurteilen Sie das Umbenennen oder Entfernen einer Klasse nicht nur danach, ob Nutzer die Klasse direkt aufrufen. Wartende, verzögerte und auf Wiederholung wartende Jobs sowie die Aufbewahrungsdauer fehlgeschlagener Jobs gehören ebenfalls zur Kompatibilität.

## Was SerializesModels speichert

`Illuminate\Foundation\Queue\Queueable` verwendet intern `SerializesModels`. Neben dem Ersetzen von Modellen stellt es `__serialize()` bereit, das die Properties des Jobs selbst auflistet, und `__unserialize()`, das die Werte zurückschreibt.

### Werte, die dem deklarierten Standardwert entsprechen, werden nicht gespeichert

`__serialize()` schließt statische und nicht initialisierte Properties aus und **lässt das Speichern auch aus, wenn der aktuelle Wert strikt gleich dem deklarierten Standardwert der Property ist**.

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

Bleibt diese Property `'csv'`, enthält das gespeicherte Array kein `format`. Ändert ein späteres Release den deklarierten Standardwert auf `'json'`, kann `'json'` auch für alte Jobs gelten. Das ist keine kosmetische Änderung des Standardwerts, sondern eine Änderung, wie alte Daten gelesen werden.

### Der Konstruktor wird beim Wiederherstellen nicht erneut aufgerufen

`__unserialize()` weist Properties Werte zu, die in der aktuellen Klasse existieren und für die das gespeicherte Array einen Schlüssel hat. Eine Ergänzung oder Validierung von Eingaben über den Konstruktor findet nicht statt.

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

Das `'csv'` in diesem Beispiel ist **der Standardwert eines Konstruktorarguments**. Es ist nicht der deklarierte Standardwert der Property selbst und ergänzt den Wert daher nicht, wenn Sie eine alte Payload ohne `format` wiederherstellen. Bleibt eine neu hinzugefügte typisierte Property nicht initialisiert, schlägt ein späterer Zugriff fehl.

Außerdem ist der Schlüssel einer public Property ihr Name, der einer protected Property trägt ein Präfix für die Sichtbarkeit, und der einer private Property enthält zusätzlich den Klassennamen. Prüfen Sie auch eine Änderung von public zu private mit alten Payloads.

> [!WARNING]
>
> `SerializesModels` listet die Properties des Jobs auf. Gehen Sie nicht davon aus, dass dieselbe Umwandlung rekursiv im Inneren beliebiger DTOs oder für Modelle tief in Arrays stattfindet. Wenn Sie andere Objekte als Modelle übergeben, gehören deren Klassen und Speicherformate ebenfalls zur Kompatibilität.

## Bezeichner und ausführende Services trennen

Speichern Sie die für die Ausführung nötigen Bezeichner und Optionen im Job und injizieren Sie ausführende Services wie API-Clients oder Exporter in `handle()`. Übergeben Sie einen Service an den Konstruktor und behalten ihn, kann auch sein interner Zustand gespeichert werden.

Das folgende Beispiel ergänzt einen gleichnamigen Job, der in der alten Version nur `reportId` hatte, um eine Formatwahl und eine Payload-Version. `ReportExporter` ist ein Contract, den das Paket bereitstellt und im Container bindet, mit `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);
    }
}
```

Dieses Design trennt die Bedeutung fehlender Werte von den neu dispatchten Werten.

* Fehlt `format` in einer alten Payload, wird der deklarierte Standardwert `'csv'` verwendet.
* Fehlt `payloadVersion` in einer alten Payload, wird sie als deklarierter Standardwert `1` behandelt.
* Bei neu erzeugten Jobs setzt der Konstruktor `payloadVersion` auf `2`. Da der Wert vom deklarierten Standardwert abweicht, wird er gespeichert.
* Ist `format` eines neuen Jobs `'json'`, wird auch dieser Wert gespeichert. `'csv'` wird ausgelassen, behalten Sie daher den deklarierten Standardwert `'csv'` künftig bei.

Wollen Sie später nur das Standardformat neuer Jobs ändern, können Sie den Standardwert des Konstruktorarguments und die Zuweisung ändern und dies getrennt vom Property-Standardwert für alte Daten betrachten. Verwalten Sie die Payload-Version nicht als Release-Nummer des Pakets, sondern als **Generation des Speicherformats und seiner Interpretation**.

> [!TIP]
>
> Eine Versionsnummer allein bringt keine Kompatibilität. Die neue Version in diesem Beispiel kann auch das alte Format verarbeiten, aber nichts garantiert, dass alte Worker das neue `format` oder `payloadVersion` verstehen. Sie brauchen zusätzlich die weiter unten beschriebene Reihenfolge beim Aktualisieren von Dispatch-Seite und Workern.

## Übergebene Modelle von Snapshots unterscheiden

Speichern Sie ein Modell direkt in einer Property, ersetzt `SerializesAndRestoresModelIdentifiers` es durch einen `ModelIdentifier` mit Klasse, ID, Verbindungsname, Namen geladener Relationen usw. Beim Wiederherstellen eines einzelnen Modells lädt es dieses mit `firstOrFail()` erneut aus der Datenbank und lädt die betreffenden Relationen.

Dies ist kein Mechanismus, der Attribute oder verknüpfte Daten zum Zeitpunkt des Dispatchens festhält.

* Wird der Datensatz vor der Ausführung aktualisiert, sieht das wiederhergestellte Modell den aktualisierten Inhalt.
* Vorab angewendete Einschränkungen auf Relationen werden nicht gespeichert, wenden Sie benötigte Bedingungen daher zur Ausführungszeit erneut an.
* Schließen Sie unnötige Relationen mit `withoutRelations()` oder `#[WithoutRelations]` vom Speichern aus.
* Wurde das Modell gelöscht, kann der Job bereits beim Wiederherstellen fehlschlagen, vor `handle()`.

Dürfen Jobs mit gelöschtem Modell verworfen werden, können Sie das offizielle `#[DeleteWhenMissingModels]` in Betracht ziehen. Das Paket sollte das Verwerfen jedoch nicht eigenmächtig zum Standard machen, sondern die Fehlerrichtlinie für Nutzer offenlegen. Übergeben Sie nur eine ID und suchen in `handle()`, steuern Sie Fehlschlag oder Überspringen dieser Suche selbst.

Benötigen Sie den Inhalt zum Zeitpunkt des Dispatchens, speichern Sie die nötigen Werte explizit oder legen Sie etwa eine unveränderliche Report-Anfrage in der Datenbank ab und übergeben deren ID. Unterscheiden Sie das bloße Übergeben eines Modells vom Speichern eines Snapshots von Geschäftsdaten.

### Dispatchen nach dem Commit ist ein separates Problem

Wenn Sie Datensätze verarbeiten, die in einer Transaktion erstellt wurden, verhindern Sie, dass Worker sie vor dem Commit lesen.

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

DB::transaction(function () use ($reportId) {
    // Den Zieldatensatz in dieser Transaktion erstellen oder aktualisieren.

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

`afterCommit()` wartet mit dem Dispatchen bis zum Commit der offenen übergeordneten Transaktion und verwirft das Dispatchen bei einem Rollback. Es löst weder die Kompatibilität von Klassennamen und Properties noch die doppelte Ausführung externer Verarbeitung.

## Fakes und Tests alter Payloads trennen

Ein normales `Queue::fake()` zeichnet Dispatches auf. Ein erfolgreiches `assertPushed()` allein bestätigt weder Speichern und Wiederherstellen noch, dass die neue Version alte Payloads verarbeitet.

Einen Roundtrip mit der aktuellen Klasse können Sie so prüfen.

```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()` führt `serialize()` und `unserialize()` innerhalb des Fakes aus. Es prüft jedoch weder das Dispatchen an ein echtes Backend noch die Ausführung durch einen Worker oder die Erzeugung durch alten Code.

### Mit der alten Version erzeugte Fixtures aufbewahren

Solange das alte `GenerateReport` nur `reportId` hat, erzeugen Sie einen festen Job ohne echte Daten. Der folgende Vorbereitungscode wird einmal in der Testumgebung der alten Version ausgeführt. Legen Sie `tests/fixtures/` vorher an.

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

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

Stellen Sie die Fixture unter Versionskontrolle und laden Sie sie aus den Tests der neuen Version. Würden Sie sie bei jedem Testlauf mit der neuen Version neu erzeugen, prüften Sie keine Abwärtskompatibilität mehr. Lassen Sie sie daher unverändert.

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

Das `TestCase` in diesem Beispiel ist die Basisklasse, die Sie in der [Testumgebung für Pakete](https://kawax.biz/de/advanced/package-testing.md) einrichten. Übergeben Sie `unserialize()` nur selbst verwaltete Fixtures und niemals externe Eingaben.

Führen Sie außerdem `handle()` des wiederhergestellten alten Jobs mit einem Test-Exporter aus und prüfen Sie Ausgabe und gespeicherte Ergebnisse. Wenn Sie Modelle übergeben, prüfen Sie Aktualisierung, Löschung, Verbindung und erneutes Laden von Relationen separat, und auf der tatsächlich genutzten Queue-Verbindung auch das Dispatchen nach dem Commit und Wiederholungen.

## Die lesende Seite vor dem Dispatchen des neuen Formats aktualisieren

**1. Ein Release vorbereiten, das das alte Format lesen kann**

Verteilen Sie zuerst Code, der den alten Klassennamen und die Interpretation alter Werte beibehält und zusätzlich das neue Format lesen kann. Prüfen Sie mit Fixtures nicht nur das Wiederherstellen, sondern auch das Verarbeitungsergebnis.

**2. Sicherstellen, dass keine alten Worker übrig bleiben**

Nehmen Sie den Austausch der Worker in das Deployment-Verfahren der nutzenden Anwendung auf. Bei einem normalen `queue:work` fordert `php artisan queue:restart` das Beenden nach dem aktuellen Job an, und Sie prüfen, dass der Prozessmanager die neue Version startet. Für das Neustartsignal wird der Cache verwendet.

**3. Das Dispatchen des neuen Formats aktivieren**

Schalten Sie erst auf das Dispatchen des neuen Formats um, wenn die lesende Seite es unterstützt, etwa per Feature-Flag. Beginnt schon die Verteilung desselben Codes mit dem Erzeugen des neuen Formats, brauchen Sie ein Verfahren, das es von alten Workern fernhält, etwa das Dispatchen pausieren oder alte und neue Queues mit passenden Workern trennen.

**4. Den Zeitraum für Wiederholungen und Rollbacks weiter unterstützen**

Prüfen Sie auch Wiederholungen fehlgeschlagener Jobs und Jobs, die später innerhalb von Chains dispatcht werden. Kehren Sie nach dem Speichern des neuen Formats zu altem Code zurück, prüfen Sie, ob der alte Code es lesen kann. Entfernen Sie die Unterstützung alter Klassen oder Formate nicht, nur weil die Queue einmal leer war.

`queue:restart` ist kein Befehl, der gespeicherte Payloads umschreibt. Nennen Sie in den Update-Hinweisen des Pakets die unterstützten Formate, den nötigen Austausch der Worker und die Bedingungen für das Abschaffen des alten Formats. Halten Sie dieselbe Änderung auch im [Versionskompatibilitätsmanagement](https://kawax.biz/de/advanced/package-versioning.md) und im [Changelog-Betrieb](https://kawax.biz/de/advanced/package-changelog.md) fest.

## Herangezogene Primärquellen

* [Offizielle Laravel-Dokumentation: Aufbau von Jobs und Dependency Injection](https://github.com/laravel/docs/blob/13.x/queues.md#class-structure)
* [Offizielle Laravel-Dokumentation: Dispatchen nach dem Commit](https://github.com/laravel/docs/blob/13.x/queues.md#jobs-and-database-transactions)
* [Offizielle Laravel-Dokumentation: Worker und Deployment](https://github.com/laravel/docs/blob/13.x/queues.md#queue-workers-and-deployment)
* [Offizielle Laravel-Dokumentation: Umgang mit gelöschten Modellen](https://github.com/laravel/docs/blob/13.x/queues.md#ignoring-missing-models)
* [Queue: Erstellen von Objekt-Payloads](https://github.com/laravel/framework/blob/v13.42.0/src/Illuminate/Queue/Queue.php)
* [Queueable: Aufbau des Job-Traits](https://github.com/laravel/framework/blob/v13.42.0/src/Illuminate/Foundation/Queue/Queueable.php)
* [SerializesModels: Speichern und Wiederherstellen von Properties](https://github.com/laravel/framework/blob/v13.42.0/src/Illuminate/Queue/SerializesModels.php)
* [SerializesAndRestoresModelIdentifiers: Ersetzen und erneutes Laden von Modellen](https://github.com/laravel/framework/blob/v13.42.0/src/Illuminate/Queue/SerializesAndRestoresModelIdentifiers.php)
* [CallQueuedHandler: Wiederherstellen und Ausführen](https://github.com/laravel/framework/blob/v13.42.0/src/Illuminate/Queue/CallQueuedHandler.php)
* [QueueFake: Speichern und Wiederherstellen im Fake](https://github.com/laravel/framework/blob/v13.42.0/src/Illuminate/Support/Testing/Fakes/QueueFake.php)
* [Offizielle Laravel-Tests: Serialisierung von Modellen](https://github.com/laravel/framework/blob/v13.42.0/tests/Integration/Queue/ModelSerializationTest.php)


## Related topics

- [Laravel-Pakete mit Orchestra Testbench testen](https://kawax.biz/de/advanced/package-testing.md)
- [Weiterführende Themen](https://kawax.biz/de/advanced/index.md)
- [Laravel-Updates Juli 2026](https://kawax.biz/de/blog/changelog/202607.md)
- [Ausführungssteuerung von Queue-Jobs](https://kawax.biz/de/advanced/queue-job-control.md)
- [Geplante Aufgaben in Paketen registrieren und pflegen](https://kawax.biz/de/advanced/package-scheduling.md)


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