Menambahkan data di akhir berkas — append — adalah pola yang sangat umum: file log yang bertambah setiap kali terjadi kejadian, counter kunjungan, hingga antrean tugas sederhana. Masalahnya, append justru menjadi lokasi klasik munculnya race condition: ketika dua proses menulis ke berkas yang sama di waktu yang hampir bersamaan, hasil akhirnya bisa rusak tanpa pesan error apa pun. Pada chapter ini kita akan mempelajari dua cara melakukan append, memahami mengapa penulisan bersamaan berbahaya, lalu menanganinya dengan mekanisme file locking melalui flock() dan flag LOCK_EX.

Menambahkan Data dengan Mode a #

Mode pembukaan 'a' pada fopen() dirancang khusus untuk append: isi lama dipertahankan, dan setiap fwrite() selalu menempelkan data di akhir berkas — bahkan bila Anda memindahkan penunjuk posisi dengan fseek(), penulisan tetap dialihkan ke ujung:

php
<?php
$berkas = __DIR__ . '/kunjungan.txt';
file_put_contents($berkas, "Pengunjung pertama datang\n");

$handle = fopen($berkas, 'a');
fwrite($handle, "Pengunjung kedua datang\n");
fwrite($handle, "Pengunjung ketiga datang\n");
fclose($handle);

echo file_get_contents($berkas);

unlink($berkas);

Output yang Dihasilkan:

text
Pengunjung pertama datang
Pengunjung kedua datang
Pengunjung ketiga datang

Berkas hasilnya memuat ketiga baris secara berurutan: baris pertama berasal dari file_put_contents() yang menulis dari awal, sedangkan dua fwrite() berikutnya menempel di akhir karena mode 'a'. Bandingkan dengan mode 'w' dari chapter sebelumnya yang akan menghapus Pengunjung pertama datang sebelum menulis ulang. Mode 'a' juga otomatis membuat berkas baru bila belum ada, sehingga cocok untuk berkas log yang dibuat saat kejadian pertama terjadi.

Flag FILE_APPEND pada file_put_contents #

Untuk append satu string utuh, file_put_contents() menyediakan flag FILE_APPEND sehingga Anda tidak perlu membuka handle sendiri:

php
<?php
$berkas = __DIR__ . '/log-aktivitas.txt';

file_put_contents($berkas, "Program dimulai\n");
file_put_contents($berkas, "Data diproses\n", FILE_APPEND);
file_put_contents($berkas, "Program selesai\n", FILE_APPEND);

echo file_get_contents($berkas);

unlink($berkas);

Output yang Dihasilkan:

text
Program dimulai
Data diproses
Program selesai

Panggilan pertama tanpa flag menimpa (berkas masih kosong, jadi efeknya sama dengan membuat baru), sedangkan dua panggilan berikutnya dengan FILE_APPEND menambahkan di akhir. Flag ini adalah parameter ketiga file_put_contents() dan dapat digabungkan dengan flag lain memakai operator bitwise OR (|) — kombinasi yang paling sering dipakai di dunia nyata adalah FILE_APPEND | LOCK_EX, yang akan kita temui lagi di bagian akhir chapter.

Bahaya Penulisan Bersamaan #

Untuk memahami mengapa locking diperlukan, bayangkan dua proses PHP — misalnya dua request web yang datang nyaris bersamaan — menulis ke berkas log yang sama. Ilustrasi berikut menunjukkan apa yang bisa terjadi:

text
Tanpa locking:
  Proses A menulis: "pengguna=1\n"
  Proses B menulis: "pengguna=2\n"
  Keduanya menulis pada posisi yang sama -> isi saling menimpa,
  atau baris keduanya tercampur menjadi satu baris rusak.

Dengan LOCK_EX:
  Proses A: kunci -> menulis -> buka kunci
  Proses B: menunggu -> kunci -> menulis -> buka kunci
  Setiap penulisan utuh; isi akhir selalu konsisten.

Skenario yang lebih halus lagi adalah pola baca–ubah–tulis: sebuah counter harus dibaca dulu, dinaikkan, lalu ditulis kembali. Jika proses B membaca nilai counter sebelum proses A selesai menulis nilai barunya, kenaikan yang dilakukan A akan hilang ditimpa tulisan B. Sistem operasi tidak melarang dua proses membuka berkas yang sama secara bersamaan — penjadwalan penulisan sepenuhnya menjadi tanggung jawab kita, dan itulah pekerjaan flock().

Mengunci dengan flock: Pola Baca-Ubah-Tulis #

Fungsi flock() memasang kunci pada handle berkas yang sudah dibuka. Contoh berikut menaikkan counter pengunjung dengan aman di bawah kunci eksklusif LOCK_EX — hanya satu proses pada satu waktu yang boleh memegang kunci jenis ini:

php
<?php
$berkas = __DIR__ . '/pengunjung.txt';
file_put_contents($berkas, '41');

$handle = fopen($berkas, 'r+');

if (flock($handle, LOCK_EX)) {
    $jumlah = (int) fgets($handle);
    $jumlah++;

    ftruncate($handle, 0);
    rewind($handle);
    fwrite($handle, (string) $jumlah);
    fflush($handle);

    flock($handle, LOCK_UN);
}
fclose($handle);

echo "Jumlah pengunjung sekarang: " . file_get_contents($berkas) . "\n";

unlink($berkas);

Output yang Dihasilkan:

text
Jumlah pengunjung sekarang: 42

