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.
'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.
'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.
Separar identificadores y servicios de ejecución
Guarda en el job los identificadores y opciones necesarios para la ejecución, e inyecta enhandle() 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.
- 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 declarado1. - En los jobs nuevos, el constructor fija
payloadVersionen2. Como difiere del valor por defecto declarado, este valor se guarda. - Si el
formatde 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'.
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().
#[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
UnQueue::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 elGenerateReport 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.
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
- Documentación oficial de Laravel: estructura de los jobs e inyección de dependencias
- Documentación oficial de Laravel: despacho tras el commit
- Documentación oficial de Laravel: workers y despliegue
- Documentación oficial de Laravel: tratamiento de modelos eliminados
- Queue: creación de payloads de objeto
- Queueable: composición del trait de jobs
- SerializesModels: guardado y restauración de propiedades
- SerializesAndRestoresModelIdentifiers: sustitución y recuperación de modelos
- CallQueuedHandler: restauración y ejecución
- QueueFake: guardado y restauración dentro del fake
- Pruebas oficiales de Laravel: serialización de modelos