Saltar al contenido principal
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.
Esta página se centra en Laravel 13 y revisa la implementación de Framework v13.42.0. Presupone los fundamentos de las colas y las pruebas de paquetes. 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. 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”. 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.
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.
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.
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.
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.
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.
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í.
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.
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.
El TestCase de este ejemplo es la clase base que preparas en el entorno de pruebas de paquetes. 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 y en la gestión del changelog.

Fuentes primarias consultadas

Última modificación el 12 de noviembre de 2026