1. Profundizando
  2. Programación de tareas

Únete a nuestra comunidad de Telegram @webblend!

Aquí encontrarás fragmentos de código de Laravel y consejos útiles sobre desarrollo web.

Introducción

En el pasado, es posible que hayas escrito una entrada de configuración de cron para cada tarea que necesitabas programar en tu servidor. Sin embargo, esto puede volverse rápidamente molesto porque tu programación de tareas ya no está en control de origen y debes conectarte a tu servidor mediante SSH para ver tus entradas de cron existentes o agregar entradas adicionales.

El programador de comandos de Laravel ofrece un enfoque fresco para gestionar tareas programadas en tu servidor. El programador te permite definir de manera fluida y expresiva tu programación de comandos dentro de tu propia aplicación Laravel. Al usar el programador, solo se necesita una entrada de cron en tu servidor. Tu programación de tareas se define en el método schedule del archivo app/Console/Kernel.php. Para ayudarte a empezar, se define un ejemplo simple dentro del método.

Definir Cronogramas

Puedes definir todas tus tareas programadas en el método schedule de la clase App\Console\Kernel de tu aplicación. Para empezar, veamos un ejemplo. En este ejemplo, programaremos un cierre para que se llame todos los días a la medianoche. Dentro del cierre, ejecutaremos una consulta de base de datos para limpiar una tabla:

<?php
 
namespace App\Console;
 
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
use Illuminate\Support\Facades\DB;
 
class Kernel extends ConsoleKernel
{
/**
* Define el cronograma de comandos de la aplicación.
*/
protected function schedule(Schedule $schedule): void
{
$schedule->call(function () {
DB::table('recent_users')->delete();
})->daily();
}
}

Además de programar usando cierres, también puedes programar objetos invocables. Los objetos invocables son clases PHP simples que contienen un método __invoke:

$schedule->call(new DeleteRecentUsers)->daily();

Si deseas ver una descripción general de tus tareas programadas y la próxima vez que estén programadas para ejecutarse, puedes usar el comando Artisan schedule:list:

php artisan schedule:list

Programación de Comandos Artisan

Además de programar cierres, también puedes programar comandos Artisan y comandos del sistema. Por ejemplo, puedes usar el método command para programar un comando Artisan utilizando el nombre o la clase del comando.

Cuando programas comandos Artisan utilizando la clase del comando, puedes pasar un array de argumentos adicionales de línea de comandos que se deben proporcionar al comando cuando se invoque:

use App\Console\Commands\SendEmailsCommand;
 
$schedule->command('emails:send Taylor --force')->daily();
 
$schedule->command(SendEmailsCommand::class, ['Taylor', '--force'])->daily();

Programación de Trabajos en Cola

El método job se puede usar para programar un trabajo en cola. Este método proporciona una forma conveniente de programar trabajos en cola sin usar el método call para definir cierres para encolar el trabajo:

use App\Jobs\Heartbeat;
 
$schedule->job(new Heartbeat)->everyFiveMinutes();

Se pueden proporcionar argumentos opcionales segundo y tercero al método job que especifica el nombre de la cola y la conexión de la cola que se debe usar para encolar el trabajo:

use App\Jobs\Heartbeat;
 
// Despachar el trabajo a la cola "heartbeats" en la conexión "sqs"...
$schedule->job(new Heartbeat, 'heartbeats', 'sqs')->everyFiveMinutes();

Programación de Comandos de Shell

El método exec se puede usar para emitir un comando al sistema operativo:

$schedule->exec('node /home/forge/script.js')->daily();

Opciones de Frecuencia de Cronograma

Ya hemos visto algunos ejemplos de cómo puedes configurar una tarea para que se ejecute en intervalos específicos. Sin embargo, hay muchas más frecuencias de programación de tareas que puedes asignar a una tarea:

