← Back to list

Jebakan Singleton di Laravel Octane: Mengapa Aplikasi Anda Bisa Menyajikan “Stale Data”?

Performance yang jauh lebih cepat adalah alasan utama kita melirik Laravel Octane. Dengan menjalankan aplikasi di atas Swoole atau…

Arieansyah · 2026-01-20 15:16 · 0 claps · 3.7 min read
#laravel #laravel-octane #frankenphp #yajra-datatables #web-performance
Open on Medium ↗

Jebakan Singleton di Laravel Octane: Mengapa Aplikasi Anda Bisa Menyajikan “Stale Data”?

Performance yang jauh lebih cepat adalah alasan utama kita melirik Laravel Octane. Dengan menjalankan aplikasi di atas Swoole atau FrankenPHP, kita menghilangkan overhead bootstrapping framework pada setiap request.

Namun, kecepatan ini datang dengan harga: Perubahan total pada mental model siklus hidup aplikasi (Application Lifecycle).

Jika Anda memindahkan aplikasi Laravel legacy ke Octane tanpa memahami konsep Request Injection dan Singleton, Anda mungkin akan menghadapi bug paling menakutkan dalam programming: Data Corruption atau Stale State. User A bisa tiba-tiba melihat data User B, atau validasi form menjadi kacau.

Mari kita bedah masalah ini dengan studi kasus nyata yang baru saja saya alami saat me-refactor aplikasi POS (Point of Sales).

🧠 Perbedaan Mental Model: Life & Death

Di setup tradisional (PHP-FPM):

  1. Request masuk.
  2. Aplikasi booting.
  3. Controller & Service di-resolve.
  4. Response dikirim.
  5. Aplikasi mati (destroyed). Semua memori dibersihkan.

Di Laravel Octane:

  1. Worker start. Aplikasi booting SEKALI.
  2. Request 1 masuk → Gunakan instance aplikasi yang sudah hidup.
  3. Request 2 masuk → Gunakan instance YANG SAMA.

Karena aplikasi “tidak pernah mati” antar-request, objek yang bersifat Singleton akan tetap hidup di memori.

💀 “Si Tersangka Utama”: Request Injection di Constructor

Inilah pola coding yang umum di Laravel biasa, tapi berbahaya di Octane jika Class tersebut di-resolve sebagai Singleton atau long-lived object:

// ❌ KODE BERBAHAYA DI OCTANE
class UserService
{
    protected $request;

    // Constructor Injection
    public function __construct(Request $request) {
        $this->request = $request;
    }

    public function updateProfile() {
        // Menggunakan $this->request yang disimpan saat inisialisasi
        return User::update($this->request->all());
    }
}

Apa yang sebenarnya terjadi?

  1. Worker Start: Aplikasi jalan.
  2. Request 1 (User A: “Alice”): Masuk. Aplikasi me-resolve UserService. Karena ini pertama kalinya, Container membuat instance baru dan meng-inject Request milik Alice. — $userService->request berisi data "Alice". — Fungsi updateProfile berjalan dengan benar.
  3. Request 2 (User B: “Bob”): Masuk. Aplikasi melihat UserService sudah pernah dibuat (sebagai singleton). Aplikasi menggunakan kembali (REUSE) instance UserService yang tadi. — MASALAHNYA: Instance ini masih memegang $this->request milik ALICE! — Saat Bob memanggil updateProfile, kode akan mengupdate user dengan data Alice, bukan Bob.

Ini disebut Stale Request State.

🔍 Studi Kasus Nyata: Yajra DataTables & AJAX Detection

Saya menemukan kasus menarik pada library populer Yajra DataTables. Pattern umum penggunaannya adalah me-return object DataTable di method render():

// Controller
public function index(UsersDataTable $dataTable) {
    return $dataTable->render('users.index');
}

Di balik layar, library ini mencoba “pintar” dengan mendeteksi otomatis:

  • Jika request biasa (HTML) → return View.
  • Jika request AJAX → return JSON.

Dimana bug-nya?

Logikanya kira-kira seperti ini:

public function render() {
    // $this->request() diambil saat object DataTable dibuat
    if ($this->request()->ajax()) {
        return $this->json();
    }
    return $this->view();
}

