> ## 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 cola de paquetes y compatibilidad al actualizar

> Analiza cómo Laravel 13 guarda y restaura jobs y explica la compatibilidad de nombres de clase y propiedades, la recuperación de modelos, las pruebas con payloads antiguos y el orden de actualización de los workers.

Asegúrate de que los jobs despachados antes de actualizar un paquete puedan procesarse también después. Lo que guarda la cola no es el código que se ejecutará, sino el nombre de la clase del job y su estado. Además de las pruebas unitarias del código nuevo, necesitas **pruebas que restauren con el código nuevo el estado guardado por el código antiguo**.

> [!NOTE]
>
> Esta página se centra en Laravel 13 y revisa la implementación de Framework `v13.42.0`. Presupone los [fundamentos de las colas](https://kawax.biz/es/queues.md) y las [pruebas de paquetes](https://kawax.biz/es/advanced/package-testing.md). El diseño de compatibilidad que sigue es una política del paquete. Laravel no tiene una función que migre automáticamente sus propios formatos de payload.

## El código cambia entre el guardado y la ejecución

En los jobs de objeto, `Queue::createObjectPayload()` ejecuta `serialize(clone $job)` y coloca el resultado en `data.command` del payload. Si se cifra, la misma serialización ocurre antes.

En el worker, `CallQueuedHandler::getCommand()` aplica `unserialize()` a los datos guardados y pasa el job restaurado al Dispatcher. Un `handle()` normal se ejecuta con el código y el contenedor de servicios que tenga el worker en ese momento.

```mermaid theme={null}
sequenceDiagram
    participant P as Lado que despacha<br>versión antigua del paquete
    participant Q as Cola
    participant W as Worker<br>versión nueva del paquete

    P->>Q: Guardar nombre de clase y propiedades
    Note over P,W: El despliegue actualiza el código
    Q->>W: Obtener el payload guardado
    W->>W: Restaurar en la clase actual
    W->>W: Ejecutar el handle() actual
```

Si no reinicias los workers de larga duración, el código antiguo sigue ejecutándose tras la actualización. En despliegues graduales, considera no solo "despachador antiguo con worker nuevo", sino también "despachador nuevo con worker antiguo".

| Qué cambia | Efecto en los jobs guardados |
| - | - |
| Nombre de clase completo del job | Si la clase no se puede cargar con el nombre antiguo, el job no se restaura correctamente |
| Nombre y visibilidad de las propiedades | Las claves al guardar y al restaurar pueden dejar de coincidir |
| Tipo de las propiedades | Puede que los valores antiguos no se puedan asignar y la restauración falle |
| Valor por defecto declarado de las propiedades | Puede cambiar el significado de los valores cuyo guardado se omitió |
| `handle()` o los servicios de los que depende | Aunque la restauración funcione, puede cambiar el resultado de procesar datos antiguos |

No juzgues el cambio de nombre o la eliminación de una clase solo por si los usuarios la llaman directamente. Los jobs en espera, retrasados o pendientes de reintento, y el periodo de retención de los jobs fallidos, también forman parte de la compatibilidad.

## Qué guarda SerializesModels

`Illuminate\Foundation\Queue\Queueable` usa `SerializesModels` internamente. Además de sustituir modelos, proporciona `__serialize()`, que enumera las propiedades del propio job, y `__unserialize()`, que devuelve los valores.

### Los valores iguales al valor por defecto declarado no se guardan

`__serialize()` excluye las propiedades estáticas y no inicializadas, y además **omite el guardado cuando el valor actual es estrictamente igual al valor por defecto declarado de la propiedad**.

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

Si esta propiedad sigue siendo `'csv'`, el array guardado no contiene `format`. Si una versión posterior cambia el valor por defecto declarado a `'json'`, `'json'` puede aplicarse también a los jobs antiguos. No es un cambio cosmético del valor por defecto, sino un cambio en cómo se leen los datos antiguos.

### El constructor no se vuelve a llamar al restaurar

`__unserialize()` asigna valores a las propiedades que existen en la clase actual y que tienen clave en el array guardado. No ejecuta ningún completado ni validación de la entrada a través del constructor.

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

El `'csv'` de este ejemplo es **el valor por defecto de un argumento del constructor**. No es el valor por defecto declarado de la propiedad, así que no completa el valor al restaurar un payload antiguo sin `format`. Si una propiedad tipada añadida queda sin inicializar, el acceso posterior falla.

Además, la clave de una propiedad public es su nombre, la de una protected lleva un prefijo de visibilidad y la de una private incluye también el nombre de la clase. Verifica con payloads antiguos también un cambio de public a private.

> [!WARNING]
>
> `SerializesModels` enumera las propiedades del job. No supongas que la misma conversión se aplica recursivamente dentro de DTOs arbitrarios o a modelos anidados en arrays. Si pasas objetos que no son modelos, sus clases y formatos de guardado también forman parte de la compatibilidad.

## Separar identificadores y servicios de ejecución

Guarda en el job los identificadores y opciones necesarios para la ejecución, e inyecta en `handle()` los servicios de ejecución como clientes de API o exportadores. Si pasas un servicio al constructor y lo conservas, su estado interno puede acabar guardándose.

El siguiente ejemplo añade una elección de formato y una versión de payload a un job con el mismo nombre que en la versión antigua solo tenía `reportId`. `ReportExporter` es un contrato que el paquete proporciona y registra en el contenedor, con `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);
    }
}
```

Este diseño separa el significado de los valores ausentes de los valores que se despachan de nuevo.

* Si un payload antiguo no tiene `format`, se usa el valor por defecto declarado `'csv'`.
* Si un payload antiguo no tiene `payloadVersion`, se trata como el valor por defecto declarado `1`.
* En los jobs nuevos, el constructor fija `payloadVersion` en `2`. Como difiere del valor por defecto declarado, este valor se guarda.
* Si el `format` de un job nuevo es `'json'`, ese valor también se guarda. `'csv'` se omite, así que mantén en adelante el valor por defecto declarado `'csv'`.

Si más adelante quieres cambiar solo el formato por defecto de los jobs nuevos, puedes cambiar el valor por defecto del argumento del constructor y la asignación, y estudiarlo por separado del valor por defecto de la propiedad que se usa para los datos antiguos. Gestiona la versión de payload no como el número de versión del paquete, sino como **la generación del formato de guardado y de su interpretación**.

> [!TIP]
>
> Añadir un número de versión no basta para lograr compatibilidad. La versión nueva de este ejemplo también procesa el formato antiguo, pero nada garantiza que los workers antiguos entiendan el nuevo `format` o `payloadVersion`. También necesitas el orden de actualización del despachador y los workers que se describe más abajo.

## Distinguir el paso de modelos de las instantáneas

Si guardas un modelo directamente en una propiedad, `SerializesAndRestoresModelIdentifiers` lo sustituye por un `ModelIdentifier` con la clase, el ID, el nombre de la conexión, los nombres de las relaciones cargadas, etc. Al restaurar un único modelo, lo vuelve a obtener de la base de datos con `firstOrFail()` y carga las relaciones correspondientes.

No es un mecanismo que fije los atributos o los datos relacionados en el momento del despacho.

* Si el registro se actualiza antes de la ejecución, el modelo restaurado refleja el contenido actualizado.
* Las restricciones aplicadas de antemano a las relaciones no se guardan, así que vuelve a aplicar las condiciones necesarias en tiempo de ejecución.
* Excluye del guardado las relaciones innecesarias con `withoutRelations()` o `#[WithoutRelations]`.
* Si el modelo se eliminó, el job puede fallar en la fase de restauración, antes de su `handle()`.

Si es aceptable descartar los jobs cuyo modelo se eliminó, puedes considerar el atributo oficial `#[DeleteWhenMissingModels]`. Sin embargo, el paquete no debe convertir el descarte en el comportamiento por defecto por su cuenta, sino exponer la política de fallos a los usuarios. Si pasas solo un ID y lo buscas en `handle()`, tú controlas el fallo o la omisión de esa búsqueda.

Si necesitas el contenido del momento del despacho, guarda explícitamente los valores necesarios o registra en la base de datos algo como una solicitud de informe inmutable y pasa su ID. Distingue entre simplemente pasar un modelo y guardar una instantánea de los datos de negocio.

### Despachar tras el commit es otro problema

Si procesas registros creados dentro de una transacción, evita que los workers empiecen a leerlos antes del commit.

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

DB::transaction(function () use ($reportId) {
    // Crear o actualizar el registro objetivo en esta transacción.

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

`afterCommit()` espera a despachar hasta el commit de la transacción padre abierta y descarta el despacho si hay rollback. No resuelve la compatibilidad de nombres de clase y propiedades ni la ejecución duplicada de procesos externos.

## Separar los fakes de las pruebas con payloads antiguos

Un `Queue::fake()` normal registra los despachos. Que `assertPushed()` pase no confirma por sí solo el guardado y la restauración, ni que la versión nueva procese payloads antiguos.

Puedes verificar un ciclo completo con la clase actual así.

```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()` ejecuta `serialize()` y `unserialize()` dentro del fake. Sin embargo, no verifica el despacho a un backend real, la ejecución por un worker ni la generación con el código antiguo.

### Conservar fixtures generados con la versión antigua

Mientras el `GenerateReport` antiguo solo tenga `reportId`, genera un job fijo sin datos reales. El siguiente código de preparación se ejecuta una vez en el entorno de pruebas de la versión antigua. Crea `tests/fixtures/` de antemano.

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

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

Pon el fixture bajo control de versiones y cárgalo desde las pruebas de la versión nueva. Si lo regeneras con la versión nueva en cada ejecución, ya no verifica la compatibilidad hacia atrás, así que mantenlo fijo.

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

El `TestCase` de este ejemplo es la clase base que preparas en el [entorno de pruebas de paquetes](https://kawax.biz/es/advanced/package-testing.md). Pasa a `unserialize()` solo fixtures que gestiones tú mismo, nunca entradas externas.

Además, ejecuta el `handle()` del job antiguo restaurado con un exportador de prueba y verifica la salida y los resultados guardados. Si pasas modelos, verifica por separado la actualización, la eliminación, la conexión y la recuperación de relaciones y, en la conexión de cola que uses realmente, el despacho tras el commit y los reintentos.

## Actualizar el lado lector antes de despachar el nuevo formato

**1. Preparar una versión que pueda leer el formato antiguo**

Distribuye primero código que mantenga el nombre de clase antiguo y la interpretación de los valores antiguos y que además pueda leer el formato nuevo. Con los fixtures, comprueba no solo la restauración sino también el resultado del procesamiento.

**2. Confirmar que no quedan workers antiguos**

Incluye la sustitución de workers en el procedimiento de despliegue de la aplicación que usa el paquete. Con un `queue:work` normal, `php artisan queue:restart` solicita la salida tras el job actual, y confirmas que el gestor de procesos arranca la versión nueva. La señal de reinicio usa la caché.

**3. Activar el despacho del formato nuevo**

Cuando el lado lector ya lo admita, cambia al despacho del formato nuevo, por ejemplo con un feature flag. Si distribuir el mismo código ya empieza a generar el formato nuevo, necesitas un procedimiento que no lo entregue a workers antiguos, como pausar el despacho o separar colas antiguas y nuevas con sus workers correspondientes.

**4. Mantener el soporte durante el periodo de reintentos y rollback**

Revisa también los reintentos de jobs fallidos y los jobs que se despachan más tarde dentro de cadenas. Si vuelves al código antiguo después de guardar el formato nuevo, verifica que el código antiguo pueda leerlo. No elimines el soporte de clases o formatos antiguos solo porque la cola se vació una vez.

`queue:restart` no es un comando que reescriba los payloads guardados. En las notas de actualización del paquete, indica los formatos admitidos, la sustitución de workers necesaria y las condiciones para retirar el formato antiguo. Registra el mismo cambio en la [gestión de compatibilidad de versiones](https://kawax.biz/es/advanced/package-versioning.md) y en la [gestión del changelog](https://kawax.biz/es/advanced/package-changelog.md).

## Fuentes primarias consultadas

* [Documentación oficial de Laravel: estructura de los jobs e inyección de dependencias](https://github.com/laravel/docs/blob/13.x/queues.md#class-structure)
* [Documentación oficial de Laravel: despacho tras el commit](https://github.com/laravel/docs/blob/13.x/queues.md#jobs-and-database-transactions)
* [Documentación oficial de Laravel: workers y despliegue](https://github.com/laravel/docs/blob/13.x/queues.md#queue-workers-and-deployment)
* [Documentación oficial de Laravel: tratamiento de modelos eliminados](https://github.com/laravel/docs/blob/13.x/queues.md#ignoring-missing-models)
* [Queue: creación de payloads de objeto](https://github.com/laravel/framework/blob/v13.42.0/src/Illuminate/Queue/Queue.php)
* [Queueable: composición del trait de jobs](https://github.com/laravel/framework/blob/v13.42.0/src/Illuminate/Foundation/Queue/Queueable.php)
* [SerializesModels: guardado y restauración de propiedades](https://github.com/laravel/framework/blob/v13.42.0/src/Illuminate/Queue/SerializesModels.php)
* [SerializesAndRestoresModelIdentifiers: sustitución y recuperación de modelos](https://github.com/laravel/framework/blob/v13.42.0/src/Illuminate/Queue/SerializesAndRestoresModelIdentifiers.php)
* [CallQueuedHandler: restauración y ejecución](https://github.com/laravel/framework/blob/v13.42.0/src/Illuminate/Queue/CallQueuedHandler.php)
* [QueueFake: guardado y restauración dentro del fake](https://github.com/laravel/framework/blob/v13.42.0/src/Illuminate/Support/Testing/Fakes/QueueFake.php)
* [Pruebas oficiales de Laravel: serialización de modelos](https://github.com/laravel/framework/blob/v13.42.0/tests/Integration/Queue/ModelSerializationTest.php)


## Related topics

- [Probar paquetes Laravel con Orchestra Testbench](https://kawax.biz/es/advanced/package-testing.md)
- [Temas avanzados](https://kawax.biz/es/advanced/index.md)
- [Control de ejecución de jobs de cola](https://kawax.biz/es/advanced/queue-job-control.md)
- [Gestión de la compatibilidad de versiones de paquetes](https://kawax.biz/es/advanced/package-versioning.md)
- [Etiquetas del contenedor en paquetes y agregación de plugins](https://kawax.biz/es/advanced/package-container-tags.md)


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