Método Descripción
->cron('* * * * *'); Ejecuta la tarea según una programación personalizada de cron
->everySecond(); Ejecuta la tarea cada segundo
->everyTwoSeconds(); Ejecuta la tarea cada dos segundos
->everyFiveSeconds(); Ejecuta la tarea cada cinco segundos
->everyTenSeconds(); Ejecuta la tarea cada diez segundos
->everyFifteenSeconds(); Ejecuta la tarea cada quince segundos
->everyTwentySeconds(); Ejecuta la tarea cada veinte segundos
->everyThirtySeconds(); Ejecuta la tarea cada treinta segundos
->everyMinute(); Ejecuta la tarea cada minuto
->everyTwoMinutes(); Ejecuta la tarea cada dos minutos
->everyThreeMinutes(); Ejecuta la tarea cada tres minutos
->everyFourMinutes(); Ejecuta la tarea cada cuatro minutos
->everyFiveMinutes(); Ejecuta la tarea cada cinco minutos
->everyTenMinutes(); Ejecuta la tarea cada diez minutos
->everyFifteenMinutes(); Ejecuta la tarea cada quince minutos
->everyThirtyMinutes(); Ejecuta la tarea cada treinta minutos
->hourly(); Ejecuta la tarea cada hora
->hourlyAt(17); Ejecuta la tarea cada hora a los 17 minutos de la hora
->everyOddHour($minutes = 0); Ejecuta la tarea cada hora impar
->everyTwoHours($minutes = 0); Ejecuta la tarea cada dos horas
->everyThreeHours($minutes = 0); Ejecuta la tarea cada tres horas
->everyFourHours($minutes = 0); Ejecuta la tarea cada cuatro horas
->everySixHours($minutes = 0); Ejecuta la tarea cada seis horas
->daily(); Ejecuta la tarea cada día a la medianoche
->dailyAt('13:00'); Ejecuta la tarea cada día a las 13:00
->twiceDaily(1, 13); Ejecuta la tarea diariamente a la 1:00 y 13:00
->twiceDailyAt(1, 13, 15); Ejecuta la tarea diariamente a las 1:15 y 13:15
->weekly(); Ejecuta la tarea cada domingo a las 00:00
->weeklyOn(1, '8:00'); Ejecuta la tarea cada semana los lunes a las 8:00
->monthly(); Ejecuta la tarea el primer día de cada mes a las 00:00
->monthlyOn(4, '15:00'); Ejecuta la tarea cada mes el día 4 a las 15:00
->twiceMonthly(1, 16, '13:00'); Ejecuta la tarea mensualmente el día 1 y 16 a las 13:00
->lastDayOfMonth('15:00'); Ejecuta la tarea el último día del mes a las 15:00
->quarterly(); Ejecuta la tarea el primer día de cada trimestre a las 00:00
->quarterlyOn(4, '14:00'); Ejecuta la tarea cada trimestre el día 4 a las 14:00
->yearly(); Ejecuta la tarea el primer día de cada año a las 00:00
->yearlyOn(6, 1, '17:00'); Ejecuta la tarea cada año el 1 de junio a las 17:00
->timezone('America/New_York'); Establece la zona horaria para la tarea

Estos métodos se pueden combinar con restricciones adicionales para crear cronogramas aún más ajustados que solo se ejecuten en ciertos días de la semana. Por ejemplo, puedes programar un comando para que se ejecute semanalmente los lunes:

// Ejecutar una vez a la semana los lunes a la 1 PM...
$schedule->call(function () {
// ...
})->weekly()->mondays()->at('13:00');
 
// Ejecutar cada hora de 8 AM a 5 PM los días laborables...
$schedule->command('foo')
->weekdays()
->hourly()
->timezone('America/Chicago')
->between('8:00', '17:00');

A continuación, se muestra una lista de restricciones de programación adicionales:

Método Descripción
->weekdays(); Limita la tarea a los días laborables
->weekends(); Limita la tarea a los fines de semana
->sundays(); Limita la tarea al domingo
->mondays(); Limita la tarea al lunes
->tuesdays(); Limita la tarea al martes
->wednesdays(); Limita la tarea al miércoles
->thursdays(); Limita la tarea al jueves
->fridays(); Limita la tarea al viernes
->saturdays(); Limita la tarea al sábado
->days(array|mixed); Limita la tarea a días específicos
->between($startTime, $endTime); Limita la tarea para ejecutarse entre horas de inicio y fin
->unlessBetween($startTime, $endTime); Limita la tarea para no ejecutarse entre horas de inicio y fin
->when(Closure); Limita la tarea según una prueba de veracidad
->environments($env); Limita la tarea a entornos específicos

Restricciones Diarias

El método days se puede usar para limitar la ejecución de una tarea a días específicos de la semana. Por ejemplo, puedes programar un comando para que se ejecute cada hora los domingos y miércoles:

$schedule->command('emails:send')
->hourly()
->days([0, 3]);

Alternativamente, puedes usar las constantes disponibles en la clase Illuminate\Console\Scheduling\Schedule al definir los días en los que debe ejecutarse una tarea:

use Illuminate\Console\Scheduling\Schedule;
 
$schedule->command('emails:send')
->hourly()
->days([Schedule::SUNDAY, Schedule::WEDNESDAY]);

Restricciones de Tiempo

El método between se puede usar para limitar la ejecución de una tarea según la hora del día:

$schedule->command('emails:send')
->hourly()
->between('7:00', '22:00');

