Antara momen request tiba dan kode bisnis Anda berjalan, ada satu rangkaian persiapan yang hampir selalu dibutuhkan: class harus bisa dimuat, error handler harus terpasang, konfigurasi harus terbaca, dan service seperti koneksi database harus siap dipakai. Rangkaian persiapan itu disebut bootstrap — proses menghidupkan aplikasi dari keadaan mati menjadi siap menerima request. Tanpa bootstrap yang tertata, persiapan-persiapan ini tersebar di banyak berkas dan sulit diprediksi urutannya. Pada chapter ini kita akan menyusun bootstrap secara eksplisit, memakai pola front controller sebagai pintu masuk tunggal, dan membedakan bootstrap dari routing maupun handler.

Apa yang Dikerjakan Bootstrap #

Bootstrap yang lengkap biasanya mengerjakan lima hal secara berurutan:

  1. Autoloading — memastikan setiap class bisa dimuat otomatis saat pertama kali dipakai.
  2. Error handling — memasang handler yang menampilkan detail galat saat development dan menyembunyikannya saat produksi.
  3. Pemuatan konfigurasi — membaca environment variable dan berkas konfigurasi menjadi objek bertipe.
  4. Pembangunan service — menyiapkan koneksi database, logger, dan service lain, biasanya lewat container.
  5. Eksekusi — menyerahkan kendali ke routing lalu handler untuk memproses request.

Urutan ini penting: error handler harus terpasang sebelum konfigurasi dibaca, supaya galat saat membaca konfigurasi tertangani dengan baik. Konfigurasi harus siap sebelum service dibangun, karena service membaca kredensial darinya. Bab-bab sebelumnya telah menyiapkan bahan bakunya — separation of concerns memberi kerangka pemisahan, dependency injection memberi teknik penyambungan, dan configuration management memberi sumber datanya.

Untuk autoloading, standar industri hari ini adalah autoloader Composer. Satu baris berikut memuat seluruh class proyek dan paket sesuai pemetaan PSR-4:

php
<?php
// public/index.php - baris pertama di hampir semua proyek modern
require __DIR__ . "/../vendor/autoload.php";

Di balik layar, Composer mendaftarkan fungsi lewat spl_autoload_register(). Versi minimal yang menunjukkan mekanismenya terlihat seperti berikut:

php
<?php
// Autoloader sederhana tanpa Composer (untuk memahami mekanismenya)
spl_autoload_register(function (string $class): void {
    $berkas = __DIR__ . "/src/" . str_replace("\\", "/", $class) . ".php";
    if (is_file($berkas)) {
        require $berkas;
    }
});

Saat PHP menemukan class yang belum didefinisikan, interpreter memanggil setiap fungsi terdaftar sampai salah satunya berhasil memuat berkas yang mendefinisikan class tersebut. Mendaftarkan autoloader adalah pekerjaan awal bootstrap karena hampir semua langkah sesudahnya — error handler berbasis class, objek konfigurasi, service — bergantung padanya.

Pola Front Controller #

Cara paling tua menjalankan aplikasi PHP adalah satu berkas PHP per halaman: beranda.php, kontak.php, produk.php. Setiap berkas menyiapkan ulang koneksinya sendiri, dan perubahan global harus diedit di banyak tempat. Front controller membalik pendekatan itu: seluruh request HTTP masuk melalui satu berkas saja — index.php — yang kemudian memilih kode mana yang berjalan berdasarkan URI.

text
public/
└── index.php        # satu-satunya pintu masuk HTTP
src/
├── Controller/
├── Service/
└── Repository/
config/
└── app.php

Struktur di atas menempatkan index.php di folder public/ agar seluruh berkas lain berada di luar jangkauan URL langsung. Web server diarahkan agar semua request ditulis ulang ke berkas tersebut — di Apache lewat mod_rewrite, di Nginx lewat direktif try_files. Isi front controller paling sederhana terlihat seperti ini:

php
<?php
// public/index.php - satu-satunya pintu masuk seluruh request HTTP
$routes = [
    "/" => fn(): string => "Beranda Toko Buku Berkah",
    "/kontak" => fn(): string => "Halaman Kontak",
];

// Di bawah web server, $_SERVER["REQUEST_URI"] berisi path yang diminta.
// Di CLI nilai tersebut tidak ada, jadi kita beri nilai bawaan "/".
$uri = parse_url($_SERVER["REQUEST_URI"] ?? "/", PHP_URL_PATH);

if (isset($routes[$uri])) {
    echo $routes[$uri]();
} else {
    http_response_code(404);
    echo "404 - Halaman tidak ditemukan";
}

