Saya membuat lima scheduler PHP CLI untuk JICT, MAL, NPCT1, TPKT3, dan KOJA. Setiap scheduler membaca daftar container dari SQL Server, mengambil data tracking dari sumber terminal, lalu menyimpan hasilnya ke tb_data_terminal. Tantangannya adalah setiap sumber memakai endpoint, autentikasi, dan format respons yang berbeda.
Masalah yang diselesaikan
Input scheduler berasal dari EXEC [dash_terminal_to] ?, dengan kode terminal sebagai parameter. Outputnya adalah satu record tracking yang dapat di-insert atau diperbarui pada tb_data_terminal berdasarkan kombinasi nomor container, BL, dan terminal.
Sumber eksternal tidak seragam. JICT mengembalikan JSON dan memakai cookie, KOJA mengembalikan HTML, NPCT1 membuat session dan CSRF token, TPKT3 login untuk memperoleh Bearer token, sedangkan MAL memerlukan pemilihan hasil berdasarkan kapal dan voyage ketika satu container memiliki lebih dari satu hasil.
Runtime server menggunakan PHP 7.4, sehingga implementasi tidak boleh memakai sintaks PHP 8. Scheduler juga perlu menangani container kosong, data yang tidak ditemukan, tanggal kosong, respons yang berubah, dan proses cron yang saling tumpang tindih.
Keputusan teknis
- PHP native sebagai entry point CLI agar scheduler dapat langsung dipanggil oleh cron tanpa framework atau Composer.
curluntuk request HTTP,DOMDocumentdanDOMXPathuntuk respons HTML, sertaPDOdenganpdo_sqlsrvuntuk SQL Server.- Satu folder scheduler per terminal agar request, session, parser, dan mapping vendor tidak bercampur.
.envuntuk konfigurasi koneksi dan endpoint, log harian untuk hasil batch, serta lock file untuk mencegah eksekusi paralel.
Alur data yang dipakai adalah stored procedure -> adapter terminal -> normalisasi -> transaksi SQL Server -> tb_data_terminal. Setiap adapter mengisolasi perbedaan vendor; bagian pembacaan sumber, logging, transaksi, dan upsert dipertahankan dengan pola yang sama.
Implementasi
Mengambil dan memvalidasi respons HTML KOJA
KOJA tidak menyediakan JSON, sehingga parser membaca tabel HTML menggunakan DOMDocument. Parser tidak mengandalkan urutan kolom; ia mengumpulkan pasangan label dan nilai, memeriksa label wajib, lalu memastikan container pada respons sama dengan container yang diminta.
$requiredLabels = ['container id', 'arrival', 'out time', 'in time / stack cy'];
foreach ($requiredLabels as $label) {
if (!array_key_exists($label, $fields)) {
throw new RuntimeException("Field {$label} tidak ditemukan pada respons KOJA.");
}
}
$responseContainer = strtoupper(trim($fields['container id']));
if ($responseContainer !== strtoupper(trim($requestedContainer))) {
throw new RuntimeException(
"Container ID respons KOJA tidak sesuai: {$responseContainer}"
);
}Jika halaman masih memuat form pencarian tetapi tidak memiliki tabel hasil, parseKojaResponse() mengembalikan null; batch mencatatnya sebagai not_found dan tidak menyimpan record. Struktur HTML yang tidak dikenali tetap dianggap error agar perubahan halaman tidak berubah menjadi data kosong secara diam-diam.
Request KOJA memakai POST dengan application/x-www-form-urlencoded. Batas koneksi dan respons masing-masing dipasang ke 10 dan 30 detik, kemudian delay antarrequest dibaca dari konfigurasi.
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query([
'CNTR_ID' => $container,
'submit' => 'Show Detail',
]),
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_TIMEOUT => 30,Menyimpan data secara idempoten
Setiap scheduler menyiapkan statement insert, pemeriksaan record, dan update. Insert mengisi created_at dari SQL Server, sedangkan update hanya mengisi updated_at; key pencarian tetap menangani bl_no atau terminal yang bernilai NULL.
$insertStatement = $pdo->prepare(
'INSERT INTO [tb_data_terminal] '
. '([container_no], [bl_no], [terminal], [gate_in_terminal], [gate_out_terminal], '
. '[discharge_date], [arrive_date], [stack_date], [vessel], [voyage], [created_at], [updated_at]) '
. 'VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, GETDATE(), NULL)'
);
$updateStatement = $pdo->prepare(
'UPDATE [tb_data_terminal] SET '
. '[gate_in_terminal] = ?, [gate_out_terminal] = ?, [discharge_date] = ?, '
. '[arrive_date] = ?, [stack_date] = ?, [vessel] = ?, [voyage] = ?, [updated_at] = GETDATE() '
. 'WHERE [container_no] = ? '
. 'AND (([bl_no] = ?) OR ([bl_no] IS NULL AND ? IS NULL)) '
. 'AND (([terminal] = ?) OR ([terminal] IS NULL AND ? IS NULL))'
);Penyimpanan per container dibungkus transaksi. Jika insert atau update gagal, transaksi di-rollback sebelum error dicatat ke log; TPKT3 memakai COALESCE() pada update untuk mempertahankan tanggal yang sudah tersimpan ketika aktivitas tidak ada pada respons terbaru.
Menjaga batch aman untuk cron
Setiap run.php membuat lock sebelum menghubungi endpoint. Jika proses sebelumnya masih berjalan, scheduler selesai tanpa memproses batch kedua.
$lockHandle = fopen($logDirectory . '/scheduler.lock', 'c');
if ($lockHandle === false || !flock($lockHandle, LOCK_EX | LOCK_NB)) {
fwrite(STDERR, "Scheduler KOJA masih berjalan. Eksekusi baru dibatalkan.\n");
exit(0);
}Batch mencatat jumlah source, insert, update, not_found, gagal, dan data yang dilewati. Pada KOJA, tiga kegagalan berturut-turut menghentikan proses untuk membatasi request ketika koneksi atau struktur respons bermasalah.
Validasi sintaks dapat dilakukan tanpa menulis ke database dengan php -l config.php dan php -l run.php. Untuk menjalankan scheduler, server memerlukan PHP CLI 7.4, ekstensi curl, pdo_sqlsrv, sqlsrv, serta dom pada scheduler yang memproses HTML.