De manera similar, el método unlessBetween se puede usar para excluir la ejecución de una tarea durante un período de tiempo:

$schedule->command('emails:send')
->hourly()
->unlessBetween('23:00', '4:00');

Restricciones de Prueba de Veracidad

El método when se puede usar para limitar la ejecución de una tarea según el resultado de una prueba de veracidad dada. En otras palabras, si el cierre dado devuelve true, la tarea se ejecutará siempre que ninguna otra condición restrictiva evite que la tarea se ejecute:

$schedule->command('emails:send')->daily()->when(function () {
return true;
});

El método skip se puede ver como el inverso de when. Si el método skip devuelve true, la tarea programada no se ejecutará:

$schedule->command('emails:send')->daily()->skip(function () {
return true;
});

Cuando se usan métodos encadenados de when, el comando programado solo se ejecutará si todas las condiciones de when devuelven true.

Restricciones de Entorno

El método environments se puede usar para ejecutar tareas solo en los entornos especificados (según la variable de entorno APP_ENV):

$schedule->command('emails:send')
->daily()
->environments(['staging', 'production']);

Zonas Horarias

Usando el método timezone, puedes especificar que la hora de una tarea programada debe interpretarse dentro de una zona horaria dada:

$schedule->command('report:generate')
->timezone('America/New_York')
->at('2:00')

Si asignas repetidamente la misma zona horaria a todas tus tareas programadas, es posible que desees definir un método scheduleTimezone en tu clase App\Console\Kernel. Este método debería devolver la zona horaria predeterminada que se debe asignar a todas las tareas programadas:

use DateTimeZone;
 
/**
* Obtener la zona horaria que se debe usar de forma predeterminada para los eventos programados.
*/
protected function scheduleTimezone(): DateTimeZone|string|null
{
return 'modify_10x/scheduling.return_text_262';
}

Advertencia Recuerda que algunas zonas horarias utilizan el horario de verano. Cuando ocurren cambios en el horario de verano, tu tarea programada puede ejecutarse dos veces o incluso no ejecutarse en absoluto. Por esta razón, recomendamos evitar la programación de zonas horarias cuando sea posible.

Prevención de Superposiciones de Tareas

De forma predeterminada, las tareas programadas se ejecutarán incluso si la instancia anterior de la tarea aún está en ejecución. Para evitar esto, puedes usar el método withoutOverlapping:

$schedule->command('emails:send')->withoutOverlapping();

En este ejemplo, el comando Artisan emails:send se ejecutará cada minuto si aún no se está ejecutando. El método withoutOverlapping es especialmente útil si tienes tareas que varían drásticamente en su tiempo de ejecución, lo que te impide prever exactamente cuánto tiempo tomará una tarea dada.

Si es necesario, puedes especificar cuántos minutos deben pasar antes de que expire el bloqueo "sin solapamiento". De forma predeterminada, el bloqueo expirará después de 24 horas:

$schedule->command('emails:send')->withoutOverlapping(10);

Detrás de escena, el método withoutOverlapping utiliza la caché de tu aplicación para obtener bloqueos. Si es necesario, puedes borrar estos bloqueos de caché usando el comando Artisan schedule:clear-cache. Esto suele ser necesario solo si una tarea queda atascada debido a un problema inesperado del servidor.

Ejecución de Tareas en un Solo Servidor

Advertencia Para utilizar esta función, tu aplicación debe estar utilizando el controlador de caché predeterminado de tu aplicación, que puede ser database, memcached, dynamodb o redis. Además, todos los servidores deben comunicarse con el mismo servidor central de caché.

Si el programador de tu aplicación se está ejecutando en varios servidores, es posible que desees limitar un trabajo programado para que se ejecute solo en un servidor. Por ejemplo, supongamos que tienes una tarea programada que genera un nuevo informe cada viernes por la noche. Si el programador de tareas se está ejecutando en tres servidores, la tarea programada se ejecutará en los tres servidores y generará el informe tres veces. ¡No es bueno!

Para indicar que la tarea debe ejecutarse solo en un servidor, utiliza el método onOneServer al definir la tarea programada. El primer servidor que obtenga la tarea asegurará un bloqueo atómico en el trabajo para evitar que otros servidores ejecuten la misma tarea al mismo tiempo:

$schedule->command('report:generate')
->fridays()
->at('17:00')
->onOneServer();

Nombrar Tareas para un Solo Servidor

A veces, es posible que necesites programar el mismo trabajo para que se despache con diferentes parámetros, mientras aún le indicas a Laravel que ejecute cada permutación del trabajo en un solo servidor. Para lograr esto, puedes asignar a cada definición de programación un nombre único mediante el método name:

