Saltar al contenido principal
Para añadir a un paquete una sincronización periódica o un heartbeat, separa la provisión del proceso de su registro en la programación. Diseña el paquete de modo que su simple instalación no añada tareas periódicas a la aplicación consumidora, y que quien lo usa pueda elegir si lo activa, con qué frecuencia se ejecuta y cómo se comporta en varios servidores. En esta página se revisa el código fuente oficial de Laravel Framework v13.39.0 y se aplican al desarrollo de paquetes las operaciones básicas de Programación de tareas. Para la carga de providers, consulta Estructura interna de la detección automática de paquetes; para la publicación de la configuración, consulta Fusión y caché de la configuración de paquetes.

Registrar tras la resolución de Schedule

En una aplicación Laravel normal, FoundationServiceProvider registra Schedule::class como singleton. Su creación llama a resolveConsoleSchedule() del Console Kernel, que aplica la zona horaria de la aplicación y el store de caché del scheduler. El paquete debe extender este Schedule compartido, y no limitarse a crear su propio new Schedule(). El siguiente provider supone una estructura en la que Acme\Courier\ corresponde a src/ mediante PSR-4 de Composer y el paquete incluye config/courier.php.
callAfterResolving() espera a la resolución futura si el servicio aún no se ha resuelto y, si ya se resolvió, lo obtiene y llama al callback en ese momento. Así se puede registrar aunque otro provider haya usado Schedule antes. Para más detalles, consulta Hooks de resolución de servicios en paquetes. runningInConsole() no identifica un comando Artisan concreto. En este ejemplo se excluyen los usos de Schedule a través de HTTP. Además, el provider que registra el hook no debe ser un provider diferido: si boot() no se ejecuta antes del momento necesario, el hook tampoco se registra.
Registrar un evento y ejecutar una tarea son cosas distintas. Cargar este provider no ejecuta el heartbeat en ese momento. El registro por sí solo tampoco inicia cron ni schedule:work.

La activación explícita corresponde a quien usa el paquete

El archivo config/courier.php del paquete deja el estado inicial desactivado.
En la aplicación consumidora se publica o crea la configuración y se cambia schedule.enabled a true. En este ejemplo, los valores se leen en el momento del registro. La zona horaria tiene UTC como valor inicial; si eliges otra, ten en cuenta también las ejecuciones duplicadas u omitidas por el horario de verano. Como la configuración se fusiona de forma superficial, el registrador también especifica el valor predeterminado de cada clave. Así, aunque una sobrescritura parcial elimine otras claves del array schedule, se puede registrar con la configuración básica.

Evitar el registro duplicado en el mismo Schedule

El siguiente src/CourierSchedule.php registra un heartbeat mínimo que escribe una entrada de log. Con WeakMap, este registrador añade la tarea al mismo objeto Schedule una sola vez.
Como este registrador es un singleton, aunque se vuelva a obtener desde el hook del provider, comparte la comprobación de registro previo. En cambio, sí puede registrar en un Schedule nuevo. Evita usar una bandera estática de “ya registrado” para todo el proceso, que excluiría también los Schedule de contenedores nuevos o de las pruebas. Schedule::call(), exec() y job() añaden eventos a un array. No son APIs que busquen un evento con el mismo nombre para reemplazarlo ni que eliminen duplicados.
withoutOverlapping() no corrige los registros duplicados. Si la misma tarea se registra dos veces y la primera ejecución termina y libera el bloqueo, el evento posterior también puede ejecutarse. onOneServer() tampoco es una función para eliminar registros duplicados.El WeakMap anterior solo evita los duplicados de este registrador. Si quien usa el paquete añade la misma tarea en routes/console.php, desactiva el registro automático del paquete y deja la responsabilidad del registro en un único lugar.

El nombre del evento también afecta a la compatibilidad de los bloqueos

name() es un alias de description(), pero su efecto en el nombre del bloqueo depende del tipo de evento. A continuación se muestran los nombres de mutex predeterminados. En un CallbackEvent, asigna un nombre no vacío antes de withoutOverlapping() u onOneServer(). Si no se ha definido un nombre, estos métodos lanzan una excepción. job() normalmente usa como nombre inicial el nombre de la clase del job, entre otros. Sin embargo, si registras el mismo job con distintos argumentos o destinos, especifica explícitamente un nombre para cada destino. Cambiar solo los argumentos no hace que el nombre inicial pase a ser distinto automáticamente.
SyncCatalogJob es un ejemplo de clase de job que proporciona el paquete. Para implementar jobs, consulta Colas y jobs. Si cambias el nombre, el comando o la expresión cron, según el tipo de evento también cambia el identificador del bloqueo. En despliegues donde conviven procesos de la versión anterior y la nueva, el mismo proceso puede adquirir bloqueos distintos. No lo trates como un simple cambio de texto visible: gestiona el nombre de la tarea como una especificación pública estable.

