Hampir setiap aplikasi memiliki pengaturan yang bisa berubah tanpa mengubah kode: nama aplikasi, kredensial database, kunci API payment gateway, hingga batas maksimum upload. Jika pengaturan seperti itu ditulis permanen di dalam logika, aplikasi yang sama tidak bisa dipindahkan dari laptop ke server produksi tanpa diedit ulang. Configuration management adalah latihan memisahkan “apa yang berbeda antar lingkungan” dari “apa yang menjadi perilaku aplikasi”. Pada chapter ini kita akan menyusun konfigurasi dari berbagai sumber, membingkainya sebagai objek bertipe, dan menjaga rahasia agar tidak pernah masuk ke repositori kode.

Memisahkan Konfigurasi dari Kode #

Patokannya sederhana: kalau sebuah nilai bisa berbeda antara komputer Anda dan server produksi, nilai itu bukan bagian dari kode — nilai itu konfigurasi. Aplikasi yang sehat bisa dijalankan di laptop, di server CI, dan di server produksi tanpa satu baris kode pun diubah; yang berbeda hanyalah data yang diberikan kepada aplikasi. Pemisahan seperti ini juga menjaga testability, karena test boleh menyuntikkan konfigurasi tiruan — misalnya database SQLite di memori — tanpa menyentuh kode produksi. Prinsip ini sejalan dengan separation of concerns yang sudah kita bahas: logika bisnis tidak perlu tahu dari mana nilai konfigurasi berasal.

Sumber Konfigurasi yang Umum #

PHP tidak memaksakan satu format konfigurasi. Tabel berikut merangkum pilihan yang paling sering dipakai di ekosistem nyata:

