Tool CLI yang dipakai di lebih dari satu tempat hampir pasti membawa pengaturan yang berubah mengikuti lingkungan: alamat database, ukuran batch, batas waktu, dan mode verbose. Jika pengaturan tersebut ditempel sebagai konstanta di dalam kode, setiap pindah mesin berarti mengedit kode — dan pengguna tool Anda mungkin tidak boleh menyentuh kode sama sekali. Berkas konfigurasi memisahkan “apa yang program lakukan” dari “bagaimana program dikonfigurasi”, sehingga perilaku bisa diubah tanpa satu pun perubahan kode. Pada chapter ini kita membangun pola konfigurasi yang matang untuk tool CLI: memilih format, memuat dengan nilai default, mencari berkas di lokasi baku, menimpa nilai lewat environment variable, memvalidasi hasil akhir, dan menangani rahasia dengan aman.

Berkas konfigurasi melengkapi — bukan menggantikan — command line argument yang sudah kita bahas di chapter Command Line Arguments : argument cocok untuk nilai yang berubah setiap pemanggilan (nama berkas, flag --verbose), sedangkan berkas konfigurasi menyimpan pengaturan yang menetap untuk satu lingkungan.

Format Konfigurasi yang Umum #

Tiga format paling sering dijumpai pada tool PHP, masing-masing dengan karakter yang berbeda:

FormatCara memuatKelebihanKekurangan
PHP array (return)require $berkassintaks PHP penuh, cepat, di-cache opcachehanya terbaca oleh PHP
JSONjson_decode(..., true)lintas bahasa, mudah dipertukarkantanpa komentar, tipe terbatas
INIparse_ini_string() / parse_ini_file()ringkas, lazim di dunia Unixaturan tipe dan kutip perlu hati-hati

Untuk tool internal tim PHP, format PHP array adalah pilihan paling natural: ia mendukung konstanta, ekspresi, dan array bersarang tanpa lapisan parsing tambahan. Format JSON menang saat berkas yang sama dikonsumsi sistem lain, dan INI cocok bila Anda ingin bergaya Unix. Format PHP array memakai pola berkas yang mengembalikan array:

php
<?php
$berkas = sys_get_temp_dir() . "/latihan-konfig.php";

file_put_contents($berkas, <<<'PHP'
<?php
return [
    "nama_aplikasi" => "impor-tugas",
    "batch_size" => 50,
    "koneksi" => [
        "host" => "127.0.0.1",
        "port" => 5432,
    ],
];
PHP);

$config = require $berkas;

echo $config["nama_aplikasi"], "\n";
echo "batch: ", $config["batch_size"], " baris per siklus\n";
echo "port  : ", $config["koneksi"]["port"], "\n";

unlink($berkas);

Output yang Dihasilkan:

text
impor-tugas
batch: 50 baris per siklus
port  : 5432

Mari kita bedah contoh di atas:

  1. file_put_contents(...): contoh menulis berkas konfigurasi ke direktori sementara agar bisa dijalankan tanpa menyentuh proyek — pada tool nyata, berkas seperti ini disimpan permanen di lokasi yang akan kita bahas pada bagian search path.
  2. Isi berkas: berkas hanya berisi <?php dan return [...] — tanpa echo, tanpa efek samping; memuatnya murni mengembalikan data.
  3. require $berkas: pernyataan require mengembalikan nilai dari pernyataan return berkas tersebut, sehingga array konfigurasi langsung terikat ke variabel $config.
  4. Akses bersarang: kunci port diambil melalui $config["koneksi"]["port"] karena struktur array bebas bersarang.

Memuat JSON dan INI #

JSON dimuat dengan json_decode() pada mode array asosiatif, sedangkan INI cukup diberikan ke parse_ini_string(). Perhatikan parameter keduanya pada contoh berikut:

php
<?php
$jsonConfig = '{"nama_aplikasi": "impor-tugas", "batch_size": 50, "verbose": true}';

$configJson = json_decode($jsonConfig, true);
var_dump($configJson["verbose"]);

$isiIni = <<<'INI'
[nama]
aplikasi = impor-tugas

[koneksi]
host = 127.0.0.1
port = 5432
timeout = 30
INI;
$configIni = parse_ini_string($isiIni, true, INI_SCANNER_TYPED);

echo $configJson["nama_aplikasi"], " → ", $configIni["koneksi"]["host"], ":", $configIni["koneksi"]["port"], "\n";

Output yang Dihasilkan:

text
bool(true)
impor-tugas → 127.0.0.1:5432

Parameter true pada json_decode() memaksa hasil berupa array, bukan object stdClass. Pada parse_ini_string(), parameter kedua true mengaktifkan bagian [koneksi] sehingga struktur hasilnya bersarang, dan konstanta INI_SCANNER_TYPED membuat port = 5432 dimuat sebagai integer 5432, bukan string "5432".

