Program pada Bagian 2 mampu mengelola tugas, tetapi ingatannya hilang begitu proses berakhir — variabel memori ikut terbebaskan. Pada bagian ini kita memberi program ingatan permanen: setiap perintah memuat daftar tugas dari berkas tugas.json, mengubahnya di memori, lalu menuliskan seluruh hasilnya kembali. JSON kita pilih karena formatnya dapat dibaca manusia, didukung fungsi bawaan PHP, dan mudah dipertukarkan dengan bahasa pemrograman lain.
Mengubah Array Menjadi JSON dan Sebaliknya #
Sepasang fungsi json_encode() dan json_decode() menjadi jembatan dua arah antara array PHP dan teks JSON. Fungsi json_encode() menerima nilai PHP lalu menghasilkan string JSON, sedangkan json_decode() dengan argumen kedua true menghasilkan array asosiatif, bukan object. Mari kita buktikan bolak-baliknya:
<?php
$tugas = [
['id' => 1, 'judul' => 'Belajar PHP dasar', 'selesai' => false],
];
$json = json_encode($tugas, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
echo $json . "\n";
$hasil = json_decode($json, true);
printf("Round-trip identik: %s\n", $hasil === $tugas ? 'ya' : 'tidak');Output yang Dihasilkan:
[
{
"id": 1,
"judul": "Belajar PHP dasar",
"selesai": false
}
]
Round-trip identik: yaPerbandingan $hasil === $tugas menghasilkan ya karena urutan kunci dan nilai tersimpan utuh melalui JSON — inilah yang membuat format ini aman dipakai sebagai penyimpanan tanpa kehilangan struktur.
Flag yang Membuat JSON Ramah Dibaca #
Secara bawaan json_encode() menghasilkan JSON satu baris dan mengubah karakter non-ASCII menjadi escape sequence \uXXXX. Dua flag kita padukan untuk memperbaikinya:
<?php
$tugas = ['id' => 1, 'judul' => 'Minum kopi ☕', 'selesai' => false];
echo json_encode($tugas) . "\n";
echo json_encode($tugas, JSON_UNESCAPED_UNICODE) . "\n";Output yang Dihasilkan:
{"id":1,"judul":"Minum kopi \u2615","selesai":false}
{"id":1,"judul":"Minum kopi ☕","selesai":false}Baris pertama memperlihatkan ☕ berubah menjadi \u2615 — sah secara JSON, tetapi menyulitkan penyuntingan manual. Flag JSON_UNESCAPED_UNICODE menjaga karakter UTF-8 tetap tertulis asli, sementara JSON_PRETTY_PRINT (dipakai pada blok sebelumnya) menata setiap field di baris tersendiri dengan indentasi. Gabungan keduanya menghasilkan berkas yang enak dibaca pemelihara program, bukan hanya mesin.
storage.php: muatTugas dan simpanTugas #
Seluruh urusan berkas kita pusatkan ke storage.php — hanya dua fungsi, tanpa variabel global. Fungsi muatTugas() memperlakukan berkas yang belum ada sebagai daftar kosong, sehingga program bisa langsung dipakai sebelum tugas.json pernah dibuat:
function muatTugas(string $berkas = BERKAS_TUGAS): array
{
if (!is_file($berkas)) {
return [];
}
$isi = file_get_contents($berkas);
if ($isi === false) {
return [];
}
$tugas = json_decode($isi, true);
return is_array($tugas) ? $tugas : [];
}Sementara itu simpanTugas() menangkap tiga urusan sekaligus: flag JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE untuk keterbacaan, penambahan baris baru di akhir berkas sesuai konvensi berkas teks, dan LOCK_EX yang mengunci berkas selama penulitian agar dua proses yang menulis bersamaan tidak saling menimpa isi:
function simpanTugas(array $tugas, string $berkas = BERKAS_TUGAS): bool
{
$json = json_encode($tugas, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
if ($json === false) {
return false;
}
return file_put_contents($berkas, $json . "\n", LOCK_EX) !== false;
}Kedua fungsi menerima jalur berkas sebagai parameter dengan nilai bawaan konstanta BERKAS_TUGAS (yaitu tugas.json di sebelah storage.php). Mari kita lihat perilakunya pada berkas yang belum ada:
<?php
require_once __DIR__ . '/storage.php';
$berkas = sys_get_temp_dir() . '/tugas-demo-bagian3.json';
if (is_file($berkas)) {
unlink($berkas);
}
var_dump(muatTugas($berkas));
$tugas = [
['id' => 1, 'judul' => 'Belajar PHP dasar', 'selesai' => false],
];
simpanTugas($tugas, $berkas);
echo file_get_contents($berkas);
var_dump(muatTugas($berkas) === $tugas);
unlink($berkas);Output yang Dihasilkan:
array(0) {
}
[
{
"id": 1,
"judul": "Belajar PHP dasar",
"selesai": false
}
]
bool(true)Blok di atas memakai direktori temporari sebagai lokasi demo agar tidak menyeret berkas contoh — perhatikan bahwa simpanTugas() berhasil membuat berkas baru pada penyimpanan pertama tanpa langkah persiapan apa pun.
Mengalirkan Penyimpanan ke Perintah CLI #
Fungsi jalankan() kini menerima jalur berkas sebagai parameter kedua. Setiap fungsi perintah mengikuti pola tiga langkah yang sama: muat dari berkas, ubah di memori, simpan seluruh hasilnya. Mari kita simulasikan empat pemanggilan CLI terpisah terhadap satu berkas yang sama:
<?php
require_once __DIR__ . '/main.php';
$berkas = sys_get_temp_dir() . '/tugas-cli-demo-bagian3.json';
if (is_file($berkas)) {
unlink($berkas);
}
jalankan(['main.php', 'tambah', 'Belajar PHP dasar'], $berkas);
jalankan(['main.php', 'tambah', 'Belajar OOP'], $berkas);
jalankan(['main.php', 'selesai', '1'], $berkas);
echo "--- eksekusi berikutnya ---\n";
jalankan(['main.php', 'list'], $berkas);
unlink($berkas);Output yang Dihasilkan:
Tugas ditambahkan: [1] Belajar PHP dasar
Daftar Tugas:
1. [ ] Belajar PHP dasar
Tugas ditambahkan: [2] Belajar OOP
Daftar Tugas:
1. [ ] Belajar PHP dasar
2. [ ] Belajar OOP
Tugas ditandai selesai: [1] Belajar PHP dasar
Daftar Tugas:
1. [x] Belajar PHP dasar
2. [ ] Belajar OOP
--- eksekusi berikutnya ---
Daftar Tugas:
1. [x] Belajar PHP dasar
2. [ ] Belajar OOPInilah momen kunci bagian ini: pemanggilan list terakhir berdiri sendiri, persis seperti eksekusi kedua di Bagian 2 yang selalu kosong — namun sekarang kedua tugas tetap ada karena setiap jalankan() membaca kembali isi berkas.
Menjalankan Program #
Kali ini kita jalankan programnya sungguhan sebagai proses terpisah dan amati datanya bertahan:
$ php main.php tambah "Belajar PHP dasar"
$ php main.php tambah "Belajar OOP"Tugas ditambahkan: [1] Belajar PHP dasar
Daftar Tugas:
1. [ ] Belajar PHP dasar
Tugas ditambahkan: [2] Belajar OOP
Daftar Tugas:
1. [ ] Belajar PHP dasar
2. [ ] Belajar OOP$ php main.php selesai 1Tugas ditandai selesai: [1] Belajar PHP dasar
Daftar Tugas:
1. [x] Belajar PHP dasar
2. [ ] Belajar OOP$ php main.php listDaftar Tugas:
1. [x] Belajar PHP dasar
2. [ ] Belajar OOPIsi tugas.json kini bisa diperiksa langsung — berkat dua flag tadi, strukturnya terbaca tanpa alat bantu:
$ cat tugas.json[
{
"id": 1,
"judul": "Belajar PHP dasar",
"selesai": true
},
{
"id": 2,
"judul": "Belajar OOP",
"selesai": false
}
]Apa yang Berubah dari Bagian 2 #
Fungsi jalankan() tidak lagi membuat $tugas = []; setiap perintah kini memanggil muatTugas() di awal dan simpanTugas() di akhir. Seluruh fungsi operasi array dari Bagian 2 tidak berubah sama sekali — pemisahan operasi murni dari I/O terbayar: kita hanya menambah lapisan penyimpanan tanpa menyentuh logika bisnis. Keterbatasan utama yang tersisa bukan lagi memori, melainkan ketangguhan: apa yang terjadi bila judul kosong, ID bukan angka, atau berkas JSON rusak? Bagian 4 menjawabnya.
Ringkasan #
Persistensi sederhana cukup dibangun dari dua fungsi: muatTugas() yang toleran terhadap berkas yang belum ada, dan simpanTugas() yang menulis JSON terformat dengan penguncian LOCK_EX. Pola muat-ubah-simpan membuat setiap eksekusi mandiri dan konsisten dengan isi berkas terakhir. Dengan data kini hidup di disk, kita bisa fokus memperkokoh program terhadap masukan yang buruk pada Bagian 4.
Referensi #
- file_put_contents
— menulis berkas sekaligus flag
LOCK_EX. - json_encode — daftar lengkap flag format keluaran JSON.
- flock
— mekanisme penguncian berkas yang mendasari
LOCK_EX.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
main.php
Unduh berkas<?php
declare(strict_types=1);
/**
* Pengelola Daftar Tugas — Bagian 3: Penyimpanan JSON.
*
* Daftar tugas kini bertahan lintas eksekusi: setiap perintah
* memuat tugas dari tugas.json, mengubahnya di memori, lalu
* menyimpan seluruh hasilnya kembali.
*/
require_once __DIR__ . '/storage.php';
const KELUAR_OK = 0;
const KELUAR_GAGAL = 1;
function perintahBantuan(): int
{
echo <<<TEKS
Pengelola Daftar Tugas — manajemen daftar tugas pribadi
Penggunaan:
php main.php Tampilkan bantuan ini
php main.php list Tampilkan daftar tugas
php main.php tambah <judul> Tambah tugas baru
php main.php selesai <id> Tandai tugas sebagai selesai
php main.php hapus <id> Hapus tugas
Contoh:
php main.php tambah "Belajar PHP dasar"
php main.php selesai 1
php main.php hapus 2
TEKS;
return KELUAR_OK;
}
// ---------------------------------------------------------------------------
// Operasi murni atas array tugas — tanpa I/O, mudah diuji terpisah.
// ---------------------------------------------------------------------------
/** Bentuk satu tugas baru dengan status belum selesai. */
function tugasBaru(int $id, string $judul): array
{
return ['id' => $id, 'judul' => $judul, 'selesai' => false];
}
/** Hitung ID untuk tugas berikutnya: selalu terbesar + 1. */
function idBerikutnya(array $tugas): int
{
$terbesar = 0;
foreach ($tugas as $t) {
$terbesar = max($terbesar, $t['id']);
}
return $terbesar + 1;
}
/** Kembalikan daftar tugas baru yang sudah berisi tugas tambahan. */
function tambahTugas(array $tugas, string $judul): array
{
$tugas[] = tugasBaru(idBerikutnya($tugas), $judul);
return $tugas;
}
/** Cari satu tugas berdasarkan ID; null bila tidak ada. */
function cariTugas(array $tugas, int $id): ?array
{
foreach ($tugas as $t) {
if ($t['id'] === $id) {
return $t;
}
}
return null;
}
/** Kembalikan daftar baru dengan status selesai true; null bila ID tidak ada. */
function tandaiSelesai(array $tugas, int $id): ?array
{
foreach ($tugas as $i => $t) {
if ($t['id'] === $id) {
$tugas[$i]['selesai'] = true;
return $tugas;
}
}
return null;
}
/** Kembalikan daftar baru tanpa tugas tersebut; null bila ID tidak ada. */
function hapusTugas(array $tugas, int $id): ?array
{
foreach ($tugas as $i => $t) {
if ($t['id'] === $id) {
array_splice($tugas, $i, 1);
return $tugas;
}
}
return null;
}
/** Cetak daftar tugas ke layar dengan format centang. */
function tampilkanTugas(array $tugas): void
{
if ($tugas === []) {
echo "Belum ada tugas.\n";
return;
}
echo "Daftar Tugas:\n";
foreach ($tugas as $t) {
printf(" %d. [%s] %s\n", $t['id'], $t['selesai'] ? 'x' : ' ', $t['judul']);
}
}
// ---------------------------------------------------------------------------
// Perintah CLI — tiap fungsi memuat, mengubah, lalu menyimpan.
// ---------------------------------------------------------------------------
function perintahList(string $berkas): int
{
tampilkanTugas(muatTugas($berkas));
return KELUAR_OK;
}
function perintahTambah(array $argv, string $berkas): int
{
$judul = $argv[2] ?? null;
if ($judul === null) {
printf("Error: judul tugas wajib diisi. Contoh: php main.php tambah \"Belajar PHP\"\n");
return KELUAR_GAGAL;
}
$tugas = muatTugas($berkas);
$tugas = tambahTugas($tugas, $judul);
if (!simpanTugas($tugas, $berkas)) {
echo "Error: gagal menyimpan berkas tugas.\n";
return KELUAR_GAGAL;
}
$baru = $tugas[array_key_last($tugas)];
printf("Tugas ditambahkan: [%d] %s\n\n", $baru['id'], $baru['judul']);
tampilkanTugas($tugas);
return KELUAR_OK;
}
function perintahSelesai(array $argv, string $berkas): int
{
$id = $argv[2] ?? null;
if ($id === null) {
printf("Error: ID tugas wajib diisi. Contoh: php main.php selesai 1\n");
return KELUAR_GAGAL;
}
$tugas = muatTugas($berkas);
$hasil = tandaiSelesai($tugas, (int) $id);
if ($hasil === null) {
printf("Error: tugas dengan ID %d tidak ditemukan.\n", (int) $id);
return KELUAR_GAGAL;
}
if (!simpanTugas($hasil, $berkas)) {
echo "Error: gagal menyimpan berkas tugas.\n";
return KELUAR_GAGAL;
}
$terkait = cariTugas($hasil, (int) $id);
printf("Tugas ditandai selesai: [%d] %s\n\n", $terkait['id'], $terkait['judul']);
tampilkanTugas($hasil);
return KELUAR_OK;
}
function perintahHapus(array $argv, string $berkas): int
{
$id = $argv[2] ?? null;
if ($id === null) {
printf("Error: ID tugas wajib diisi. Contoh: php main.php hapus 1\n");
return KELUAR_GAGAL;
}
$tugas = muatTugas($berkas);
$terhapus = cariTugas($tugas, (int) $id);
$hasil = hapusTugas($tugas, (int) $id);
if ($hasil === null) {
printf("Error: tugas dengan ID %d tidak ditemukan.\n", (int) $id);
return KELUAR_GAGAL;
}
if (!simpanTugas($hasil, $berkas)) {
echo "Error: gagal menyimpan berkas tugas.\n";
return KELUAR_GAGAL;
}
printf("Tugas dihapus: [%d] %s\n\n", $terhapus['id'], $terhapus['judul']);
tampilkanTugas($hasil);
return KELUAR_OK;
}
function perintahTidakDikenal(string $perintah): int
{
printf("Error: perintah tidak dikenal: %s\n\n", $perintah);
perintahBantuan();
return KELUAR_GAGAL;
}
/**
* Titik masuk program. Jalur berkas penyimpanan bisa diganti —
* default-nya tugas.json di sebelah berkas ini, namun parameter
* memungkinkan pengujian memakai berkas sementara.
*/
function jalankan(array $argv, string $berkas = BERKAS_TUGAS): int
{
$perintah = $argv[1] ?? '';
if ($perintah === '') {
return perintahBantuan();
}
return match ($perintah) {
'list' => perintahList($berkas),
'tambah' => perintahTambah($argv, $berkas),
'selesai' => perintahSelesai($argv, $berkas),
'hapus' => perintahHapus($argv, $berkas),
default => perintahTidakDikenal($perintah),
};
}
// Jalankan hanya bila dieksekusi langsung sebagai skrip CLI,
// bukan saat di-include dari berkas lain (blok contoh, pengujian).
if (PHP_SAPI === 'cli' && isset($argv[0]) && realpath($argv[0]) === __FILE__) {
exit(jalankan($argv));
}
storage.php
Unduh berkas<?php
declare(strict_types=1);
/**
* Penyimpanan daftar tugas dalam berkas JSON — Bagian 3.
*
* Dua fungsi ini adalah satu-satunya gerbang I/O berkas pada
* proyek: seluruh perintah CLI bekerja lewat muatTugas() dan
* simpanTugas(). Validasi ketat dikerjakan pada Bagian 4.
*/
const BERKAS_TUGAS = __DIR__ . '/tugas.json';
/**
* Muat seluruh tugas dari berkas JSON.
*
* Berkas yang belum ada diperlakukan sebagai daftar kosong;
* berkas dibuat otomatis pada penyimpanan pertama.
*/
function muatTugas(string $berkas = BERKAS_TUGAS): array
{
if (!is_file($berkas)) {
return [];
}
$isi = file_get_contents($berkas);
if ($isi === false) {
return [];
}
$tugas = json_decode($isi, true);
return is_array($tugas) ? $tugas : [];
}
/**
* Simpan seluruh tugas ke berkas JSON.
*
* JSON_PRETTY_PRINT membuat isi berkas mudah dibaca dan disunting
* manual; JSON_UNESCAPED_UNICODE menjaga teks non-ASCII tetap utuh.
* Flag LOCK_EX mengunci berkas selama penulisan agar isi tidak
* tertimpa proses lain yang menulis bersamaan.
*/
function simpanTugas(array $tugas, string $berkas = BERKAS_TUGAS): bool
{
$json = json_encode($tugas, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
if ($json === false) {
return false;
}
return file_put_contents($berkas, $json . "\n", LOCK_EX) !== false;
}