Output yang Dihasilkan:

text
Beranda Toko Buku Berkah

Tiga bagian bekerja di sini. Array $routes memetakan URI ke closure yang tahu cara menanganinya; parse_url(..., PHP_URL_PATH) mengambil path murni dari URI lengkap sehingga query string seperti ?halaman=2 ikut terbuang; dan cabang else menyiapkan respons 404 untuk URI yang tidak dikenal. Contoh ini berjalan di CLI karena nilai $_SERVER["REQUEST_URI"] dijaga dengan ?? "/" — teknik yang sama yang membuat front controller bisa diuji tanpa web server sungguhan.

Fungsi boot yang Eksplisit #

Front controller di atas masih mengerjakan semuanya sendiri. Langkah berikutnya adalah mengangkat persiapan menjadi fungsi bernama yang jelas — konvensi yang lazim dinamai boot() — sehingga proses hidupnya aplikasi terbaca sebagai satu daftar langkah, bukan tersebar di antara kode routing. Mari kita bangun versi ringkasnya:

php
<?php
final class Konfigurasi
{
    public function __construct(
        public readonly string $lingkungan,
        public readonly bool $debug,
    ) {}
}

final class Aplikasi
{
    /** @var array<string, Closure> */
    private array $pabrik = [];

    public function __construct(public readonly Konfigurasi $konfigurasi) {}

    public function daftar(string $nama, Closure $pabrik): void
    {
        $this->pabrik[$nama] = $pabrik;
    }

    public function ambil(string $nama): mixed
    {
        return ($this->pabrik[$nama])();
    }
}

function boot(): Aplikasi
{
    // 1. baca konfigurasi dari environment
    $lingkungan = getenv("APP_ENV") ?: "development";
    $konfigurasi = new Konfigurasi($lingkungan, debug: $lingkungan === "development");

    // 2. bangun aplikasi beserta service yang dibutuhkan
    $aplikasi = new Aplikasi($konfigurasi);
    $aplikasi->daftar("koneksiDb", fn(): string => "PDO(sqlite::memory:) siap");

    return $aplikasi;
}

$dev = boot();
putenv("APP_ENV=production");
$prod = boot();

echo "Dev : {$dev->konfigurasi->lingkungan}, debug ", ($dev->konfigurasi->debug ? "aktif" : "nonaktif"), "\n";
echo "Prod: {$prod->konfigurasi->lingkungan}, debug ", ($prod->konfigurasi->debug ? "aktif" : "nonaktif"), "\n";
echo "Service: ", $dev->ambil("koneksiDb"), "\n";

Output yang Dihasilkan:

text
Dev : development, debug aktif
Prod: production, debug nonaktif
Service: PDO(sqlite::memory:) siap

Mari kita bedah komponennya satu per satu:

  1. Konfigurasi adalah objek nilai beku yang menyimpan hasil pembacaan environment — pola yang sama dengan chapter configuration management.
  2. Aplikasi membungkus konfigurasi sekaligus bertindak sebagai container mini: method daftar() menyimpan pabrik (factory) berupa closure, dan ambil() menjalankan pabrik tersebut untuk menghasilkan service.
  3. boot() sendiri hanya berurusan dengan persiapan — fungsi ini tidak tahu apa pun tentang URI atau request.
  4. Dua panggilan boot() di bagian bawah menunjukkan bahwa perilaku aplikasi berubah mengikuti environment: development mengaktifkan debug, production mematikannya.

Nilai terbesar fungsi boot() yang eksplisit adalah kemampuannya diuji terpisah. Test bisa memanggil boot(), memeriksa isi Konfigurasi, lalu mengambil service dari container — semuanya tanpa menjalankan web server atau mensimulasikan HTTP.

Booting Malas untuk Service Berat #

Bootstrap di atas membangun semua service saat boot() dipanggil. Untuk aplikasi kecil hal itu murah, tetapi beberapa service sungguhan berat dibangunnya: koneksi database, klien API, atau parser template. Membangun semuanya di awal berarti membayar biaya tersebut pada setiap request — termasuk request yang akhirnya tidak memakai service itu. Solusinya adalah booting malas (lazy booting): mendaftarkan service sebagai resep, lalu membangunnya hanya saat pertama kali diambil:

php
<?php
final class WadahService
{
    /** @var array<string, Closure> */
    private array $pabrik = [];

    private array $cache = [];

    public function daftar(string $nama, Closure $pabrik): void
    {
        $this->pabrik[$nama] = $pabrik;
    }