Pola Search Path #

Berkas konfigurasi yang baik tidak memaksa pengguna mengingat satu lokasi tetap. Tool Unix klasik mencari konfigurasinya berlapis: direktori kerja saat ini (paling spesifik, untuk proyek tertentu), direktori user (~/.config/nama-tool, konvensi XDG), lalu direktori sistem (/etc/nama-tool, untuk administrator). Pencarian berhenti pada berkas pertama yang ditemukan; jika tidak ada sama sekali, tool memakai default internal:

php
<?php
$base = sys_get_temp_dir() . "/latihan-searchpath";

$lapisan = [
    "kerja"  => $base . "/.impor-tugas",
    "user"   => $base . "/config/impor-tugas",
    "sistem" => $base . "/etc/impor-tugas",
];

foreach ($lapisan as $dir) {
    if (!is_dir($dir)) {
        mkdir($dir, 0777, true);
    }
}

file_put_contents($lapisan["kerja"] . "/konfig.ini", "batch_size = 25\n");
file_put_contents($lapisan["user"] . "/konfig.ini", "batch_size = 200\n");

function cariKonfig(array $urutanDir, string $namaBerkas): ?string
{
    foreach ($urutanDir as $dir) {
        $kandidat = $dir . "/" . $namaBerkas;
        if (is_file($kandidat)) {
            return $kandidat;
        }
    }

    return null;
}

$berkas = cariKonfig(array_values($lapisan), "konfig.ini");

if ($berkas === null) {
    echo "konfigurasi tidak ditemukan, pakai default\n";
} else {
    $namaLapis = array_search(dirname($berkas), $lapisan, true);
    $isi = parse_ini_string(file_get_contents($berkas), false, INI_SCANNER_TYPED);

    echo "ditemukan di lapis: $namaLapis\n";
    echo "batch_size: ", $isi["batch_size"], "\n";
}

Output yang Dihasilkan:

text
ditemukan di lapis: kerja
batch_size: 25

Simulasi di atas sengaja menyediakan konfigurasi di dua lapis sekaligus: lapis kerja berisi batch_size = 25 dan lapis user berisi batch_size = 200. Karena fungsi cariKonfig() menelusuri lapisan sesuai urutan array, berkas di lapis kerja ditemukan lebih dulu dan menang — nilai 25 yang terpakai, 200 di lapis user diabaikan. Urutan array $lapisan itulah kebijakan prioritasnya: paling spesifik ke paling umum.

Menyatukan Default dengan array_replace_recursive #

Berkas milik pengguna jarang lengkap — biasanya hanya berisi kunci yang diubah dari bawaan. Karena itu tool Anda sebaiknya menyimpan default di dalam kode, lalu menggabungkannya dengan berkas pengguna. Fungsi yang tepat untuk ini adalah array_replace_recursive():

php
<?php
$default = [
    "batch_size" => 100,
    "verbose" => false,
    "koneksi" => [
        "host" => "127.0.0.1",
        "port" => 5432,
    ],
];

$milikUser = [
    "batch_size" => 25,
    "koneksi" => [
        "host" => "db.internal.example.com",
    ],
];

$config = array_replace_recursive($default, $milikUser);

echo "batch_size: ", $config["batch_size"], "\n";
echo "host      : ", $config["koneksi"]["host"], "\n";
echo "port      : ", $config["koneksi"]["port"], " (mewarisi default)\n";

Output yang Dihasilkan:

text
batch_size: 25
host      : db.internal.example.com
port      : 5432 (mewarisi default)

Nilai pengguna menimpa default kunci demi kunci: batch_size menjadi 25 dan host menjadi db.internal.example.com. Yang terpenting justru port: berkas pengguna tidak menyebutnya sama sekali, dan array_replace_recursive() menggabungkan secara bersarang sehingga port dari default tetap hidup di dalam sub-array koneksi. Fungsi array_merge() tidak bisa dipakai di sini karena kunci string yang sama akan diganti utuh — seluruh sub-array koneksi milik default akan hilang bersama port-nya.

Menimpa Nilai lewat Environment Variable #

Berkas konfigurasi cocok untuk nilai yang menetap, tetapi pipeline CI, cron, dan skrip otomasi sering perlu mengubah satu nilai saja untuk satu pemanggilan tanpa menyentuh berkas. Environment variable mengisi celah itu, mengikuti praktik twelve-factor app. Pola bakunya: prefix nama tool + nama kunci, dibaca sebagai lapis penimpa terakhir:

php
<?php
$config = ["batch_size" => 100, "verbose" => false];

putenv("IMPOR_BATCH_SIZE=25");
putenv("IMPOR_VERBOSE=1");

$batch = getenv("IMPOR_BATCH_SIZE");
if ($batch !== false) {
    $config["batch_size"] = (int) $batch;
}

