Salah satu hal yang cukup sering bikin bingung saat mengembangkan aplikasi berbasis CodeIgniter 4 di lingkungan lokal (XAMPP/Laragon) adalah: aplikasi disimpan di dalam subfolder seperti htdocs/nama-folder atau www/nama-folder, tapi di sisi lain kita juga ingin tetap bisa menjalankannya lewat php spark serve tanpa perlu ubah-ubah konfigurasi setiap kali pindah cara menjalankan.
Catatan ini saya tulis berdasarkan proses troubleshooting yang saya lakukan sendiri di sebuah project CodeIgniter 4 (Laragon, folder project bernama policell), supaya tidak lupa dan bisa jadi referensi kalau ketemu masalah serupa di kemudian hari.
Daftar isi
1. Latar Belakang
Secara default, CodeIgniter 4 melayani seluruh request dari folder public/. Kalau project disimpan di htdocs/nama-folder (XAMPP) atau www/nama-folder (Laragon), URL bawaannya jadi http://localhost/nama-folder/public/... — ada embel-embel /public yang mengganggu.
Yang saya inginkan sederhana saja:
- Bisa diakses langsung lewat
http://localhost/nama-folder/(tanpa/public) saat dijalankan lewat Apache/Laragon. - Bisa juga tetap dijalankan lewat php spark serve (
http://localhost:8080/) untuk development sehari-hari. - Kedua cara itu jalan tanpa perlu gonta-ganti file
.envsetiap kali pindah mode.
baseURL yang berubah di CodeIgniter 4.4+, sampai perilaku redirect() yang ternyata tidak selalu konsisten dengan base_url().2. Gejala Masalah
Ada dua masalah berbeda yang muncul berurutan. Saya pisahkan supaya jelas urutan penyebab dan solusinya.
2.1 Error ConfigException baseURL
Percobaan pertama, saya kosongkan saja app.baseURL di .env dengan asumsi CodeIgniter akan otomatis menebak base URL dari request yang masuk (ini perilaku versi CI4 yang lebih lama). Hasilnya malah error:
Fatal error: Uncaught CodeIgniter\Exceptions\ConfigException: Config\App::$baseURL "/" is not a valid URL.
Ternyata mulai CodeIgniter 4.4+, class SiteURI memvalidasi baseURL secara ketat dan tidak lagi otomatis menebak dari string kosong seperti versi-versi sebelumnya. Jadi butuh cara lain untuk auto-detect.
2.2 Redirect Kehilangan Subfolder
Setelah baseURL berhasil di-auto-detect (lihat solusinya di bagian berikutnya), semua link biasa di halaman — yang dibangun pakai base_url() — sudah benar, lengkap dengan /nama-folder/-nya. Tapi begitu proses login, browser malah diarahkan ke:
http://localhost/admin/dashboard (404 Not Found)
padahal seharusnya ke http://localhost/nama-folder/admin/dashboard. Prefix folder-nya hilang. Anehnya, ini hanya terjadi pada hasil redirect()->to() di controller (misalnya setelah proses login atau logout) — link-link biasa di halaman tetap normal.
redirect()->to('/admin/dashboard'). Setelah garis miringnya dihapus jadi redirect()->to('admin/dashboard') pun ternyata masalahnya masih sama persis. Berarti akar masalahnya bukan di situ.3. Solusi Lengkap
Berikut susunan solusi akhir yang benar-benar berhasil, urut dari awal.
3.1 Root .htaccess
Supaya folder public/ tidak perlu muncul di URL, tambahkan file .htaccess di root project (sejajar dengan folder app/, public/, system/, writable/ — bukan di dalam folder public/):
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{THE_REQUEST} ^GET.*index\.php [NC]
RewriteRule ^(.*)index\.php/?(.*)$ /$1$2 [R=301,NE,L]
RewriteRule ^$ public/ [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ public/$1 [L]
</IfModule>
File ini yang bertugas mengarahkan semua request masuk (http://localhost/nama-folder/apa-saja) secara transparan ke folder public/ di baliknya.
3.2 Konfigurasi .env
Di file .env, kosongkan app.baseURL:
app.baseURL = ''
String kosong ini nanti yang jadi penanda bagi kode di Config/App.php (lihat poin berikutnya) untuk mengisi nilainya secara otomatis saat aplikasi berjalan.
3.3 Auto-detect baseURL di Config/App.php
Ini bagian intinya. Karena CodeIgniter 4.4+ tidak lagi menebak sendiri baseURL dari string kosong, saya tambahkan logic auto-detect manual lewat __construct() di app/Config/App.php:
public function __construct()
{
parent::__construct();
if ($this->baseURL === '') {
$isHttps = (! empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
|| ($_SERVER['SERVER_PORT'] ?? null) === '443';
$scheme = $isHttps ? 'https' : 'http';
$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
$basePath = '';
// php spark serve: docroot sudah langsung public/, tidak butuh base path.
if (PHP_SAPI !== 'cli-server') {
$scriptFilename = str_replace('\\', '/', $_SERVER['SCRIPT_FILENAME'] ?? '');
$documentRoot = str_replace('\\', '/', $_SERVER['DOCUMENT_ROOT'] ?? '');
if ($documentRoot !== '' && str_starts_with($scriptFilename, $documentRoot)) {
$relative = substr($scriptFilename, strlen($documentRoot));
$pos = strpos($relative, '/public/');
$basePath = $pos !== false ? substr($relative, 0, $pos) : '';
}
}
$this->baseURL = $scheme . '://' . $host . $basePath . '/';
}
}
Poin pentingnya ada di dua hal:
PHP_SAPI === 'cli-server'— ini nilai yang pasti benar hanya kalau PHP sedang dijalankan lewat built-in server (php spark serve / php -S). Kalau kondisi ini terpenuhi, base path dikosongkan karena docroot bawaan spark serve memang sudah langsung ke folderpublic/.- Selisih
SCRIPT_FILENAMEdanDOCUMENT_ROOT— dua-duanya adalah path fisik di disk (bukan path "logis" hasil rewrite Apache), sehingga hasilnya konsisten untuk request apa pun. Contoh:SCRIPT_FILENAME=C:/laragon/www/policell/public/index.phpdikurangiDOCUMENT_ROOT=C:/laragon/www, hasilnya/policell/public/index.php, lalu bagian sebelum/public/itulah nama foldernya:/policell.
App.php lagi sama sekali.3.4 Bungkus redirect() dengan base_url()
Nah, ini penyebab masalah redirect yang sempat bikin bingung di bagian 2.2. Setelah ditelusuri, ternyata redirect()->to() di CodeIgniter 4 memakai jalur konversi URL yang berbeda dari base_url()/site_url() — meskipun sama-sama merujuk ke konfigurasi baseURL yang sama, hasilnya bisa tidak konsisten untuk kasus tertentu (salah satunya request POST seperti submit form login).
Solusinya: jangan andalkan redirect()->to() untuk mengonversi path relatif sendiri. Bungkus manual pakai base_url() yang sudah terbukti selalu benar:
// Sebelum (berpotensi kehilangan subfolder)
return redirect()->to('admin/dashboard');
// Sesudah (konsisten, pakai mekanisme yang sama dengan link biasa)
return redirect()->to(base_url('admin/dashboard'));
Terapkan pola ini di semua pemanggilan redirect()->to() di seluruh controller, bukan cuma yang berhubungan dengan login/logout saja — supaya konsisten dan tidak muncul masalah serupa di endpoint lain di kemudian hari.
4. Kenapa Pendekatan Ini Reliable
Sebelum sampai ke solusi final di atas, saya sempat mencoba pendekatan lain yang ternyata kurang pas:
- Auto-detect dari
SCRIPT_NAME— ini path "logis" hasil rewrite Apache, dan ternyata perilakunya bisa berbeda tergantung jenis request (terutama antara GET dan POST), sehingga hasil deteksi base path bisa tidak konsisten. - Hardcode nama folder manual (mis.
$namaFolderProject = 'policell';) — ini sempat jadi solusi sementara yang bekerja, tapi punya kekurangan: harus diedit ulang setiap kali folder project di-rename atau dipindah.
Kombinasi SCRIPT_FILENAME + DOCUMENT_ROOT pada akhirnya jadi solusi paling stabil karena keduanya murni path fisik di sistem file (bukan hasil interpretasi ulang dari rewrite rule), sehingga nilainya konsisten untuk metode request apa pun. Ditambah dengan membungkus semua redirect() pakai base_url(), seluruh bagian aplikasi — baik link biasa maupun redirect — akhirnya memakai satu mekanisme resolusi URL yang sama persis.
5. Ringkasan / Cheat Sheet
localhost/nama-folder DAN php spark serve:
- Tambahkan root
.htaccessyang redirect ke folderpublic/. - Set
app.baseURL = ''di.env. - Tambahkan auto-detect
baseURLdiConfig/App.phplewat__construct(), berbasisPHP_SAPI+ selisihSCRIPT_FILENAME/DOCUMENT_ROOT. - Pastikan mod_rewrite aktif & AllowOverride All.
- Bungkus semua
redirect()->to('path')jadiredirect()->to(base_url('path')).
6. Penutup
Masalah base URL semacam ini sebenarnya cukup umum ditemui pengembang CodeIgniter (atau framework PHP lain) yang terbiasa kerja di lingkungan local server berbasis subfolder seperti XAMPP/Laragon. Kuncinya adalah memahami bahwa tidak semua fungsi URL helper di framework menggunakan jalur resolusi yang sama — kadang perlu dipaksa konsisten secara manual seperti pada kasus redirect() di atas.
Semoga catatan ini berguna buat saya sendiri di kemudian hari (atau siapa pun yang mampir dan mengalami masalah serupa) — dan tidak perlu mengulang proses troubleshooting dari nol lagi.

Posting Komentar