    public function ambil(string $nama): mixed
    {
        return $this->cache[$nama] ??= ($this->pabrik[$nama])();
    }
}

$wadah = new WadahService();
$wadah->daftar("laporanPenjualan", function (): string {
    echo "  -> laporanPenjualan baru dibangun sekarang\n";
    return "Laporan penjualan siap";
});

echo "Sebelum akses: service belum dibangun\n";
echo $wadah->ambil("laporanPenjualan"), "\n";
echo "Akses kedua:\n";
echo $wadah->ambil("laporanPenjualan"), "\n";

Output yang Dihasilkan:

text
Sebelum akses: service belum dibangun
  -> laporanPenjualan baru dibangun sekarang
Laporan penjualan siap
Akses kedua:
Laporan penjualan siap

Kunci perilaku ini ada pada satu baris: $this->cache[$nama] ??= (...)(). Operator ??= hanya mengeksekusi pabrik bila cache masih kosong, sehingga pemanggilan pertama membangun service dan menyimpannya, sedangkan pemanggilan kedua langsung mengambil hasil cache — terlihat dari pesan pembangunan yang hanya muncul sekali. Closure yang kita daftarkan memang sengaja mencetak pesan agar momen pembangunannya terlihat; service sungguhan cukup mengembalikan objeknya.

Memisahkan Bootstrap, Routing, dan Handler #

Kita sekarang punya tiga kepingan yang perlu dirangkai: persiapan aplikasi, keputusan routing, dan kode yang merespon request. Ketiganya adalah tanggung jawab yang berbeda — persiapan tidak perlu tahu URI, dan handler tidak perlu tahu cara aplikasi dihidupkan. Diagram berikut memperlihatkan alurnya:

Request masuk lewat front controller, bootstrap menyiapkan aplikasi, routing mencocokkan URI dengan handler, dan handler menghasilkan response. Contoh berikut menyusun ketiganya sebagai bagian terpisah — komentar menandai berkas asalnya di proyek nyata:

php
<?php
// --- di proyek nyata berada di boot.php: hanya urusan menyiapkan aplikasi ---
function boot(): array
{
    return [
        "namaAplikasi" => "Toko Buku Berkah",
        "debug" => true,
    ];
}

// --- di proyek nyata berada di handlers.php: hanya urusan merespon request ---
function handlerProduk(array $aplikasi): string
{
    return "[" . $aplikasi["namaAplikasi"] . "] Daftar produk tersedia";
}

function handlerGalat(array $aplikasi): string
{
    if ($aplikasi["debug"]) {
        return "Detail galat ditampilkan karena debug aktif";
    }
    return "Terjadi kesalahan, silakan coba lagi nanti";
}

// --- di proyek nyata berada di public/index.php: front controller ---
$routes = [
    "/produk" => "handlerProduk",
];

$_SERVER["REQUEST_URI"] = "/produk?halaman=2";  // simulasi request masuk
$uri = parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH);
$handler = $routes[$uri] ?? "handlerGalat";

echo $handler(boot()), "\n";

$_SERVER["REQUEST_URI"] = "/tidak-ada";          // simulasi request tak dikenal
$uri = parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH);
$handler = $routes[$uri] ?? "handlerGalat";

echo $handler(boot()), "\n";

Output yang Dihasilkan:

text
[Toko Buku Berkah] Daftar produk tersedia
Detail galat ditampilkan karena debug aktif

Perhatikan arah alir datanya: handler menerima aplikasi sebagai parameter dan hanya membaca kebutuhannya, tidak pernah memanggil boot() sendiri. Routing hanyalah pencocokan string URI ke nama handler — tidak memuat konfigurasi, tidak menyentuh database. Dengan pemisahan ini, tiap bagian berubah karena alasan yang berbeda tanpa saling menyeret: handler baru bisa ditambah tanpa menyentuh boot(), dan strategi bootstrap boleh dirombak tanpa mengubah satu handler pun.

Ringkasan #

Bootstrap adalah urutan persiapan yang eksplisit: autoloading, error handling, konfigurasi, service, lalu eksekusi. Front controller menjadikan index.php satu-satunya pintu masuk request; fungsi boot() mengangkat persiapan menjadi kode yang bisa dibaca dan diuji; booting malas menunda pembangunan service mahal sampai benar-benar dibutuhkan. Aplikasi yang dihidupkan dengan cara ini punya titik awal yang jelas dan prediktabel. Pertanyaan berikutnya bersifat strategis: seberapa jauh struktur seperti ini perlu dibawa — dan kapan struktur berlapis justru berlebihan? Itulah bahan chapter penutup section ini.

Referensi #