$verbose = getenv("IMPOR_VERBOSE");
if ($verbose !== false) {
    $config["verbose"] = filter_var($verbose, FILTER_VALIDATE_BOOL);
}

var_dump($config["batch_size"]);
var_dump($config["verbose"]);

Output yang Dihasilkan:

text
int(25)
bool(true)

Nilai environment selalu berupa string, sehingga konversi dilakukan sendiri: cast (int) untuk angka, dan filter_var() dengan FILTER_VALIDATE_BOOL untuk boolean (menerima "1", "true", "on", "yes" sebagai true). Cek !== false memastikan variabel yang memang tidak di-set tidak ikut menimpa — getenv() mengembalikan false saat variabel tidak ada, yang berbeda dari string kosong. Urutan prioritas lengkapnya menjadi: default di kode → berkas konfigurasi → environment variable → command line argument.

Memvalidasi Konfigurasi #

Konfigurasi yang salah harus membuat tool gagal di awal dengan pesan jelas, bukan berjalan aneh di tengah proses. Kumpulkan semua masalah dulu, laporkan sekaligus, lalu keluar dengan exit code bukan nol agar pipeline yang memanggil tool tahu ada masalah:

php
<?php
function validasiConfig(array $config): array
{
    $errors = [];

    if (!array_key_exists("batch_size", $config)) {
        $errors[] = "kunci 'batch_size' wajib ada";
    } elseif (!is_int($config["batch_size"]) || $config["batch_size"] < 1) {
        $errors[] = "'batch_size' harus berupa integer >= 1";
    }

    if (!array_key_exists("nama_aplikasi", $config)) {
        $errors[] = "kunci 'nama_aplikasi' wajib ada";
    } elseif (!is_string($config["nama_aplikasi"]) || $config["nama_aplikasi"] === "") {
        $errors[] = "'nama_aplikasi' harus berupa string tidak kosong";
    }

    return $errors;
}

$configGagal = ["nama_aplikasi" => "", "batch_size" => 0];
foreach (validasiConfig($configGagal) as $error) {
    echo "- $error\n";
}

echo validasiConfig(["nama_aplikasi" => "impor-tugas", "batch_size" => 50]) === []
    ? "config valid\n"
    : "config tidak valid\n";

Output yang Dihasilkan:

text
- 'batch_size' harus berupa integer >= 1
- 'nama_aplikasi' harus berupa string tidak kosong
config valid

Fungsi validasiConfig() memisahkan dua jenis masalah: kunci yang hilang (cabang if pertama) dan kunci yang ada tapi bernilai salah (cabang elseif). Dengan mengumpulkan ke array $errors, pengguna melihat seluruh kesalahan sekaligus alih-alih memperbaiki satu lalu menemukan error berikutnya pada pemanggilan berikutnya. Pada tool nyata, loop pelaporan ini berakhir dengan exit(1) setelah seluruh pesan dicetak.

Rahasia Tetap di Environment #

Satu jenis nilai tidak boleh pernah hidup di berkas konfigurasi yang masuk repositori: rahasia. Kata sandi database dan API key yang ter-commit akan bocor ke setiap orang yang punya akses repo, dan menghapusnya di commit berikutnya tidak menghapusnya dari histori Git. Pola amannya: rahasia selalu dibaca dari environment, berkas konfigurasi hanya menyimpan pengaturan yang tidak rahasia:

php
<?php
putenv("IMPOR_API_KEY=demo-abc-12345");

$apiKey = getenv("IMPOR_API_KEY");

if ($apiKey !== false) {
    echo "API key terbaca dari environment (", strlen($apiKey), " karakter)\n";
} else {
    echo "API key belum di-set — tool harus menolak jalan\n";
}

Output yang Dihasilkan:

text
API key terbaca dari environment (14 karakter)

Pada pemakaian nyata, baris putenv() tidak ada — nilainya diisi oleh lingkungan (systemd, Docker, CI secret) sebelum tool dijalankan. Tool cukup membaca dengan getenv() dan menolak berjalan bila kosong, karena lanjut tanpa kunci justru menghasilkan kegagalan yang membingungkan di tengah jalan.

Ringkasan #

Kita kini punya kerangka konfigurasi utuh: format dipilih sadar (PHP array, JSON, atau INI), berkas dicari berlapis dari direktori kerja hingga /etc, default di kode disatukan lewat array_replace_recursive(), environment variable menimpa nilai per pemanggilan, hasil akhir divalidasi di awal dengan pesan yang jelas, dan rahasia tidak pernah menyentuh berkas. Tool yang dikonfigurasi dengan rapi tinggal satu langkah lagi untuk sampai ke pengguna: dikemas dan didistribusikan — materi chapter Distributing CLI Tools berikutnya.

Referensi #