Di Octane:

  1. Request 1 (Load Halaman / HTML): Object DataTable dibuat. $request->ajax() = false. Return HTML. Object DataTable ini (atau request di dalamnya) tersimpan di memori worker.
  2. Request 2 (DataTables AJAX call): Worker yang sama menangani request. Tapi karena logika deteksi masih “memegang” state dari Request 1 (yang bukan AJAX), method render() kembali me-return HTML, bukan JSON.
  3. Akibatnya: DataTables di frontend error dengan pesan “Invalid JSON response”, padahal kodenya tidak berubah.

🕵️‍♂️ Misteri “Draw Mismatch”: Kenapa Tabel Stuck di “Processing…”?

Selain error JSON, salah satu gejala paling membingungkan di Octane adalah saat DataTables stuck di loading “Processing…” meskipun response dari server terlihat sukses (HTTP 200).

Ini terjadi karena mekanisme keamanan internal DataTables yang disebut Draw Counter.

Bagaimana DataTables Bekerja?

  1. Frontend: Mengirim request dengan parameter draw=1 (counter).
  2. Backend: Wajib mengembalikan parameter draw yang SAMA PERSIS (draw: 1).
  3. Frontend: Mengecek apakah response.draw === request.draw. Jika beda, response dibuang!

Apa yang terjadi di Octane?

Jika Backend me-reuse request object yang “Stale” (kadaluarsa):

  1. Frontend: Kirim draw=1.
  2. Backend: Mengambil request lama dari memori (misalnya request “Load Halaman” pertama). — Di request lama ini, TIDAK ADA parameter draw. — Backend merespons dengan default: draw: 0.
  3. Frontend: 1 === 0? FALSE. Response dianggap tidak valid dan diabaikan.

Hasilnya? Data sebenarnya ada, tapi tabel menolak menampilkannya dan user hanya melihat loading spinner yang berputar selamanya.

🛡️ Solusi: Injection yang Benar

Bagaimana cara memperbaikinya? Kuncinya adalah: Jangan pernah menyimpan State Request di dalam Constructor Service.

1. Gunakan Method Injection

Alih-alih menyimpannya di constructor, minta Request hanya saat method dipanggil. Laravel akan selalu memberikan instance request yang paling fresh.

// ✅ KODE AMAN
class UserService
{
    // Hapus constructor injection

    public function updateProfile(Request $request) {
        // $request di sini SELALU request yang sedang aktif
        return User::update($request->all());
    }
}

2. Hindari Helper Global request() Tanpa Parameter

Helper request() mengambil instance dari Container. Di beberapa konteks Octane, ini bisa me-return instance yang lama jika tidak hati-hati. Lebih aman menggunakan $request yang di-pass dari Controller.

3. Decoupling: Pattern “Dedicated Data Route”

Untuk kasus DataTables tadi, solusi terbaiknya adalah Eksplisit. Jangan biarkan kode menebak-nebak tipe request.

Pisahkan route untuk View dan Data:

// Route
Route::get('users', [UserController::class, 'index']);      // Khusus HTML
Route::get('users/data', [UserController::class, 'data']);  // Khusus JSON
// Controller
public function index() {
    return view('users.index'); // Jelas return View
}

public function data(UsersDataTable $dataTable) {
    return $dataTable->ajax(); // Jelas return JSON (Bypass logika render)
}

Dengan cara ini, kita memutus ketergantungan pada “state detection” dan membuat kode 100% aman untuk berjalan di lingkungan long-lived process seperti Octane.

Kesimpulan

Bermigrasi ke Laravel Octane bukan sekadar “install dan lupa”. Ia menuntut kedisiplinan kode yang lebih tinggi.

Aturan emasnya: Anggap semua Service Anda adalah Singleton yang hidup selamanya. Jangan pernah simpan data spesifik user (seperti Request, User, atau Session) ke dalam properti class Service. Selalu pass data tersebut via method arguments.

Selamat meng-optimize! 🚀


메타데이터
post_id
cf5f67998cd7
slug
jebakan-singleton-di-laravel-octane-mengapa-aplikasi-anda-bisa-menyajikan-stale-data-cf5f67998cd7
url
https://medium.com/@arieansyah/jebakan-singleton-di-laravel-octane-mengapa-aplikasi-anda-bisa-menyajikan-stale-data-cf5f67998cd7
canonical_url
https://medium.com/@arieansyah/jebakan-singleton-di-laravel-octane-mengapa-aplikasi-anda-bisa-menyajikan-stale-data-cf5f67998cd7
author_url
https://medium.com/@arieansyah
status
ok
fetched_at
2026-08-01 18:24:42