Sekarang kita masuk ke praktik: menuliskan jadwal di routes/console.php menggunakan Schedule facade. Satu berkas ini menampung semua kebutuhan penjadwalan — dari closure singkat sampai pemanggilan command artisan dan queue job — dengan satu antarmuka berantai (fluent) yang konsisten. Pada chapter ini kita definisikan empat task untuk aplikasi pengelola tugas, beri nama pada masing-masing, lalu verifikasi hasilnya lewat schedule:list. Semua contoh dijalankan nyata di sandbox, jadi setiap output yang tampil adalah hasil eksekusi yang sebenarnya.
Titik Registrasi Jadwal #
Berkas routes/console.php bawaan Laravel 11+ berisi satu command contoh inspire:
<?php
use Illuminate\Foundation\Inspiring;
use Illuminate\Support\Facades\Artisan;
Artisan::command('inspire', function () {
$this->comment(Inspiring::quote());
})->purpose('Display an inspiring quote');Berkas ini dimuat lewat withRouting(commands: ...) di bootstrap/app.php, sama seperti routes/web.php untuk HTTP route. Artinya setiap kali proses artisan berjalan — termasuk schedule:run — definisi jadwal di berkas ini ikut tereksekusi dan terdaftar di container. Inilah yang membuat schedule:list selalu memantulkan isi terkini dari kode kita.
Perlu digarisbawahi bahwa penjadwalan di sini berbasis proses, bukan request. Tidak ada user yang menunggu, tidak ada HTTP timeout — yang ada hanya PHP CLI yang bangun tiap menit lewat cron, mengevaluasi jadwal, lalu menghilang. Konsekuensinya, segala yang kita tulis di berkas ini harus sadar konteks: jangan mengandalkan session user, jangan mengirim response, dan jangan berasumsi environment berjalan seperti di web server.
Empat Jenis Task #
Kita tambahkan empat task dengan jenis berbeda ke routes/console.php:
<?php
use App\Jobs\SendDailyReport;
use App\Models\Task;
use Illuminate\Contracts\Cache\Repository as CacheRepository;
use Illuminate\Foundation\Inspiring;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Schedule;
Artisan::command('inspire', function () {
$this->comment(Inspiring::quote());
})->purpose('Display an inspiring quote');
Schedule::call(function (CacheRepository $cache) {
$jumlah = Task::where('status', 'pending')->count();
$cache->put('ringkasan:terakhir', "Tugas tertunda: {$jumlah}", 300);
Log::info("Ringkasan tugas tertunda: {$jumlah}");
})->hourly()->name('ringkasan-tugas');
Schedule::command('tasks:prune')->dailyAt('02:30')->name('prune-tugas');
Schedule::job(new SendDailyReport)->dailyAt('06:00')->name('laporan-harian');
Schedule::exec('echo "backup database mulai"')->weeklyOn(1, '03:00')->name('backup-database');Mari kita bedah empat pola tersebut satu per satu:
Schedule::call(closure)— menjalankan closure langsung di dalam prosesschedule:run. Cocok untuk pekerjaan ringan seperti mencatat ringkasan. Karena dieksekusi lewat$container->call(), parameter bertipe pada closure di-resolve lewat dependency injection — closure di atas menerima instance cache repository tanpa kita membuatnya manual.Schedule::command('tasks:prune')— menjalankan command artisan sebagai subproses(php artisan tasks:prune). Logika kompleks sebaiknya tinggal di kelas command agar bisa diuji terpisah. Argumen command diteruskan sebagai string;Schedule::command('tasks:prune', ['--days' => 14])setara dengan sintaks array untuk parameter yang sama.Schedule::job(new SendDailyReport)— mendispatch queue job. KarenaSendDailyReportmengimplementasikanShouldQueue, pekerjaan berat berpindah ke antrean dan dijalankan worker, bukan oleh proses scheduler.Schedule::exec('...')— mengeksekusi perintah shell sistem operasi secara langsung, berguna untuk alat eksternal sepertipg_dumpataucurl.
Urutan method setelah pembuatan event bebas ditata: ->hourly() lalu ->name(), atau sebaliknya — hasil akhirnya sama karena seluruh method berantai mengembalikan objek event yang sama.
Job SendDailyReport sendiri adalah kelas biasa yang menghitung ringkasan dan menuliskannya ke log:
namespace App\Jobs;
use App\Models\Task;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Facades\Log;
class SendDailyReport implements ShouldQueue
{
use Queueable;
public function handle(): void
{
$pending = Task::where('status', 'pending')->count();
$selesai = Task::where('status', 'selesai')->count();
Log::info("Laporan harian: {$pending} tugas tertunda, {$selesai} tugas selesai.");
}
}Membuktikan Closure Benar-Benar Jalan #
Command schedule:test memungkinkan kita mengeksekusi task di luar waktunya untuk verifikasi cepat:
php artisan schedule:test --name=ringkasan-tugasOutput yang Dihasilkan:
Running [ringkasan-tugas] ..................................... 18.07ms DONEBaris DONE di atas hanya membuktikan proses selesai — bukti efek sampingnya kita periksa dari cache dan log yang ditulis closure:
Tugas tertunda: 2[2026-10-13 20:28:14] local.INFO: Ringkasan tugas tertunda: 2 Nilai 2 sesuai data sandbox: dua tugas berstatus pending. Type-hint CacheRepository $cache di signature closure terbukti terisi otomatis oleh container.
Command schedule:test juga bisa menjalankan task command — tapi perhatikan cara pencocokannya: opsi --name dibandingkan dengan string perintah, bukan nama dari ->name(). Untuk task di atas, yang cocok adalah --name=tasks:prune:
php artisan schedule:test --name=tasks:pruneOutput yang Dihasilkan:
Running ['artisan' tasks:prune] .............................. 221.82ms DONE
⇂ prune-tugas Baris ⇂ prune-tugas menunjukkan nama dari ->name() ikut tampil saat eksekusi — namun mencoba --name=prune-tugas justru ditolak dengan pesan No matching scheduled command found., karena framework mencocokkan string perintahnya. Untuk closure, identitas yang dicocokkan justru nama ->name() — persis seperti verifikasi --name=ringkasan-tugas di atas.
Penamaan Task dan schedule:list #
Method ->name() memberi identitas pada task. Tanpa nama, task closure tampil sebagai lokasi berkas di schedule:list; dengan nama, tampilannya jadi deskriptif. Bandingkan dua listing berikut — pertama tanpa ->name() pada closure:
0 * * * * Closure at: routes/console.php:15 Next Due: 38 minutes from now
30 2 * * * php artisan tasks:prune ............. Next Due: 6 hours from nowKemudian dengan ->name('ringkasan-tugas'):
0 * * * * ringkasan-tugas .................. Next Due: 38 minutes from now
30 2 * * * php artisan tasks:prune ............. Next Due: 6 hours from now
0 6 * * * laporan-harian ...................... Next Due: 9 hours from now
0 3 * * 1 echo "backup database mulai" ......... Next Due: 5 days from nowKolom pertama pada listing di atas adalah ekspresi cron hasil komposisi method frekuensi — hourly() menjadi 0 * * * *, dailyAt('02:30') menjadi 30 2 * * *. Bagaimana menggabungkan frekuensi dengan constraint hari dan jam? Itu topik chapter Schedule Frequency
. Daftar lengkap method definisi tersedia di dokumentasi Task Scheduling
.