Programació de tasques
Com programar tasques automàtiques amb el Task Scheduler de Laravel: freqüències, restriccions, monitorització i manteniment.
Per què existeix el Scheduler?#
Qualsevol aplicació web mínimament seriosa necessita executar tasques periòdiques: enviar correus de recordatori, netejar fitxers temporals, generar informes, renovar subscripcions, fer backups de la base de dades o sincronitzar dades amb serveis externs. Tradicionalment, cadascuna d'aquestes tasques requeria una entrada independent al crontab del servidor. Amb deu tasques programades, el crontab tenia deu línies, cadascuna amb la seva freqüència, la seva ruta al projecte i la seva comanda.
Això creava diversos problemes. Primer, la configuració del crontab no es guardava al control de versions: si havies de migrar a un nou servidor, havies de recordar (o documentar) totes les entrades cron. Segon, canviar la freqüència d'una tasca requeria accés SSH al servidor i editar el crontab manualment. Tercer, no hi havia cap mecanisme integrat per prevenir execucions simultànies, gestionar errors o monitoritzar si les tasques s'executaven correctament.
El Task Scheduler de Laravel resol tots aquests problemes amb una sola entrada cron. Aquesta única entrada executa php artisan schedule:run cada minut, i Laravel s'encarrega de determinar quines tasques toca executar en aquell moment concret. Les tasques es defineixen en codi PHP, es guarden al repositori Git i es poden desplegar amb la resta de l'aplicació. Canviar la freqüència d'una tasca és tan senzill com modificar una línia de codi i fer un deploy.
Definir tasques programades#
Les tasques programades es defineixen al fitxer routes/console.php utilitzant la facade Schedule. Cada tasca es defineix amb el tipus de tasca (comanda Artisan, closure, comanda shell o job) seguit de la freqüència d'execució. Laravel avalua totes les tasques cada minut i executa les que corresponen segons la seva configuració de freqüència.
El patró bàsic és cridar un mètode de la facade Schedule per definir la tasca i encadenar un mètode de freqüència. Les tasques es defineixen de manera declarativa i expressiva, cosa que fa que el fitxer routes/console.php serveixi alhora com a documentació de totes les tasques programades de l'aplicació:
// routes/console.php
use Illuminate\Support\Facades\Schedule;
// Comandes Artisan
Schedule::command('articles:publish-scheduled')->hourly();
Schedule::command('telescope:prune')->daily();
Schedule::command('backup:run')->dailyAt('02:00');
Schedule::command('sitemap:generate')->weekly();
Schedule::command('invoices:send-reminders')->dailyAt('09:00');Closures#
Les closures són útils per a tasques senzilles que no justifiquen crear una comanda Artisan completa. Per exemple, netejar registres antics d'una taula o actualitzar un comptador. Si la lògica de la tasca requereix més d'unes poques línies, és millor crear una comanda Artisan dedicada perquè serà més fàcil de testejar i mantenir:
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schedule;
// Closure per a tasques senzilles
Schedule::call(function () {
DB::table('password_reset_tokens')
->where('created_at', '<', now()->subHours(2))
->delete();
})->hourly();
// Closure amb nom (útil per a schedule:list)
Schedule::call(function () {
cache()->forget('homepage_stats');
cache()->forget('dashboard_metrics');
})->everyFifteenMinutes()
->name('clear-stats-cache');Comandes shell#
De vegades necessites executar una comanda del sistema operatiu que no és una comanda Artisan. Per exemple, un script de backup, una sincronització amb rsync o qualsevol binari del sistema. El mètode exec permet executar qualsevol comanda shell:
// Comandes del sistema operatiu
Schedule::exec('node /home/forge/script.js')->daily();
Schedule::exec('bash /home/forge/backup.sh')->dailyAt('03:00');
// Amb arguments
Schedule::exec('pg_dump -U forge mydatabase > backup.sql')
->dailyAt('04:00');Jobs de cua#
Si la tasca programada hauria d'executar-se de manera asíncrona a través del sistema de cues, pots programar un job directament. Això és útil per a tasques que poden trigar molt i no vols que bloquegin el scheduler. El job s'afegirà a la cua i es processarà pel worker com qualsevol altre job:
use App\Jobs\GenerateMonthlyReport;
use App\Jobs\SyncInventory;
use Illuminate\Support\Facades\Schedule;
Schedule::job(new GenerateMonthlyReport)->monthlyOn(1, '06:00');
Schedule::job(new SyncInventory)->everyThirtyMinutes();
// Especificar la cua i la connexió
Schedule::job(new GenerateMonthlyReport, 'reports', 'redis')
->monthlyOn(1, '06:00');Opcions de freqüència#
Laravel ofereix una gamma completa de mètodes per definir la freqüència d'execució de les tasques. Des d'execucions cada minut fins a execucions anuals, passant per expressions cron personalitzades per a qualsevol patró que no estigui cobert pels mètodes predefinits.
Els mètodes de freqüència són intuïtius i llegibles. Quan veies ->dailyAt('09:00') al codi, saps immediatament que la tasca s'executa cada dia a les 9 del matí. Això és molt més clar que l'equivalent cron 0 9 * * *, que requereix desxifrar les cinc posicions del format cron:
// Cada minut
Schedule::command('monitor:check')->everyMinute();
// Cada dos minuts
Schedule::command('queue:monitor')->everyTwoMinutes();
// Cada cinc minuts
Schedule::command('stats:update')->everyFiveMinutes();
// Cada deu minuts
Schedule::command('feeds:fetch')->everyTenMinutes();
// Cada quinze minuts
Schedule::command('cache:warm')->everyFifteenMinutes();
// Cada trenta minuts
Schedule::command('sync:products')->everyThirtyMinutes();
// Cada hora
Schedule::command('reports:hourly')->hourly();
// Cada hora a un minut concret
Schedule::command('cleanup:temp')->hourlyAt(15);
// Cada dues hores
Schedule::command('analytics:aggregate')->everyTwoHours();
// Cada dia a mitjanit
Schedule::command('logs:prune')->daily();
// Cada dia a una hora concreta
Schedule::command('invoices:generate')->dailyAt('08:00');
// Dues vegades al dia
Schedule::command('stats:compile')->twiceDaily(8, 18);
// Cada setmana (dilluns a mitjanit)
Schedule::command('reports:weekly')->weekly();
// Cada setmana en un dia i hora concrets
Schedule::command('newsletter:send')->weeklyOn(5, '10:00');
// Cada mes (dia 1 a mitjanit)
Schedule::command('billing:process')->monthly();
// Cada mes en un dia i hora concrets
Schedule::command('reports:monthly')->monthlyOn(15, '09:00');
// Cada trimestre
Schedule::command('reports:quarterly')->quarterly();
// Cada any
Schedule::command('certificates:renew')->yearly();
// Expressió cron personalitzada
Schedule::command('custom:task')->cron('0 */6 * * 1-5');L'expressió cron del darrer exemple (0 */6 * * 1-5) significa "a l'hora en punt, cada 6 hores, de dilluns a divendres". Per a patrons que els mètodes predefinits no cobreixen, les expressions cron donen flexibilitat total.
Restriccions#
Les restriccions permeten afinar quan s'executa una tasca més enllà de la freqüència bàsica. Pots limitar l'execució a certs dies de la setmana, a un rang horari, a entorns específics o a condicions personalitzades. Les restriccions s'encadenen després de la freqüència i es poden combinar entre elles.
Aquestes restriccions són especialment útils per a tasques que no haurien d'executar-se sempre. Per exemple, un informe que només té sentit en dies laborables, un procés de facturació que només s'ha d'executar en producció, o una tasca de sincronització que depèn de la disponibilitat d'un servei extern:
// Només dies laborables (dilluns a divendres)
Schedule::command('reports:daily')
->daily()
->weekdays();
// Només caps de setmana
Schedule::command('backup:full')
->daily()
->weekends();
// Només en un dia concret
Schedule::command('payroll:process')
->monthly()
->mondays();
// Només en horari laboral
Schedule::command('notifications:send')
->everyFifteenMinutes()
->between('08:00', '18:00');
// Excloure un rang horari
Schedule::command('heavy:process')
->hourly()
->unlessBetween('09:00', '17:00');
// Només en producció
Schedule::command('billing:charge')
->daily()
->environments(['production']);
// Només si es compleix una condició
Schedule::command('sync:external')
->hourly()
->when(function () {
return config('services.external.enabled');
});
// Saltar si es compleix una condició
Schedule::command('emails:promotional')
->daily()
->skip(function () {
return app()->isDownForMaintenance();
});Fus horari#
Per defecte, les tasques programades utilitzen el fus horari configurat a config/app.php. Si necessites que una tasca s'executi segons un fus horari diferent (per exemple, enviar correus a les 9 del matí hora local dels teus clients), pots especificar-lo amb el mètode timezone:
Schedule::command('notifications:morning')
->dailyAt('09:00')
->timezone('Europe/Andorra');
Schedule::command('reports:us')
->dailyAt('08:00')
->timezone('America/New_York');Prevenir solapaments#
Si una tasca programada triga més del previst (per exemple, un informe que normalment triga 5 minuts però un dia en triga 90), el scheduler podria iniciar una nova execució de la mateixa tasca abans que la primera acabi. Això pot causar problemes greus: processar les mateixes dades dues vegades, enviar correus duplicats o sobrecarregar el servidor.
El mètode withoutOverlapping impedeix que una tasca s'executi si una instància anterior encara està en curs. Laravel utilitza un lock (per defecte al driver de cache) per controlar si la tasca s'està executant. El lock es manté durant 24 hores per defecte, però pots configurar-ho amb el paràmetre expiresAt:
// No executar si l'anterior encara no ha acabat
Schedule::command('reports:generate')
->hourly()
->withoutOverlapping();
// El lock expira després de 30 minuts (en cas de fallada)
Schedule::command('sync:products')
->everyFifteenMinutes()
->withoutOverlapping(30);El paràmetre expiresAt és important com a mesura de seguretat. Si la tasca falla sense alliberar el lock (per exemple, el procés PHP es mata per un error de memòria), el lock quedaria actiu indefinidament i la tasca no tornaria a executar-se mai. Amb expiresAt, el lock caduca automàticament després del temps especificat, permetent que la tasca es torni a executar.
Execució en un sol servidor#
Quan l'aplicació s'executa en múltiples servidors (per exemple, tres servidors web darrere d'un load balancer), cada servidor té el seu propi crontab que executa schedule:run cada minut. Sense cap mesura addicional, cada tasca s'executaria tres vegades (una per servidor), cosa que seria desastrosa per a tasques com enviar factures o processar pagaments.
El mètode onOneServer garanteix que la tasca només s'executa en un dels servidors. Laravel utilitza un lock distribuït (Redis o base de dades) per coordinar els servidors: el primer que obté el lock executa la tasca, i els altres la salten. Això requereix que tots els servidors comparteixin el mateix driver de cache (normalment Redis):
Schedule::command('reports:generate')
->daily()
->onOneServer();
Schedule::command('billing:charge')
->monthlyOn(1, '06:00')
->onOneServer()
->withoutOverlapping();
Schedule::command('sitemap:generate')
->weekly()
->onOneServer();La combinació de onOneServer i withoutOverlapping és especialment robusta: onOneServer assegura que només un servidor executa la tasca, i withoutOverlapping assegura que no es solapa amb una execució anterior que encara estigui en curs.
Tasques en segon pla#
Per defecte, les tasques programades s'executen de manera seqüencial: el scheduler espera que una tasca acabi abans de començar la següent. Si tens una tasca que triga 10 minuts, totes les tasques que venen després s'endarreriran 10 minuts. Això pot ser problemàtic si tens tasques que han d'executar-se a hores precises.
El mètode runInBackground fa que la tasca s'executi en un procés PHP separat, permetent que el scheduler continuï immediatament amb les tasques restants. Això no afecta el resultat de la tasca, només evita que bloquegi les altres:
Schedule::command('reports:generate')
->daily()
->runInBackground();
Schedule::command('sync:products')
->everyThirtyMinutes()
->runInBackground()
->withoutOverlapping();Cal tenir en compte que les tasques en segon pla no funcionen amb closures, només amb comandes Artisan i comandes shell. Això és perquè Laravel necessita poder executar la tasca en un procés PHP independent, cosa que no és possible amb una closure.
Mode de manteniment#
Per defecte, les tasques programades no s'executen quan l'aplicació està en mode de manteniment (php artisan down). Això té sentit per a la majoria de tasques, perquè si l'aplicació està en manteniment és probable que la base de dades o altres serveis no estiguin disponibles.
Però algunes tasques haurien d'executar-se fins i tot durant el manteniment: per exemple, un monitor de salut del servidor o un procés de backup. El mètode evenInMaintenanceMode permet que la tasca s'executi sempre:
Schedule::command('monitor:health')
->everyMinute()
->evenInMaintenanceMode();
Schedule::command('backup:run')
->dailyAt('02:00')
->evenInMaintenanceMode();Gestió de la sortida#
Les tasques programades generen sortida (output) que normalment es perd perquè s'executen en segon pla des del crontab. Laravel ofereix diversos mètodes per capturar i gestionar aquesta sortida: escriure-la a un fitxer, enviar-la per correu o fer-ne log.
Capturar la sortida és especialment útil per a tasques crítiques on vols poder verificar posteriorment que s'han executat correctament i revisar qualsevol avís o error que hagin generat:
// Escriure la sortida a un fitxer (sobreescriu)
Schedule::command('reports:generate')
->daily()
->sendOutputTo(storage_path('logs/reports.log'));
// Afegir la sortida a un fitxer (no sobreescriu)
Schedule::command('sync:products')
->hourly()
->appendOutputTo(storage_path('logs/sync.log'));
// Enviar la sortida per correu
Schedule::command('billing:process')
->monthly()
->sendOutputTo(storage_path('logs/billing.log'))
->emailOutputTo('admin@example.com');
// Enviar correu només si hi ha errors
Schedule::command('imports:run')
->daily()
->emailOutputOnFailure('admin@example.com');El mètode emailOutputOnFailure és particularment útil perquè només envia el correu si la tasca retorna un codi de sortida diferent de zero (que indica error). Així no inundes la bústia amb correus de tasques que funcionen correctament.
Hooks de tasques#
Els hooks permeten executar codi abans, després, en cas d'èxit o en cas d'error d'una tasca programada. Això és útil per a monitorització, notificacions i neteja. Per exemple, pots enviar un ping a un servei de monitorització per confirmar que la tasca s'ha executat, notificar l'equip si falla o registrar el temps d'execució.
Laravel també suporta pings HTTP, que són la manera més senzilla d'integrar el scheduler amb serveis de monitorització externs com Healthchecks.io, Oh Dear o Envoyer. Un ping HTTP és simplement una petició GET a una URL que el servei utilitza per confirmar que la tasca s'ha executat dins del temps previst:
Schedule::command('reports:generate')
->daily()
->before(function () {
Log::info('Iniciant generació d\'informes.');
})
->after(function () {
Log::info('Generació d\'informes completada.');
})
->onSuccess(function () {
// Només s'executa si la tasca ha acabat sense errors
Notification::send($admins, new ReportReady());
})
->onFailure(function () {
// Només s'executa si la tasca ha fallat
Notification::send($admins, new ReportFailed());
});Pings HTTP#
Els pings HTTP són una eina fonamental per a la monitorització de tasques programades en producció. Serveis com Healthchecks.io funcionen de la manera següent: crees un check amb una freqüència esperada (per exemple, "ha de rebre un ping cada hora"), i si el ping no arriba dins del temps previst, el servei envia una alerta. Això detecta tant tasques que fallen com tasques que deixen d'executar-se silenciosament:
Schedule::command('billing:process')
->daily()
->pingBefore('https://hc-ping.com/xxxxx/start')
->thenPing('https://hc-ping.com/xxxxx')
->pingOnSuccess('https://hc-ping.com/xxxxx')
->pingOnFailure('https://hc-ping.com/xxxxx/fail');El mètode pingBefore envia un ping just abans que la tasca comenci. El mètode thenPing l'envia quan acaba (tant si ha tingut èxit com si ha fallat). Els mètodes pingOnSuccess i pingOnFailure permeten distingir entre els dos resultats. Combinar pingBefore i pingOnSuccess permet al servei de monitorització mesurar la durada de la tasca i alertar si triga més del previst.
Executar el Scheduler en producció#
Per posar el scheduler en marxa a producció, només necessites una única entrada al crontab del servidor. Aquesta entrada executa schedule:run cada minut, i Laravel determina internament quines tasques toca executar:
* * * * * cd /path-to-project && php artisan schedule:run >> /dev/null 2>&1L'expressió * * * * * significa "cada minut de cada hora de cada dia de cada mes de cada dia de la setmana". La redirecció >> /dev/null 2>&1 descarta tota la sortida del crontab perquè la gestió de la sortida es fa dins de Laravel amb els mètodes sendOutputTo i similars.
Si utilitzes Laravel Forge, Envoyer o un servei similar de desplegament, l'entrada cron es configura automàticament quan desplegues l'aplicació. Si gestions el servidor manualment, pots editar el crontab amb crontab -e i afegir la línia anterior.
Per verificar que l'entrada cron funciona correctament, pots comprovar que schedule:run s'executa:
# Executar manualment per verificar
php artisan schedule:run
# Veure la sortida de les tasques
php artisan schedule:run --verboseExecutar localment#
Durant el desenvolupament, no tens crontab configurat perquè el teu ordinador local no és un servidor de producció. La comanda schedule:work simula el crontab executant schedule:run cada minut de manera contínua:
php artisan schedule:workAquesta comanda s'executa en primer pla i mostra la sortida de cada tasca a la terminal. Això és molt útil per depurar tasques programades: pots veure en temps real si s'executen, amb quina freqüència i quina sortida generen. Per aturar-la, prem Ctrl+C.
Cal tenir en compte que schedule:work s'executa amb les mateixes regles que schedule:run: si una tasca està configurada per executar-se només en producció (->environments(['production'])), no s'executarà en local. Per testejar-la, hauries de canviar temporalment l'entorn o eliminar la restricció.
Llistar tasques programades#
La comanda schedule:list mostra totes les tasques programades amb la seva freqüència, la pròxima execució prevista i la descripció. Això és extremadament útil per verificar que la configuració és correcta i per documentar les tasques de l'aplicació:
php artisan schedule:listLa sortida mostra una taula amb cada tasca, la seva expressió cron, la pròxima execució programada i el nom o descripció de la tasca. Si una tasca té restriccions (com weekdays o environments), la sortida ho indica. Aquesta comanda és el primer lloc on mires quan una tasca no s'executa quan hauria: potser la freqüència no és la que pensaves, o una restricció impedeix l'execució.
Pots afegir descripcions a les tasques amb el mètode description per fer la sortida de schedule:list més informativa:
Schedule::command('reports:generate')
->daily()
->description('Genera els informes diaris de vendes');
Schedule::command('backup:run')
->dailyAt('02:00')
->description('Backup complet de la base de dades');
Schedule::command('sync:products')
->everyThirtyMinutes()
->description('Sincronitza productes amb el proveïdor extern');Testejar tasques programades#
La comanda schedule:test permet executar una tasca programada individualment, seleccionant-la d'una llista interactiva. Això és útil per verificar que una tasca funciona correctament sense haver d'esperar que arribi el moment de la seva execució programada:
php artisan schedule:testAquesta comanda mostra totes les tasques programades i et permet seleccionar quina vols executar. La tasca s'executa immediatament, ignorant la seva freqüència i restriccions configurades. Això és especialment útil per a tasques que s'executen poc freqüentment (setmanals, mensuals) i que no vols esperar per verificar.
Monitorització i salut#
En producció, no n'hi ha prou amb programar les tasques: has de verificar que realment s'executen. Una tasca pot deixar d'executar-se per molts motius: el crontab s'ha esborrat durant una actualització del servidor, el procés PHP ha fallat, el disc s'ha omplert o el servidor Redis (que gestiona els locks) no respon.
La combinació de pings HTTP i serveis de monitorització és la manera més fiable de detectar problemes. Serveis com Healthchecks.io, Oh Dear o Cronitor estan dissenyats específicament per a això: esperen un ping a intervals regulars i alerten si el ping no arriba. Això cobreix tant les fallades de la tasca com les situacions on el scheduler sencer ha deixat de funcionar.
Per a un nivell addicional de monitorització, pots configurar un heartbeat: una tasca simple que s'executa cada minut i envia un ping. Si el ping deixa d'arribar, saps que hi ha un problema amb el crontab o amb el servidor:
Schedule::call(function () {
// Heartbeat: confirma que el scheduler funciona
})->everyMinute()
->name('scheduler-heartbeat')
->pingOnSuccess('https://hc-ping.com/xxxxx');Finalment, és bona pràctica registrar l'inici i la fi de les tasques crítiques al log. Això crea un historial que pots consultar per verificar quan s'han executat les tasques i si han tingut errors:
Schedule::command('billing:process')
->monthlyOn(1, '06:00')
->onOneServer()
->withoutOverlapping()
->before(function () {
Log::info('Iniciant processat de factures mensual.');
})
->onSuccess(function () {
Log::info('Processat de factures completat correctament.');
})
->onFailure(function () {
Log::error('Error durant el processat de factures.');
})
->pingOnSuccess('https://hc-ping.com/xxxxx')
->pingOnFailure('https://hc-ping.com/xxxxx/fail')
->emailOutputOnFailure('admin@example.com');Aquesta configuració combina totes les bones pràctiques: execució en un sol servidor, prevenció de solapaments, logging, pings de monitorització i notificació per correu en cas d'error. Per a tasques crítiques com el processat de factures, val la pena invertir en una configuració robusta que garanteixi l'execució fiable i la detecció ràpida de problemes.