Aplikasi console yang mumpuni baru menjadi separuh cerita jika harus selalu diketik manusia. Sebagian besar pekerjaan otomasi — mengirim laporan jam 3 pagi, membersihkan cache tiap 5 menit, memproses pesanan setiap jam — justru harus berjalan saat semua orang tidur. Di dunia Unix, penjadwal semacam itu bernama cron, dan PHP adalah salah satu penghuni tetapnya. Pada chapter ini kita akan memasang skrip PHP ke dalam cron, mencegah dua instance berjalan bersamaan dengan file lock, mencatat output job ke log, serta menguji logika “haruskah job ini berjalan” tanpa menunggu jam demi jam.
Cron dan Lima Kolom Waktunya #
Cron membaca daftar jadwal dari crontab — berkas milik tiap pengguna yang diedit lewat perintah crontab -e. Setiap baris crontab berisi lima kolom waktu diikuti perintah shell yang akan dijalankan. Kelima kolom tersebut, dari kiri ke kanan, adalah:
| Posisi | Makna | Rentang | Contoh |
|---|---|---|---|
| 1 | menit | 0-59 | 17 |
| 2 | jam | 0-23 | 3 |
| 3 | tanggal dalam bulan | 1-31 | * |
| 4 | bulan | 1-12 | * |
| 5 | hari dalam pekan | 0-6 (0 = Minggu) | * |
Tanda bintang berarti “setiap nilai”, sehingga 17 3 * * * terbaca sebagai “pukul 03.17 setiap hari”. Beberapa notasi ringkas lain yang perlu Anda kenal: */5 pada kolom menit berarti setiap 5 menit, 1,15 berarti daftar nilai, 9-17 berarti rentang, dan @daily adalah singkatan untuk 0 0 * * *. Satu baris jadwal lengkap untuk skrip PHP kita terlihat seperti ini:
17 3 * * * /usr/bin/php /var/www/toko/kirim-laporan.phpPerhatikan dua jalur absolut: lokasi binary php dan lokasi skrip. Kolom waktu dan perintah dipisahkan spasi, dan seluruh baris harus muat dalam satu baris tanpa lipatan.
Menjadwalkan Skrip PHP #
Lingkungan cron jauh lebih miskin daripada shell interaktif Anda: variabel PATH biasanya hanya /usr/bin:/bin, tidak ada konfigurasi shell, dan direktori kerja bukan direktori proyek. Karena itu skrip yang dijadwalkan harus mandiri — gunakan jalur absolut untuk semua berkas, dan jangan bergantung pada environment interaktif. Skrip hitung-pending.php berikut dirancang untuk dipanggil cron setiap 5 menit:
<?php
$berkas = "/tmp/jadwal/pesanan.csv";
@mkdir(dirname($berkas), 0777, true);
if (!file_exists($berkas)) {
// Data contoh untuk demo; job sungguhan membaca berkas produksi.
file_put_contents($berkas, "id,status\n1,pending\n2,diproses\n3,pending\n4,kirim\n");
}
$baris = file($berkas, FILE_IGNORE_NEW_LINES);
array_shift($baris); // buang baris header
$pending = 0;
foreach ($baris as $row) {
[, $status] = explode(",", $row);
if ($status === "pending") {
$pending++;
}
}
echo "Pesanan pending: $pending\n";
if ($pending > 0) {
echo "Menjadwalkan pemrosesan batch...\n";
}Output yang Dihasilkan:
Pesanan pending: 2
Menjadwalkan pemrosesan batch...Sebelum dipasang ke cron, uji dulu secara manual — job yang sama persis harus berjalan mulus dari terminal:
php hitung-pending.phpOutput yang Dihasilkan:
Pesanan pending: 2
Menjadwalkan pemrosesan batch...Baru setelah itu pasang jadwalnya, misalnya setiap 5 menit:
*/5 * * * * /usr/bin/php /var/www/toko/hitung-pending.phpMencegah Tumpang Tindih dengan flock #
Misalkan job Anda berjalan tiap menit, tetapi kadang butuh 90 detik. Tanpa pengaman, menit berikutnya cron meluncurkan instance kedua yang mengolah data yang sama bersamaan — laporan dobel, berkas korup, race condition klasik. Solusi Unix untuk ini adalah file lock: satu berkas kunci, dan hanya instance yang berhasil menguncinya yang boleh bekerja. Utilitas flock membungkus pola itu dalam satu baris crontab:
* * * * * flock -n /tmp/hitung-pending.lock /usr/bin/php /var/www/toko/hitung-pending.phpOpsi -n berarti non-blocking: bila kunci sedang dipegang instance lain, perintah baru langsung menyerah tanpa antre. PHP memiliki mekanisme yang sama lewat fungsi flock(). Contoh berikut menunjukkan keduanya sekaligus — anak proses (dijalankan dengan proc_open(), dibahas tuntas pada chapter Subprocesses) memegang kunci selama satu detik, sementara proses utama mencoba mengambilnya:
<?php
$lockPath = "/tmp/jadwal/pembersihan.lock";
@mkdir(dirname($lockPath), 0777, true);
// Anak proses memegang kunci selama kira-kira 1 detik, lalu keluar.
$kodeAnak = '$f = fopen($argv[1], "c");'
. ' flock($f, LOCK_EX);'
. ' sleep(1);';
$anak = proc_open(
[PHP_BINARY, "-r", $kodeAnak, $lockPath],
[0 => ["pipe", "r"], 1 => ["pipe", "w"], 2 => ["pipe", "w"]],
$pipes
);
fclose($pipes[0]);
fclose($pipes[1]);
fclose($pipes[2]);
usleep(500_000); // beri waktu anak mengunci berkas
$fh = fopen($lockPath, "c");
if (flock($fh, LOCK_EX | LOCK_NB)) {
echo "Kunci langsung diperoleh\n";
flock($fh, LOCK_UN);
} else {
echo "Instance lain sedang berjalan - eksekusi dilewati\n";
}
proc_close($anak); // tunggu anak selesai; kunci otomatis dilepas
if (flock($fh, LOCK_EX)) {
echo "Setelah instance lama selesai, kunci diperoleh\n";
flock($fh, LOCK_UN);
}
fclose($fh);Output yang Dihasilkan:
Instance lain sedang berjalan - eksekusi dilewati
Setelah instance lama selesai, kunci diperolehTiga hal terjadi di sini. Pertama, flock($fh, LOCK_EX | LOCK_NB) mencoba kunci eksklusif tanpa menunggu — gagal karena anak masih memegangnya, jadi instance baru memilih melompati eksekusi (di job nyata, titik ini adalah tempat exit(0) agar cron tidak menganggapnya gagal). Kedua, proc_close($anak) menunggu anak keluar, dan kernel melepas kunci secara otomatis saat proses pemegangnya mati — inilah alasan lock berbasis flock() aman bahkan saat job crash. Ketiga, percobaan kedua dengan mode blocking berhasil karena kunci sudah bebas.
Mencatat Output Job #
Job terjadwal berjalan tanpa terminal, jadi echo yang tidak ditangani akan dibuang — atau lebih buruk, dikirim cron lewat email ke mailbox lokal. Ada dua tempat pencatatan yang lazim. Yang pertama adalah log sistem cron: alihkan seluruh output di baris crontab:
17 3 * * * /usr/bin/php /var/www/toko/kirim-laporan.php >> /var/log/toko/laporan.log 2>&1Operator >> menambahkan (bukan menimpa), dan 2>&1 menyatukan stderr ke stdout sehingga pesan galat ikut tercatat. Cara kedua adalah mencatat sendiri dari dalam skrip dengan pola fungsi kecil berikut:
<?php
$logFile = "/tmp/jadwal/job.log";
@mkdir(dirname($logFile), 0777, true);
@unlink($logFile); // reset arena uji
function catatLog(string $pesan, DateTimeImmutable $waktu, string $file): void
{
file_put_contents($file, $waktu->format("[Y-m-d H:i:s]") . " $pesan\n", FILE_APPEND);
}
// Waktu dibekukan supaya contoh deterministik; job nyata memakai new DateTimeImmutable("now").
$waktu = new DateTimeImmutable("2026-01-15 03:17:00", new DateTimeZone("Asia/Jakarta"));
catatLog("mulai: hitung-pending.php", $waktu, $logFile);
catatLog("selesai: 2 pesanan pending", $waktu, $logFile);
echo file_get_contents($logFile);Output yang Dihasilkan:
[2026-01-15 03:17:00] mulai: hitung-pending.php
[2026-01-15 03:17:00] selesai: 2 pesanan pendingFlag FILE_APPEND membuat setiap baris menempel di akhir berkas, dan format [Y-m-d H:i:s] menjadikan setiap entri mudah disaring saat audit. Untuk tool yang lebih besar, arahkan pencatatan ini ke logger terstruktur seperti yang dibahas pada section Debugging dan Observability.
Job yang Terlewat #
Cron punya satu kelemahan struktural: ia hanya tahu waktu, bukan kehadiran mesin. Bila server mati pukul 02.00 dan hidup pukul 04.00, job pukul 03.17 tidak pernah berjalan — cron tidak “mengejar” yang terlewat. Untuk mesin yang sering mati-hidup seperti laptop, GNU anacron mengisi celah ini: ia menjalankan job harian/mingguan kapan pun mesin pertama kali menyala, lalu mencatat tanggal eksekusi terakhirnya. Perbandingan ketiganya:
| Aspek | cron | anacron | systemd timer |
|---|---|---|---|
| Pemicu | waktu persis | menyala setelah jadwal terlewat | waktu persis |
| Granularitas terkecil | menit | hari | detik |
| Mengejar job terlewat | tidak | ya | ya, dengan Persistent=true |
| Khas di | server | desktop dan laptop | server Linux modern |
Alternatif: systemd Timer #
Distribusi server modern biasanya memakai systemd, yang menawarkan timer sebagai pengganti cron dengan fitur mengejar jadwal terlewat. Sebuah timer dideklarasikan dalam berkas unit terpisah dari service-nya:
[Unit]
Description=Menjalankan laporan penjualan harian
[Timer]
OnCalendar=*-*-* 03:17:00
Persistent=true
[Install]
WantedBy=timers.targetSekali systemctl enable --now laporan.timer dijalankan, job dieksekusi setiap pukul 03.17 — dan berkat Persistent=true, bila mesin mati melewati jadwal itu, job dijalankan saat boot berikutnya. Format OnCalendar juga lebih mudah dibaca untuk jadwal rumit dibanding lima kolom cron, misalnya Mon..Fri 09:00 untuk hari kerja saja.
Menguji Logika Terjadwal #
Menunggu jam 03.17 untuk mengetahui apakah keputusan job benar adalah cara debugging yang sangat lambat. Pisahkan keputusan dari eksekusi: kapan job harus berjalan ditentukan fungsi kecil yang menerima waktu sebagai parameter, sehingga bisa diuji dengan waktu beku:
<?php
function shouldRun(DateTimeImmutable $now, ?string $lastRunDate): bool
{
if ($lastRunDate === null) {
return true; // belum pernah dijalankan sama sekali
}
return $now->format("Y-m-d") !== $lastRunDate;
}
$sekarang = new DateTimeImmutable("2026-01-15 08:00:00", new DateTimeZone("Asia/Jakarta"));
var_dump(shouldRun($sekarang, "2026-01-14")); // terakhir jalan kemarin
var_dump(shouldRun($sekarang, "2026-01-15")); // sudah jalan hari ini
var_dump(shouldRun($sekarang, null)); // belum pernah jalan
Output yang Dihasilkan:
bool(true)
bool(false)
bool(true)Pola “sekali per hari” di atas membandingkan tanggal hari ini dengan tanggal eksekusi terakhir — nilainya di job nyata dibaca dari berkas status kecil setelah job sukses. Dengan waktu sebagai parameter, seluruh kasus (belum pernah jalan, sudah jalan, terlewat sehari) diuji dalam hitungan milidetik tanpa mengubah jam sistem.
Ringkasan #
Cron menjalankan skrip PHP berdasarkan lima kolom waktu dengan environment yang minim, sehingga jalur absolut dan kemandirian skrip adalah syarat utama. Lock berbasis flock() mencegah instance bertumpuk, output dicatat ke log agar tidak hilang, anacron dan systemd timer menangani jadwal yang terlewat, dan logika keputusan diuji dengan waktu beku. Job terjadwal yang kita tulis ini sering kali menghabiskan waktunya mengolah berkas — memindahkan, memutar, dan membersihkan — persis materi chapter Filesystem Automation berikutnya.