Tareas programadas
Laravel incluye un programador de tareas (Task Scheduler) que te permite definir tareas automáticas en PHP de forma expresiva. Solo necesitas configurar una entrada cron en el servidor y Laravel se encarga del resto.
¿Por qué usar el Scheduler?
Sin Laravel, necesitarías crear una entrada cron para cada tarea programada. Esto se vuelve difícil de mantener y el historial de tareas queda fuera del control de versiones.
El Scheduler de Laravel resuelve esto: defines todas las tareas en código PHP y solo necesitas una entrada cron que ejecute el scheduler cada minuto.
Ejemplos de tareas programadas:
- Limpieza: eliminar registros antiguos, archivos temporales
- Reportes: generar y enviar informes diarios/semanales
- Sincronización: actualizar datos desde APIs externas
- Mantenimiento: optimizar base de datos, renovar tokens
- Notificaciones: enviar recordatorios, resúmenes
Configurar el scheduler
En producción, necesitas añadir esta entrada cron que ejecuta el scheduler cada minuto:
* * * * * cd /ruta/a/tu/proyecto && php artisan schedule:run >> /dev/null 2>&1
Para editar el crontab en Linux/Mac:
crontab -e
En desarrollo local, puedes ejecutar el scheduler manualmente o dejarlo corriendo en segundo plano:
# Ejecutar una vez (comprueba qué tareas tocan ahora)
php artisan schedule:run
# Ejecutar continuamente cada minuto (desarrollo)
php artisan schedule:work
Definir tareas programadas
Las tareas se definen en routes/console.php
(Laravel 11+) o en el método schedule()
de app/Console/Kernel.php (versiones
anteriores). Aquí usamos el enfoque moderno:
<?php
// routes/console.php
use Illuminate\Support\Facades\Schedule;
// Ejecutar un comando Artisan cada hora
Schedule::command('app:clean-temp-files')->hourly();
// Ejecutar un Job cada día a las 2:00 AM
Schedule::job(new App\Jobs\GenerateDailyReport)->dailyAt('02:00');
// Ejecutar código directamente cada 5 minutos
Schedule::call(function () {
\App\Models\User::whereNull('email_verified_at')
->where('created_at', '<', now()->subDays(7))
->delete();
})->everyFiveMinutes();
Frecuencias disponibles
Laravel proporciona métodos expresivos para definir cuándo ejecutar cada tarea:
<?php
use Illuminate\Support\Facades\Schedule;
// Cada X minutos
Schedule::command('inspire')->everyMinute();
Schedule::command('inspire')->everyFiveMinutes();
Schedule::command('inspire')->everyTenMinutes();
Schedule::command('inspire')->everyFifteenMinutes();
Schedule::command('inspire')->everyThirtyMinutes();
// Cada hora
Schedule::command('inspire')->hourly();
Schedule::command('inspire')->hourlyAt(17); // A los :17 de cada hora
// Diario
Schedule::command('inspire')->daily(); // A las 00:00
Schedule::command('inspire')->dailyAt('13:00'); // A las 13:00
Schedule::command('inspire')->twiceDaily(1, 13); // A la 1:00 y 13:00
// Semanal
Schedule::command('inspire')->weekly();
Schedule::command('inspire')->weeklyOn(1, '8:00'); // Lunes a las 8:00
// Mensual
Schedule::command('inspire')->monthly();
Schedule::command('inspire')->monthlyOn(15, '9:00'); // El día 15 a las 9:00
// Días específicos
Schedule::command('inspire')->weekdays(); // Lunes a viernes
Schedule::command('inspire')->weekends(); // Sábados y domingos
Schedule::command('inspire')->sundays();
Schedule::command('inspire')->mondays();
// ... tuesdays(), wednesdays(), thursdays(), fridays(), saturdays()
También puedes usar expresiones cron directamente:
<?php
// Expresión cron: minuto hora día mes día-semana
Schedule::command('emails:send')->cron('0 6 * * 1-5'); // Lunes a viernes a las 6:00
Programar comandos Artisan
La forma más común es programar comandos Artisan que hayas creado:
php artisan make:command CleanExpiredTokens
<?php
// app/Console/Commands/CleanExpiredTokens.php
declare(strict_types=1);
namespace App\Console\Commands;
use App\Models\PasswordResetToken;
use Illuminate\Console\Command;
class CleanExpiredTokens extends Command
{
protected $signature = 'app:clean-expired-tokens';
protected $description = 'Elimina tokens de recuperación expirados';
public function handle(): int
{
$deleted = PasswordResetToken::where('created_at', '<', now()->subHour())
->delete();
$this->info("Eliminados {$deleted} tokens expirados.");
return Command::SUCCESS;
}
}
<?php
// routes/console.php
use Illuminate\Support\Facades\Schedule;
Schedule::command('app:clean-expired-tokens')->hourly();
Programar Jobs
También puedes programar Jobs (que vimos en la lección de Colas):
<?php
use App\Jobs\SendWeeklyNewsletter;
use App\Jobs\CleanOldLogs;
use Illuminate\Support\Facades\Schedule;
// Ejecutar un Job cada semana
Schedule::job(new SendWeeklyNewsletter)->weekly()->mondays()->at('09:00');
// Job con parámetros
Schedule::job(new CleanOldLogs(days: 30))->daily();
// Enviar el Job a una cola específica
Schedule::job(new SendWeeklyNewsletter, 'emails')->weekly();
Evitar solapamiento
Si una tarea tarda más que su frecuencia, podrían
ejecutarse varias instancias a la vez. Usa
withoutOverlapping() para evitarlo:
<?php
use Illuminate\Support\Facades\Schedule;
// No se ejecutará si la anterior aún está corriendo
Schedule::command('app:sync-products')
->everyFiveMinutes()
->withoutOverlapping();
// Con tiempo máximo de bloqueo (default 24 horas)
Schedule::command('app:long-running-task')
->daily()
->withoutOverlapping(expiresAt: 60); // 60 minutos
Ejecutar en un solo servidor
Si tu aplicación corre en múltiples servidores, puedes asegurar que la tarea solo se ejecute en uno:
<?php
use Illuminate\Support\Facades\Schedule;
Schedule::command('app:send-daily-report')
->daily()
->onOneServer();
Nota: Para que onOneServer()
funcione, necesitas configurar un driver de
caché compartido (como Redis o la base de datos)
en lugar del driver file.
Ejecutar en segundo plano
Por defecto, las tareas se ejecutan secuencialmente.
Si tienes varias tareas que deben ejecutarse al
mismo tiempo, usa runInBackground():
<?php
use Illuminate\Support\Facades\Schedule;
// Estas tareas se ejecutarán en paralelo
Schedule::command('app:task-one')->daily()->runInBackground();
Schedule::command('app:task-two')->daily()->runInBackground();
Condiciones de ejecución
Puedes añadir condiciones para que una tarea solo se ejecute en ciertos escenarios:
<?php
use Illuminate\Support\Facades\Schedule;
// Solo en producción
Schedule::command('app:optimize-db')
->weekly()
->environments(['production']);
// Solo si la condición se cumple
Schedule::command('app:send-promotions')
->daily()
->when(fn () => app()->isProduction());
// Solo si NO se cumple la condición
Schedule::command('app:debug-report')
->hourly()
->skip(fn () => app()->isProduction());
Hooks: antes y después
Puedes ejecutar código antes o después de una tarea, y en caso de éxito o fallo:
<?php
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Schedule;
Schedule::command('app:process-payments')
->daily()
->before(function () {
Log::info('Iniciando procesamiento de pagos...');
})
->after(function () {
Log::info('Procesamiento de pagos completado.');
})
->onSuccess(function () {
// Se ejecuta si no hubo errores
})
->onFailure(function () {
Log::error('Error en el procesamiento de pagos.');
});
Notificar resultados
Puedes enviar la salida de una tarea por email o hacer ping a una URL:
<?php
use Illuminate\Support\Facades\Schedule;
// Enviar output por email
Schedule::command('app:generate-report')
->weekly()
->emailOutputTo('admin@example.com');
// Solo si hubo errores
Schedule::command('app:critical-task')
->hourly()
->emailOutputOnFailure('devops@example.com');
// Ping a servicio de monitoreo (útil para healthchecks)
Schedule::command('app:heartbeat')
->everyMinute()
->pingOnSuccess('https://healthcheck.io/abc123');
Ver tareas programadas
Para ver todas las tareas y cuándo se ejecutarán:
php artisan schedule:list
Esto muestra una tabla con cada tarea, su expresión cron, descripción y próxima ejecución.
Ejemplo completo
Un archivo routes/console.php típico
con varias tareas:
<?php
use App\Jobs\SendWeeklyNewsletter;
use App\Jobs\CleanExpiredSessions;
use Illuminate\Support\Facades\Schedule;
// Limpieza de sesiones expiradas cada hora
Schedule::job(new CleanExpiredSessions)
->hourly()
->description('Limpiar sesiones expiradas');
// Eliminar tokens de password reset cada 6 horas
Schedule::command('app:clean-expired-tokens')
->everySixHours()
->withoutOverlapping()
->description('Limpiar tokens expirados');
// Enviar newsletter los lunes a las 9:00
Schedule::job(new SendWeeklyNewsletter)
->weekly()
->mondays()
->at('09:00')
->onOneServer()
->description('Enviar newsletter semanal');
// Backup de base de datos diario (solo producción)
Schedule::command('backup:run')
->dailyAt('03:00')
->environments(['production'])
->onOneServer()
->emailOutputOnFailure('admin@example.com')
->description('Backup diario de BD');
// Sincronizar productos desde API externa
Schedule::command('app:sync-products')
->everyThirtyMinutes()
->withoutOverlapping(expiresAt: 25)
->runInBackground()
->description('Sincronizar productos');
Resumen
- El Task Scheduler permite definir tareas automáticas en PHP
- Solo necesitas una entrada cron:
* * * * * php artisan schedule:run - Define tareas en
routes/console.phpconSchedule::command()oSchedule::job() - Usa métodos como
hourly(),daily(),weekly()para frecuencias withoutOverlapping()evita ejecuciones simultáneasonOneServer()asegura ejecución única en clusters- Usa
php artisan schedule:listpara ver tareas yschedule:workpara desarrollo
Ejercicios
Ejercicio 1: Limpieza de archivos temporales
Crea un comando app:clean-temp-files
que elimine archivos de más de 24 horas del directorio
storage/app/temp. Prográmalo para
ejecutarse cada 6 horas.
Ver solución
php artisan make:command CleanTempFiles
<?php
// app/Console/Commands/CleanTempFiles.php
declare(strict_types=1);
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;
class CleanTempFiles extends Command
{
protected $signature = 'app:clean-temp-files';
protected $description = 'Elimina archivos temporales de más de 24 horas';
public function handle(): int
{
$files = Storage::disk('local')->files('temp');
$deleted = 0;
foreach ($files as $file) {
$lastModified = Storage::disk('local')->lastModified($file);
if ($lastModified < now()->subDay()->timestamp) {
Storage::disk('local')->delete($file);
$deleted++;
}
}
$this->info("Eliminados {$deleted} archivos temporales.");
return Command::SUCCESS;
}
}
<?php
// routes/console.php
use Illuminate\Support\Facades\Schedule;
Schedule::command('app:clean-temp-files')
->everySixHours()
->description('Limpiar archivos temporales');
Ejercicio 2: Recordatorio de usuarios inactivos
Crea un Job SendInactivityReminder
que envíe una notificación a usuarios que no han
iniciado sesión en los últimos 30 días. Prográmalo
para ejecutarse cada lunes a las 10:00, solo en
producción.
Ver solución
php artisan make:job SendInactivityReminder
php artisan make:notification InactivityReminder
<?php
// app/Jobs/SendInactivityReminder.php
declare(strict_types=1);
namespace App\Jobs;
use App\Models\User;
use App\Notifications\InactivityReminder;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class SendInactivityReminder implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function handle(): void
{
User::where('last_login_at', '<', now()->subDays(30))
->whereNotNull('last_login_at')
->chunk(100, function ($users) {
foreach ($users as $user) {
$user->notify(new InactivityReminder());
}
});
}
}
<?php
// app/Notifications/InactivityReminder.php
declare(strict_types=1);
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class InactivityReminder extends Notification
{
use Queueable;
public function via(object $notifiable): array
{
return ['mail'];
}
public function toMail(object $notifiable): MailMessage
{
return (new MailMessage)
->subject('Te echamos de menos')
->greeting("Hola {$notifiable->name}")
->line('Hace tiempo que no te vemos por aquí.')
->line('¿Hay algo en lo que podamos ayudarte?')
->action('Visitar la plataforma', route('dashboard'))
->salutation('El equipo de ' . config('app.name'));
}
}
<?php
// routes/console.php
use App\Jobs\SendInactivityReminder;
use Illuminate\Support\Facades\Schedule;
Schedule::job(new SendInactivityReminder)
->weekly()
->mondays()
->at('10:00')
->environments(['production'])
->onOneServer()
->description('Recordatorio a usuarios inactivos');
Ejercicio 3: Reporte diario con hooks
Crea un comando app:daily-stats que
calcule estadísticas del día (usuarios nuevos,
pedidos, ingresos). Usa hooks para registrar
en el log cuándo inicia y termina, y envía email
si falla.
Ver solución
php artisan make:command DailyStats
<?php
// app/Console/Commands/DailyStats.php
declare(strict_types=1);
namespace App\Console\Commands;
use App\Models\Order;
use App\Models\User;
use Illuminate\Console\Command;
class DailyStats extends Command
{
protected $signature = 'app:daily-stats';
protected $description = 'Genera estadísticas del día';
public function handle(): int
{
$today = now()->startOfDay();
$stats = [
'new_users' => User::whereDate('created_at', $today)->count(),
'orders' => Order::whereDate('created_at', $today)->count(),
'revenue' => Order::whereDate('created_at', $today)
->where('status', 'completed')
->sum('total'),
];
$this->info('Estadísticas del día:');
$this->table(
['Métrica', 'Valor'],
[
['Nuevos usuarios', $stats['new_users']],
['Pedidos', $stats['orders']],
['Ingresos', number_format($stats['revenue'], 2) . ' €'],
]
);
return Command::SUCCESS;
}
}
<?php
// routes/console.php
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Schedule;
Schedule::command('app:daily-stats')
->dailyAt('23:55')
->before(function () {
Log::info('Iniciando generación de estadísticas diarias...');
})
->after(function () {
Log::info('Estadísticas diarias generadas correctamente.');
})
->onFailure(function () {
Log::error('Error al generar estadísticas diarias.');
})
->emailOutputOnFailure('admin@example.com')
->description('Estadísticas diarias');
¿Has encontrado un error o tienes una sugerencia para mejorar esta lección?
Escríbenos¿Te está gustando el curso?
Tenemos cursos premium con proyectos reales, soporte personalizado y certificado.
Descubrir cursos premium