$schedule->job(new CheckUptime('https://laravel-docs.com'))
->name('check_uptime:laravel-docs.com')
->everyFiveMinutes()
->onOneServer();
 
$schedule->job(new CheckUptime('https://vapor.laravel.com'))
->name('check_uptime:vapor.laravel.com')
->everyFiveMinutes()
->onOneServer();

De manera similar, los cierres programados deben asignarse un nombre si se espera que se ejecuten en un solo servidor:

$schedule->call(fn () => User::resetApiRequestCount())
->name('reset-api-request-count')
->daily()
->onOneServer();

Tareas en Segundo Plano

De forma predeterminada, varias tareas programadas al mismo tiempo se ejecutarán de forma secuencial según el orden en que se definen en tu método schedule. Si tienes tareas de larga duración, esto puede hacer que las tareas posteriores comiencen mucho más tarde de lo anticipado. Si deseas ejecutar tareas en segundo plano para que puedan ejecutarse todas simultáneamente, puedes usar el método runInBackground:

$schedule->command('analytics:report')
->daily()
->runInBackground();

Advertencia El método runInBackground solo se puede utilizar al programar tareas mediante los métodos command y exec.

Modo de Mantenimiento

Las tareas programadas de tu aplicación no se ejecutarán cuando la aplicación esté en modo de mantenimiento, ya que no queremos que tus tareas interfieran con algún mantenimiento inacabado que puedas estar realizando en tu servidor. Sin embargo, si deseas forzar que una tarea se ejecute incluso en modo de mantenimiento, puedes llamar al método evenInMaintenanceMode al definir la tarea:

$schedule->command('emails:send')->evenInMaintenanceMode();

Ejecución del Programador

Ahora que hemos aprendido cómo definir tareas programadas, hablemos de cómo ejecutarlas realmente en nuestro servidor. El comando Artisan schedule:run evaluará todas tus tareas programadas y determinará si necesitan ejecutarse según la hora actual del servidor.

Entonces, al usar el programador de Laravel, solo necesitamos agregar una única entrada de configuración de cron a nuestro servidor que ejecute el comando schedule:run cada minuto. Si no sabes cómo agregar entradas de cron a tu servidor, considera usar un servicio como Laravel Forge que puede gestionar las entradas de cron por ti:

* * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1

Tareas Programadas en Sub-Minuto

En la mayoría de los sistemas operativos, las tareas cron están limitadas a ejecutarse un máximo de una vez por minuto. Sin embargo, el programador de Laravel te permite programar tareas para que se ejecuten a intervalos más frecuentes, incluso tan a menudo como una vez por segundo:

$schedule->call(function () {
DB::table('recent_users')->delete();
})->everySecond();

Cuando se definen tareas subminuto en tu aplicación, el comando schedule:run continuará ejecutándose hasta el final del minuto actual en lugar de salir inmediatamente. Esto permite que el comando invoque todas las tareas subminuto necesarias a lo largo del minuto.

Dado que las tareas subminuto que tardan más de lo esperado en ejecutarse podrían retrasar la ejecución de tareas subminuto posteriores, se recomienda que todas las tareas subminuto envíen trabajos en cola o comandos en segundo plano para manejar el procesamiento real de la tarea.

use App\Jobs\DeleteRecentUsers;
 
$schedule->job(new DeleteRecentUsers)->everyTenSeconds();
 
$schedule->command('users:delete')->everyTenSeconds()->runInBackground();

Interrupción de Tareas en Sub-Minuto

Como el comando schedule:run se ejecuta durante todo el minuto de invocación cuando se definen tareas subminuto, a veces es necesario interrumpir el comando al implementar tu aplicación. De lo contrario, una instancia del comando schedule:run que ya está en ejecución continuaría utilizando el código de tu aplicación implementado anteriormente hasta que termine el minuto actual.

Para interrumpir las invocaciones en progreso de schedule:run, puedes agregar el comando schedule:interrupt al script de implementación de tu aplicación. Este comando debe invocarse después de que tu aplicación haya terminado de implementarse:

php artisan schedule:interrupt

Ejecución del Programador Localmente

Normalmente, no agregarías una entrada de cron para el programador a tu máquina de desarrollo local. En cambio, puedes usar el comando Artisan schedule:work. Este comando se ejecutará en primer plano e invocará el programador cada minuto hasta que termines el comando:

php artisan schedule:work

Salida de Tareas

El programador de Laravel proporciona varios métodos convenientes para trabajar con la salida generada por las tareas programadas. Primero, usando el método sendOutputTo, puedes enviar la salida a un archivo para su inspección posterior:

