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

  1. Schedule::call(closure) — menjalankan closure langsung di dalam proses schedule: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.
  2. 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.
  3. Schedule::job(new SendDailyReport) — mendispatch queue job. Karena SendDailyReport mengimplementasikan ShouldQueue, pekerjaan berat berpindah ke antrean dan dijalankan worker, bukan oleh proses scheduler.
  4. Schedule::exec('...') — mengeksekusi perintah shell sistem operasi secara langsung, berguna untuk alat eksternal seperti pg_dump atau curl.

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:

php
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:

bash
php artisan schedule:test --name=ringkasan-tugas

Output yang Dihasilkan:

text
  Running [ringkasan-tugas] ..................................... 18.07ms DONE

Baris DONE di atas hanya membuktikan proses selesai — bukti efek sampingnya kita periksa dari cache dan log yang ditulis closure:

text
Tugas tertunda: 2
text
[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:

bash
php artisan schedule:test --name=tasks:prune

Output yang Dihasilkan:

text
  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:

text
  0  * * * *  Closure at: routes/console.php:15  Next Due: 38 minutes from now
  30 2 * * *  php artisan tasks:prune ............. Next Due: 6 hours from now

Kemudian dengan ->name('ringkasan-tugas'):

text
  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 now

Kolom 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 .