Distinguir onOneServer y withoutOverlapping

  • onOneServer() elige el servidor que se encarga de la ejecución en ese momento. El CacheSchedulingMutex revisado añade la hora y el minuto al nombre de mutex del evento y lo mantiene con una caducidad de una hora. Es distinto del bloqueo contra solapamientos, que se libera al terminar la tarea.
  • withoutOverlapping() evita las ejecuciones solapadas mientras continúa la anterior. La caducidad indicada se expresa en minutos y su valor predeterminado es de 1.440 minutos. Con una caducidad demasiado corta, el bloqueo expira antes de que termine el proceso.
  • Cuando job() envía el trabajo a la cola, la finalización del evento programado no es la finalización del procesamiento del job. El control de duplicados en los workers se diseña por separado en el lado del job, por ejemplo con jobs únicos o con el middleware WithoutOverlapping.
A quienes activen onOneServer(), indícales como requisito una configuración de caché compartida con database, memcached, dynamodb o redis, tal como especifica la documentación oficial. Los bloqueos no garantizan una ejecución exactly-once de la lógica de negocio, así que también hace falta un proceso que no se rompa si se vuelve a ejecutar.

Resolver las dependencias pesadas en tiempo de ejecución

El LoggerInterface del heartbeat anterior lo resuelve CallbackEvent mediante call() del contenedor cuando se ejecuta el evento. Con esta estructura, no se escriben logs ni se crean clientes al registrar el hook. No colocar conexiones externas ni lógica de negocio en el constructor también es importante en Registro de comandos Artisan de paquetes y resolución de dependencias. Además, como call() y job() son CallbackEvent, no se puede usar runInBackground(). Delega los procesos largos a la cola o, si es necesario, elige la ejecución en segundo plano de command().

No apropiarse de la configuración global de la aplicación

Schedule::useCache() cambia el store del objeto mutex que tiene Schedule. No es una configuración que afecte solo a los eventos del paquete: también afecta a los eventos que comparten el mismo mutex. No lo llames incondicionalmente en el boot() del paquete; trátalo como una configuración que elige la aplicación consumidora. Del mismo modo, el paquete no debe cambiar por su cuenta Schedule::alwaysOnOneServer(), la pausa del scheduler ni la política de ejecución durante el modo de mantenimiento.

Tras cambiar la configuración, crear un Schedule nuevo

En el ejemplo, enabled, la expresión cron y la zona horaria son configuración leída durante el registro. Cambiar solo los valores de config() sobre un Schedule ya creado no añade ni elimina eventos ni vuelve a leer la configuración. La comprobación del WeakMap tampoco sirve para reconfigurar. En despliegues que usan la caché de configuración, reconstruye la caché y comprueba schedule:list en un proceso que cargue la nueva configuración. Comprueba también que no queden código ni configuración antiguos en schedulers de larga duración ni en schedule:run que estén ejecutando tareas de menos de un minuto. Para la interrupción de tareas de menos de un minuto durante el despliegue, consulta Programación de tareas.

Probar el registro por separado de la ejecución

A continuación se muestra un ejemplo de tests/CourierScheduleTest.php que usa el autoload de Composer del paquete y PHPUnit. Se usan dobles de prueba para los mutex y se comprueba el resultado del registro sin usar una caché externa ni un cron real.
Esta prueba es una prueba unitaria del registrador. La detección automática del provider, la fusión de la configuración y la integración con el Console Kernel se comprueban por separado con Orchestra Testbench o con una aplicación de verificación. No ejecutes todas las tareas de la aplicación consumidora con schedule:run solo para que pase la prueba de registro. Separar las pruebas del cuerpo de la tarea de las pruebas de integración del scheduler en un entorno controlado evita efectos secundarios de tareas periódicas no relacionadas.

Páginas relacionadas

Programación de tareas

Revisa la frecuencia de ejecución, la configuración de cron, la delegación a la cola y la operación durante los despliegues.

Hooks de resolución de servicios en paquetes

Revisa la aplicación a servicios no resueltos y ya resueltos, y las precauciones al volver a registrar.

Fuentes primarias consultadas

Última modificación el 1 de noviembre de 2026