$schedule->command('emails:send')
->daily()
->sendOutputTo($filePath);

Si deseas agregar la salida a un archivo dado, puedes usar el método appendOutputTo:

$schedule->command('emails:send')
->daily()
->appendOutputTo($filePath);

Usando el método emailOutputTo, puedes enviar la salida a una dirección de correo electrónico de tu elección. Antes de enviar por correo electrónico la salida de una tarea, debes configurar los servicios de correo electrónico de Laravel:

$schedule->command('report:generate')
->daily()
->sendOutputTo($filePath)
->emailOutputTo('[email protected]');

Si solo quieres enviar por correo electrónico la salida si el comando Artisan o el sistema programado termina con un código de salida no cero, usa el método emailOutputOnFailure:

$schedule->command('report:generate')
->daily()
->emailOutputOnFailure('[email protected]');

Advertencia Los métodos emailOutputTo, emailOutputOnFailure, sendOutputTo y appendOutputTo son exclusivos de los métodos command y exec.

Ganchos de Tareas

Usando los métodos before y after, puedes especificar el código que se ejecutará antes y después de que se ejecute la tarea programada:

$schedule->command('emails:send')
->daily()
->before(function () {
// La tarea está a punto de ejecutarse...
})
->after(function () {
// La tarea se ha ejecutado...
});

Los métodos onSuccess y onFailure te permiten especificar el código que se ejecutará si la tarea programada tiene éxito o falla. Un fallo indica que el comando Artisan o el sistema programado terminaron con un código de salida no cero:

$schedule->command('emails:send')
->daily()
->onSuccess(function () {
// La tarea tuvo éxito...
})
->onFailure(function () {
// La tarea falló...
});

Si hay salida disponible de tu comando, puedes acceder a ella en tus ganchos after, onSuccess u onFailure mediante la sugerencia de tipos de una instancia de Illuminate\Support\Stringable como argumento $output de la definición del cierre de tu gancho:

use Illuminate\Support\Stringable;
 
$schedule->command('emails:send')
->daily()
->onSuccess(function (Stringable $output) {
// La tarea tuvo éxito...
})
->onFailure(function (Stringable $output) {
// La tarea falló...
});

Ping a URLs

Usando los métodos pingBefore y thenPing, el programador puede enviar automáticamente un ping a una URL dada antes o después de que se ejecute una tarea. Este método es útil para notificar a un servicio externo, como Envoyer, que tu tarea programada está comenzando o ha terminado la ejecución:

$schedule->command('emails:send')
->daily()
->pingBefore($url)
->thenPing($url);

Los métodos pingBeforeIf y thenPingIf se pueden usar para enviar un ping a una URL dada solo si se cumple una condición dada es true:

$schedule->command('emails:send')
->daily()
->pingBeforeIf($condition, $url)
->thenPingIf($condition, $url);

Los métodos pingOnSuccess y pingOnFailure se pueden utilizar para enviar un ping a una URL dada solo si la tarea tiene éxito o falla. Un fallo indica que el comando Artisan programado o el sistema terminó con un código de salida no cero:

$schedule->command('emails:send')
->daily()
->pingOnSuccess($successUrl)
->pingOnFailure($failureUrl);

Todos los métodos de ping requieren la biblioteca HTTP Guzzle. Guzzle se instala típicamente en todos los nuevos proyectos de Laravel por defecto, pero puedes instalar Guzzle manualmente en tu proyecto utilizando el gestor de paquetes Composer si se ha eliminado accidentalmente:

composer require guzzlehttp/guzzle

Eventos

Si es necesario, puedes escuchar eventos enviados por el programador. Por lo general, las asignaciones de escuchadores de eventos se definirán dentro de la clase App\Providers\EventServiceProvider de tu aplicación:

/**
* Mapeos de escuchadores de eventos para la aplicación.
*
* @var array
*/
protected $listen = [
'Illuminate\Console\Events\ScheduledTaskStarting' => [
'App\Listeners\LogScheduledTaskStarting',
],
 
'Illuminate\Console\Events\ScheduledTaskFinished' => [
'App\Listeners\LogScheduledTaskFinished',
],
 
'Illuminate\Console\Events\ScheduledBackgroundTaskFinished' => [
'App\Listeners\LogScheduledBackgroundTaskFinished',
],
 
'Illuminate\Console\Events\ScheduledTaskSkipped' => [
'App\Listeners\LogScheduledTaskSkipped',
],
 
'Illuminate\Console\Events\ScheduledTaskFailed' => [
'App\Listeners\LogScheduledTaskFailed',
],
];