Lección 36 de 45 12 min de lectura

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:

bash
* * * * * cd /ruta/a/tu/proyecto && php artisan schedule:run >> /dev/null 2>&1

Para editar el crontab en Linux/Mac:

bash
crontab -e

En desarrollo local, puedes ejecutar el scheduler manualmente o dejarlo corriendo en segundo plano:

bash
# 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
<?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
<?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
<?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:

bash
php artisan make:command CleanExpiredTokens
php
<?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
<?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
<?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
<?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
<?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
<?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
<?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
<?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
<?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:

bash
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
<?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.php con Schedule::command() o Schedule::job()
  • Usa métodos como hourly(), daily(), weekly() para frecuencias
  • withoutOverlapping() evita ejecuciones simultáneas
  • onOneServer() asegura ejecución única en clusters
  • Usa php artisan schedule:list para ver tareas y schedule:work para 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');

¿Te está gustando el curso?

Tenemos cursos premium con proyectos reales, soporte personalizado y certificado.

Descubrir cursos premium