Mari kita bedah bagian inti contoh ini:

  1. Baris 4 (fopen($berkas, 'r+')): mode r+ membuka berkas untuk dibaca dan ditulis tanpa menghapus isi — syarat wajib pola baca-ubah-tulis.
  2. Baris 6 (flock($handle, LOCK_EX)): mengajukan kunci eksklusif; selama kunci ini dipegang, proses lain yang juga mengajukan kunci akan menunggu hingga kunci dilepas.
  3. Baris 7-8 (fgets lalu $jumlah++): membaca nilai lama dari berkas dan menaikkannya di memori — wilayah kritis dimulai dari pembacaan ini.
  4. Baris 10-11 (ftruncate dan rewind): mengosongkan berkas lalu memindahkan penunjuk ke awal; langkah ini penting karena nilai baru (42) bisa saja lebih pendek dari nilai lama, dan sisa karakter lama akan menempel bila berkas tidak dikosongkan.
  5. Baris 12-13 (fwrite dan fflush): menulis nilai baru dan memastikannya diteruskan ke sistem operasi selama kunci masih dipegang.
  6. Baris 15 (flock($handle, LOCK_UN)): melepas kunci agar proses lain boleh mengambilnya; fclose() pada baris berikutnya sebenarnya juga otomatis melepas kunci, tetapi melepaskannya secara eksplisit membuat niat kode lebih jelas.

Kunci Bersama vs Kunci Eksklusif #

flock() mengenal tiga operasi utama. LOCK_EX (exclusive) hanya boleh dipegang satu proses pada satu waktu dan wajib dipakai saat menulis; LOCK_SH (shared) boleh dipegang banyak proses sekaligus sehingga cocok untuk membaca berkas yang sedang bisa berubah — pembaca saling bertoleransi, tetapi penulis tetap mendapatkan jalur eksklusif. Terakhir, LOCK_UN melepas kunci yang sedang dipegang:

OperasiJenisSiapa yang boleh memegang bersamaanDipakai untuk
LOCK_SHkunci bersamabanyak pembacamembaca data yang bisa berubah
LOCK_EXkunci eksklusifsatu proses sajamenulis / baca-ubah-tulis
LOCK_UNpelepasan kunci—setelah selesai memakai berkas

Pola pembaca dengan kunci bersama tampak seperti ini:

php
<?php
$berkas = '/var/log/aplikasi.log';
$handle = fopen($berkas, 'r');

if (flock($handle, LOCK_SH)) {
    // beberapa proses boleh membaca bersamaan di area ini
    while (($baris = fgets($handle)) !== false) {
        // memproses baris log tanpa takut ditimpa penulis lain
    }
    flock($handle, LOCK_UN);
}

fclose($handle);

Selama pembaca memegang LOCK_SH, pengguna LOCK_EX yang ingin menulis akan menunggu — dan sebaliknya, selama penulis memegang LOCK_EX, pembaca baru ikut menunggu. Hasilnya, pembaca tidak pernah melihat setengah baris yang sedang ditulis.

Penulisan Atomik dengan LOCK_EX pada file_put_contents #

file_put_contents() menerima LOCK_EX sebagai bagian dari parameter ketiganya: kunci eksklusif otomatis dipasang selama proses penulisan berlangsung, lalu dilepas begitu selesai. Dikombinasikan dengan FILE_APPEND, inilah resep standar menulis baris log yang aman dari banyak proses:

php
<?php
$berkas = __DIR__ . '/saldo.txt';
file_put_contents($berkas, '100000');

// penulisan atomik: kunci eksklusif dipegang selama data ditulis
file_put_contents($berkas, '250000', LOCK_EX);

echo "Saldo terbaru: " . file_get_contents($berkas) . "\n";

$logFile = __DIR__ . '/kejadian.txt';
file_put_contents($logFile, "Aplikasi dimulai\n", FILE_APPEND | LOCK_EX);
file_put_contents($logFile, "Aplikasi selesai\n", FILE_APPEND | LOCK_EX);
echo file_get_contents($logFile);

unlink($berkas);
unlink($logFile);

Output yang Dihasilkan:

text
Saldo terbaru: 250000
Aplikasi dimulai
Aplikasi selesai

Perlu dicatat batas kemampuannya: flag LOCK_EX pada file_put_contents() hanya mengunci selama satu panggilan penulisan itu sendiri. Pola baca-ubah-tulis dari bagian sebelumnya tetap harus memakai fopen() + flock() manual, karena di sana jendela waktu antara pembacaan dan penulisan kembali harus berada di bawah kunci yang sama. Gunakan flag ini untuk penulisan utuh-satu-string, dan flock() manual untuk logika multi-langkah.

Ringkasan #

Append bisa dilakukan lewat mode 'a' pada fopen() atau flag FILE_APPEND pada file_put_contents(), dan keduanya aman dipakai lintas proses selama diiringi LOCK_EX — baik secara manual melalui flock() untuk pola baca-ubah-tulis, maupun otomatis lewat flag pada file_put_contents() untuk penulisan utuh. Kunci bersama LOCK_SH melindungi pembaca dari penulis yang sedang bekerja, dan ingat bahwa seluruh mekanisme ini bersifat advisory: kuncinya hanya berharga bila semua penulis menghormatinya. Kita sementara menyimpan topik locking ini, karena ada satu aktor lain yang ikut menentukan siapa-boleh-apa terhadap berkas: permission. Chapter File Permissions membaca dan mengubah izin baca, tulis, dan eksekusi dari sisi PHP.

Referensi #