SumberBentukKapan cocok dipakai
Array PHPberkas config/*.php yang return arraykonfigurasi internal aplikasi; tanpa parsing, mendukung type juggling
Environment variablepasangan KEY=VALUE dari prosesnilai yang berbeda per lingkungan, termasuk rahasia
JSONberkas .jsonkonfigurasi yang juga dibaca sistem non-PHP
YAMLberkas .yamlkonfigurasi deklaratif ala Docker Compose; perlu pustaka parser

Array PHP adalah pilihan paling natural karena tidak butuh parsing dan bisa berisi ekspresi apa pun. Environment variable unggul untuk nilai yang datang dari luar proses — container, web server, atau pipeline CI. JSON cocok kalau konfigurasi yang sama dikonsumsi sistem lain, sedangkan YAML lebih mudah dibaca manusia untuk berkas panjang, tetapi PHP membutuhkan pustaka seperti symfony/yaml karena ekstensi yaml tidak dibundel secara bawaan.

Konfigurasi sebagai Array PHP #

Cara paling umum di proyek PHP modern adalah berkas khusus yang mengembalikan array lewat return. Konvensi folder config/ seperti berikut:

php
<?php
// config/app.php - isi berkas konfigurasi aplikasi
return [
    "app" => [
        "nama" => "Toko Buku Berkah",
        "debug" => true,
    ],
    "database" => [
        "driver" => "sqlite",
        "namaBerkas" => "toko.sqlite",
    ],
];

Dalam proyek nyata, berkas tersebut dimuat dengan $config = require "config/app.php"; karena require mengembalikan nilai dari return. Untuk melihat cara mengakses nilainya, kita tulis array yang sama secara langsung:

php
<?php
$config = [
    "app" => [
        "nama" => "Toko Buku Berkah",
        "debug" => true,
    ],
    "database" => [
        "driver" => "sqlite",
        "namaBerkas" => "toko.sqlite",
    ],
];

echo "Aplikasi: {$config["app"]["nama"]}\n";
echo "Driver DB: {$config["database"]["driver"]}\n";

Output yang Dihasilkan:

text
Aplikasi: Toko Buku Berkah
Driver DB: sqlite

Struktur bertingkat seperti ini memudahkan pengelompokan: kunci app menampung identitas aplikasi, kunci database menampung pengaturan penyimpanan. Kekurangannya juga terlihat — akses $config["database"]["driver"] sepenuhnya bergantung pada ejaan kunci, dan salah ketik baru terasa saat runtime. Kekurangan itu akan kita jawab dengan objek konfigurasi bertipe di bagian akhir chapter.

Environment Variable dengan getenv dan $_ENV #

Environment variable adalah pasangan kunci-nilai yang dimiliki oleh proses yang menjalankan PHP — diatur oleh shell, web server, atau container, bukan oleh kode Anda. PHP menyediakan dua jalan baca: fungsi getenv() dan superglobal $_ENV. Fungsi putenv() mengatur variabel baru untuk proses saat ini, sehingga berguna untuk simulasi di skrip latihan dan di test:

php
<?php
// Di server nyata, nilai ini diatur oleh lingkungan (shell, container, CI).
// putenv() di sini hanya mensimulasikan kondisi tersebut.
putenv("APP_ENV=development");

echo 'getenv: ', getenv("APP_ENV"), "\n";
echo '$_ENV: ', (isset($_ENV["APP_ENV"]) ? $_ENV["APP_ENV"] : "(tidak ada)"), "\n";

// Di dalam test, kita bisa mengisi $_ENV secara manual
$_ENV["APP_ENV"] = "testing";
echo '$_ENV manual: ', $_ENV["APP_ENV"], "\n";

Output yang Dihasilkan:

text
getenv: development
$_ENV: (tidak ada)
$_ENV manual: testing

Perhatikan perilaku penting yang terlihat pada output: putenv() membuat nilai terbaca oleh getenv(), tetapi tidak mengisi $_ENV. Superglobal $_ENV adalah potret lingkungan saat proses PHP dimulai, bukan jendela hidup yang ikut berubah. Di terminal, Anda bisa mengatur variabel sebelum menjalankan skrip tanpa putenv() sama sekali:

bash
APP_ENV=production php index.php

Berkas .env dan Loader Mini #

Mengetik export KEY=VALUE satu per satu di terminal melelahkan, sehingga komunitas menyepakati konvensi berkas .env: daftar KEY=VALUE yang diletakkan di akar proyek dan dimuat ke environment saat aplikasi booting. Framework seperti Laravel dan Symfony menyediakan komponen Dotenv untuk memuatnya otomatis; baris yang diawali # diperlakukan sebagai komentar dan diabaikan. Isinya terlihat seperti berikut:

text
APP_NAME="Toko Buku Berkah"
APP_ENV=development
DATABASE_URL=sqlite:toko.sqlite
PAYMENT_API_KEY=YOUR_API_KEY

Untuk memahami apa yang sebenarnya dilakukan loader seperti itu, kita buat parser mini yang membaca isi berkas .env dan mengembalikannya sebagai array. Pembacaan berkas nyata bisa memakai file_get_contents(); di sini isinya kita tulis sebagai heredoc agar contoh berdiri sendiri:

php
<?php
function parseEnvFile(string $isi): array
{
    $hasil = [];
    foreach (explode("\n", $isi) as $baris) {
        $baris = trim($baris);
        if ($baris === "" || str_starts_with($baris, "#")) {
            continue;
        }
        $posisi = strpos($baris, "=");
        if ($posisi === false) {
            continue;
        }
        $kunci = trim(substr($baris, 0, $posisi));
        $nilai = trim(substr($baris, $posisi + 1), " \t\n\r\0\x0B\"'");
        $hasil[$kunci] = $nilai;
    }
    return $hasil;
}

$isiEnv = <<<ENV
#komentar: baris ini diabaikan
APP_NAME="Toko Buku Berkah"
APP_ENV=development
DATABASE_URL=sqlite:toko.sqlite
ENV;

$env = parseEnvFile($isiEnv);
echo "APP_NAME: {$env["APP_NAME"]}\n";
echo "APP_ENV: {$env["APP_ENV"]}\n";
echo "DATABASE_URL: {$env["DATABASE_URL"]}\n";

Output yang Dihasilkan:

text
APP_NAME: Toko Buku Berkah
APP_ENV: development
DATABASE_URL: sqlite:toko.sqlite

Mari kita bedah logika parser ini:

  1. explode("\n", $isi): memecah isi berkas menjadi baris-baris terpisah untuk diproses satu per satu.
  2. trim() + cek #: menghapus spasi tepi, lalu melompati baris kosong dan komentar.
  3. strpos($baris, "="): mencari posisi tanda sama dengan pertama sebagai batas antara kunci dan nilai; baris tanpa = dilewati.
  4. trim(..., " \t\n\r\0\x0B\"'"): membuang spasi dan tanda kutip pengapit di sekeliling nilai, sehingga "Toko Buku Berkah" menjadi Toko Buku Berkah.

Parser produksi biasanya menambahkan dukungan export KEY=VALUE, nilai multi-baris, dan interpolasi variabel — tetapi inti mekanismenya sama persis.

Objek Konfigurasi Bertipe #

Array menerima kunci apa pun, jadi salah ketik "databse" tidak langsung ketahuan — nilai yang kembali adalah null dan galatnya baru muncul jauh di tempat lain. Solusinya adalah membungkus konfigurasi dalam class dengan property bertipe. Mari kita ubah struktur array di awal chapter menjadi class AppConfig:

php
<?php
final class DatabaseConfig
{
    public function __construct(
        public readonly string $driver,
        public readonly string $namaBerkas,
        public readonly int $batasKoneksi = 5,
    ) {}

    public static function dariArray(array $data): self
    {
        return new self(
            driver: $data["driver"],
            namaBerkas: $data["namaBerkas"],
            batasKoneksi: $data["batasKoneksi"] ?? 5,
        );
    }
}

final class AppConfig
{
    public function __construct(
        public readonly string $nama,
        public readonly bool $debug,
        public readonly DatabaseConfig $database,
    ) {}

    public static function dariArray(array $data): self
    {
        return new self(
            nama: $data["app"]["nama"],
            debug: (bool) $data["app"]["debug"],
            database: DatabaseConfig::dariArray($data["database"]),
        );
    }
}

$config = AppConfig::dariArray([
    "app" => ["nama" => "Toko Buku Berkah", "debug" => true],
    "database" => ["driver" => "sqlite", "namaBerkas" => "toko.sqlite"],
]);

echo "Aplikasi: {$config->nama}\n";
echo "Debug: ", ($config->debug ? "aktif" : "nonaktif"), "\n";
echo "DB: {$config->database->driver}:{$config->database->namaBerkas}\n";
echo "Batas koneksi: {$config->database->batasKoneksi}\n";

Output yang Dihasilkan:

text
Aplikasi: Toko Buku Berkah
Debug: aktif
DB: sqlite:toko.sqlite
Batas koneksi: 5

Ada beberapa hal yang bekerja serempak pada contoh di atas. Constructor property promotion membuat deklarasi property sekaligus pengisiannya ringkas, sedangkan modifier readonly menjamin nilai tidak bisa diubah setelah objek dibuat — konfigurasi memang seharusnya beku selama request berjalan. Method dariArray() berperan sebagai satu-satunya tempat pembacaan kunci string, sehingga kesalahan ejaan terkumpul di satu titik, bukan tersebar di seluruh kode. Setelah itu, sisa aplikasi bekerja dengan $config->database->driver yang dijamin bertipe string dan lengkap dengan autocomplete dari editor.

Memisahkan Lingkungan Development dan Produksi #

Struktur folder yang lazim untuk memisahkan lingkungan adalah satu berkas dasar plus satu berkas per lingkungan:

text
config/
├── app.php            # nilai dasar, sama di semua lingkungan
├── development.php    # debug aktif, berkas lokal
└── production.php     # debug mati, berkas server

Berkas dasar menampung nilai yang tidak pernah berubah antar lingkungan, sedangkan berkas lingkungan hanya menampung perbedaannya. Saat booting, aplikasi memilih berkas lingkungan berdasarkan APP_ENV, lalu menggabungkan keduanya dengan array_replace_recursive() — fungsi yang menimpa nilai bertingkat secara dalam tanpa menghapus kunci lain:

php
<?php
$base = [
    "app" => ["nama" => "Toko Buku Berkah"],
    "database" => ["driver" => "sqlite"],
];

$development = [
    "app" => ["debug" => true],
    "database" => ["namaBerkas" => "toko-dev.sqlite"],
];

$konfigurasi = array_replace_recursive($base, $development);

print_r($konfigurasi);

Output yang Dihasilkan:

text
Array
(
    [app] => Array
        (
            [nama] => Toko Buku Berkah
            [debug] => 1
        )

    [database] => Array
        (
            [driver] => sqlite
            [namaBerkas] => toko-dev.sqlite
        )

)

Pada hasil gabungan, kunci dari berkas dasar (nama, driver) tetap utuh, sementara kunci dari berkas lingkungan (debug, namaBerkas) menimpa atau menambah. Dengan pola ini, berkas produksi cukup memuat "debug" => false dan nama berkas database di server — tanpa menyalin ulang seluruh konfigurasi. Di sinilah environment variable berperan penuh: nilai APP_ENV sendiri dibaca dari environment, bukan dari kode, sehingga berkas yang sama bisa dipakai di semua server.

Rahasia di Environment, Bukan di Kode #

Aturan terakhir sekaligus paling penting: kredensial tidak pernah ditulis di dalam kode maupun berkas konfigurasi yang ikut di-commit. Kode berpindah tangan — di-clone oleh rekan tim, di-push ke mirror, dibaca oleh layanan CI — dan setiap salinan membawa rahasia di dalamnya. Pola yang aman adalah membaca rahasia dari environment saat booting dan gagal cepat bila variabelnya belum diatur:

php
<?php
function bacaRahasia(string $nama): string
{
    $nilai = getenv($nama);
    if ($nilai === false || $nilai === "") {
        throw new RuntimeException("Variabel lingkungan $nama belum diatur");
    }
    return $nilai;
}

putenv("PAYMENT_API_KEY=YOUR_API_KEY");

echo bacaRahasia("PAYMENT_API_KEY"), "\n";

try {
    bacaRahasia("EMAIL_PASSWORD");
} catch (RuntimeException $e) {
    echo "Gagal: ", $e->getMessage(), "\n";
}

Output yang Dihasilkan:

text
YOUR_API_KEY
Gagal: Variabel lingkungan EMAIL_PASSWORD belum diatur

Fungsi bacaRahasia() mengembalikan string hanya bila variabel lingkungan ada dan tidak kosong; selebihnya fungsi tersebut melempar RuntimeException dengan pesan yang menyebut nama variabel yang hilang. Gagal cepat seperti ini jauh lebih baik daripada melanjutkan request dengan kredensial kosong lalu mendapat error samar dari payment gateway dua puluh baris kemudian. Pesan error sengaja hanya menyebut nama variabel, bukan nilainya, agar log yang bocor tidak ikut membocorkan rahasia.

Ringkasan #

Konfigurasi yang sehat mengalir satu arah: dari environment dan berkas konfigurasi, dibaca sekali saat aplikasi dimulai, dibingkai sebagai objek readonly bertipe, lalu dialirkan ke bagian kode yang membutuhkannya. Rahasia selalu tinggal di environment variable, bukan di repositori. Pertanyaan berikutnya adalah siapa yang melakukan seluruh pembacaan itu dan kapan — jawabannya adalah proses bootstrap yang akan kita bangun pada chapter berikutnya.

Referensi #