Laravel Octane bisa memberi peningkatan performa yang signifikan karena aplikasi tidak perlu melakukan bootstrap penuh di setiap request. Namun ada konsekuensi arsitektural yang sering baru terasa setelah migrasi: state yang seharusnya hanya hidup untuk satu request bisa bertahan lebih lama dari yang kita kira. Gejalanya sering membingungkan: data user sebelumnya muncul di request user lain, konfigurasi tenant terasa “nyangkut”, atau hasil request lama masih terbaca pada request berikutnya.
Masalah ini biasanya bukan karena Octane “salah”, melainkan karena ada asumsi lama dari model PHP-FPM yang tidak lagi aman saat aplikasi berjalan sebagai proses worker jangka panjang. Fokus utama artikel ini adalah empat sumber masalah yang paling umum: service singleton, static property, container reuse, dan stateful service.
Kalau Anda baru pindah ke Octane dan tiba-tiba menemukan bug yang sulit direproduksi di local non-Octane, kemungkinan besar Anda sedang berhadapan dengan salah satu pola tersebut.
Mengapa masalah ini muncul di Octane?
Pada model tradisional PHP-FPM, setiap request umumnya dieksekusi dalam lifecycle yang relatif pendek. Setelah request selesai, memori proses eksekusi tidak dipertahankan untuk konteks aplikasi yang sama seperti pada worker jangka panjang. Banyak developer akhirnya terbiasa menulis kode yang menyimpan state di object service, properti statis, atau singleton karena dalam praktiknya state tersebut “hilang sendiri” setelah request selesai.
Di Octane, pendekatannya berbeda. Aplikasi dijalankan oleh worker yang hidup lebih lama dan melayani banyak request. Artinya, object tertentu dapat tetap berada di memori antar request, terutama jika didaftarkan sebagai singleton atau disimpan pada tempat yang memang tidak dibersihkan otomatis. Ini sangat baik untuk performa, tetapi berbahaya bila kode Anda menyimpan request-specific state pada object yang berumur panjang.
Intinya: pada Octane, anggap worker sebagai proses panjang. Kalau Anda menyimpan data request di level worker, data itu bisa ikut terbawa ke request berikutnya.
Gejala umum: data user atau konfigurasi request sebelumnya terbawa
Berikut beberapa gejala yang sering muncul setelah migrasi ke Octane:
- Service cache internal menyimpan user terakhir yang diproses.
- Tenant ID dari request sebelumnya masih dipakai pada request berikutnya.
- Header, locale, timezone, atau konfigurasi dinamis tidak berubah sesuai request baru.
- Object helper yang pernah diisi dengan request tertentu tetap berisi nilai lama.
- Testing manual terasa acak: bug hanya muncul setelah beberapa request berurutan.
Masalah seperti ini sering terlihat sebagai “data nyangkut”. Padahal akar teknisnya adalah state yang tidak di-reset.
Sumber masalah utama
1. Singleton yang menyimpan data request
Singleton cocok untuk dependency yang benar-benar stateless atau aman dipakai ulang. Masalah muncul ketika singleton menyimpan state yang berasal dari request saat ini, misalnya user, tenant, locale, atau token.
Contoh yang bermasalah:
<?php
namespace App\Services;
class CurrentUserContext
{
private ?int $userId = null;
public function setUserId(?int $userId): void
{
$this->userId = $userId;
}
public function getUserId(): ?int
{
return $this->userId;
}
}
Jika service ini didaftarkan sebagai singleton:
<?php
use App\Services\CurrentUserContext;
$this->app->singleton(CurrentUserContext::class, function () {
return new CurrentUserContext();
});
Lalu di middleware atau controller Anda mengisi user ID dari request saat ini, object yang sama bisa dipakai lagi oleh request berikutnya pada worker yang sama. Jika tidak di-reset dengan benar, request selanjutnya dapat membaca user lama.
2. Static property
Static property sering dipakai untuk cache kecil atau konteks global. Di aplikasi request-per-process, pola ini kadang tidak terlihat berbahaya. Di Octane, static property hidup selama proses worker masih hidup.
Contoh bermasalah:
<?php
namespace App\Support;
class TenantContext
{
public static ?string $tenantId = null;
}
Jika satu request mengisi TenantContext::$tenantId = 'tenant-a' dan request berikutnya lupa mengubah atau mengosongkan nilainya, tenant lama dapat ikut terbaca.
3. Container reuse dan object yang resolved terlalu dini
Dependency injection container di Laravel sangat nyaman, tetapi di Octane Anda perlu lebih hati-hati terhadap kapan object di-resolve dan bagaimana lifecycle-nya. Jika sebuah service di-resolve sekali lalu disimpan dalam singleton, dan service itu menyimpan dependency yang sebenarnya terkait request, Anda bisa mendapatkan referensi usang.
Contohnya, jangan menyimpan object request atau hasil turunannya ke dalam singleton yang hidup panjang. Request saat ini adalah konteks yang berubah-ubah, sehingga tidak aman dijadikan state internal object jangka panjang.
4. Stateful service
Masalah inti sebenarnya bukan hanya singleton. Bahkan object biasa pun bisa berbahaya jika ia stateful dan state-nya dipertahankan lintas request melalui cache internal, static property, atau registry manual. Service yang aman untuk Octane idealnya stateless: menerima semua input lewat parameter method, memprosesnya, lalu mengembalikan hasil tanpa menyimpan konteks request di dalam dirinya.
Cara reproduksi bug secara sederhana
Berikut contoh sederhana untuk menunjukkan bagaimana data bisa “nyangkut” antar request.
Membuat service yang rawan
<?php
namespace App\Services;
class RequestTraceStore
{
private ?string $lastEmail = null;
public function remember(string $email): void
{
$this->lastEmail = $email;
}
public function last(): ?string
{
return $this->lastEmail;
}
}
Daftarkan sebagai singleton:
<?php
use App\Services\RequestTraceStore;
$this->app->singleton(RequestTraceStore::class, function () {
return new RequestTraceStore();
});
Membuat route uji
<?php
use App\Services\RequestTraceStore;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
Route::get('/debug/store', function (Request $request, RequestTraceStore $store) {
if ($request->filled('email')) {
$store->remember($request->query('email'));
}
return response()->json([
'current_input' => $request->query('email'),
'last_email_in_store' => $store->last(),
]);
});
Jalankan aplikasi dengan Octane, lalu panggil berurutan:
curl 'http://127.0.0.1:8000/debug/store?email=alice@example.com'
curl 'http://127.0.0.1:8000/debug/store'
Jika request kedua mengembalikan last_email_in_store = alice@example.com, Anda baru saja mereproduksi bug klasik: state singleton dari request sebelumnya masih ada.
Di lingkungan non-Octane, gejala ini bisa jadi tidak terlihat karena object dibangun ulang per request. Itu sebabnya banyak bug baru muncul setelah migrasi ke Octane.
Pola service yang aman untuk Octane
1. Utamakan service stateless
Pola paling aman adalah membuat service yang tidak menyimpan data request sebagai properti object. Semua data masuk lewat parameter method.
Contoh lebih aman:
<?php
namespace App\Services;
class AuditFormatter
{
public function formatUserAction(int $userId, string $action): array
{
return [
'user_id' => $userId,
'action' => $action,
'recorded_at' => now()->toIso8601String(),
];
}
}
Service seperti ini aman dijadikan singleton karena tidak menyimpan state antar pemanggilan. Namun tetap evaluasi apakah memang perlu singleton; jika tidak ada kebutuhan khusus, binding biasa sering lebih sederhana.
2. Jangan simpan request, user, atau tenant sebagai properti singleton
Hindari pola seperti ini:
<?php
class BadTenantService
{
private string $tenantId;
public function setTenantId(string $tenantId): void
{
$this->tenantId = $tenantId;
}
}
Lebih aman gunakan parameter eksplisit:
<?php
class TenantReportService
{
public function generate(string $tenantId): array
{
return ['tenant_id' => $tenantId];
}
}
Dengan begitu, konteks request tidak tersimpan di memori service setelah method selesai.
3. Pakai binding non-singleton untuk service yang memang stateful
Kalau Anda benar-benar membutuhkan object yang menyimpan state sementara selama pemrosesan request, jangan jadikan ia singleton. Biarkan container membuat instance baru saat dibutuhkan.
<?php
$this->app->bind(App\Services\RequestScopedAccumulator::class, function () {
return new App\Services\RequestScopedAccumulator();
});
Trade-off-nya: ada sedikit overhead pembuatan object baru. Namun untuk service yang membawa state request, ini jauh lebih aman daripada membiarkan data bocor lintas request.
4. Hindari static property untuk konteks dinamis
Jika Anda saat ini punya helper global dengan static $currentTenant, static $user, atau static $locale, anggap itu kandidat refactor. Lebih baik ambil data dari request, auth context, atau kirim sebagai parameter method.
5. Pisahkan konfigurasi aplikasi dan konfigurasi per-request
Konfigurasi yang benar-benar global masih aman jika nilainya tetap. Yang berbahaya adalah mengubah konfigurasi global berdasarkan request, lalu berharap nilainya kembali seperti semula. Bila Anda punya kebutuhan seperti multi-tenant, lebih aman mengelola state tenant secara eksplisit dalam alur request, bukan menimpa konfigurasi global yang dapat memengaruhi request lain.
Contoh refactor dari pola rawan ke pola aman
Misalkan sebelumnya Anda punya service singleton untuk menyimpan user saat ini:
<?php
class CurrentActor
{
private ?int $id = null;
public function set(?int $id): void
{
$this->id = $id;
}
public function id(): ?int
{
return $this->id;
}
}
Lalu service lain membaca CurrentActor untuk menentukan siapa yang sedang melakukan aksi. Pola ini rentan di Octane.
Refactor yang lebih aman:
<?php
class ActivityLogger
{
public function log(?int $actorId, string $event, array $payload = []): void
{
// simpan log ke database / queue
}
}
Controller atau middleware yang memegang konteks request bertugas meneruskan $actorId secara eksplisit. Kodenya mungkin sedikit lebih verbos, tetapi lifecycle datanya jelas dan jauh lebih aman.
Debugging: cara melacak state yang bocor
1. Audit semua singleton buatan sendiri
Periksa semua pemanggilan $this->app->singleton(...). Tanyakan untuk masing-masing service:
- Apakah service ini menyimpan properti yang berubah berdasarkan request?
- Apakah ada method
setUser,setTenant,setLocale,remember, atau pola serupa? - Apakah service ini menyimpan object
Request, user auth, header, atau token?
Jika jawabannya ya, itu kandidat utama penyebab bug.
2. Cari static property dan cache internal manual
Lakukan pencarian pada kode untuk keyword seperti static $, private static, atau registry global. Banyak kasus bocor state berasal dari helper lama yang terlihat sepele.
3. Tambahkan log identitas object
Untuk memastikan apakah instance yang sama dipakai ulang, Anda bisa log spl_object_id() atau hash object:
<?php
logger()->info('RequestTraceStore instance', [
'object_id' => spl_object_id($store),
]);
Jika beberapa request menampilkan ID object yang sama dan properti internalnya berubah-ubah, Anda tahu object tersebut hidup lintas request.
4. Uji dengan request berurutan, bukan satu request saja
Banyak bug Octane tidak terlihat jika Anda hanya menembak satu endpoint sekali. Uji dengan beberapa request berurutan menggunakan nilai berbeda, terutama pada endpoint yang melibatkan auth, tenant, locale, atau konfigurasi dinamis.
5. Waspadai hasil yang “kadang-kadang” salah
Karena worker bisa menangani request berbeda pada waktu berbeda, bug ini sering terlihat tidak konsisten. Ketidakkonsistenan bukan berarti bug-nya tidak nyata; justru itu ciri khas masalah state yang bocor.
Kapan singleton tetap boleh dipakai?
Singleton tetap berguna jika object-nya stateless atau hanya menyimpan state yang benar-benar immutable dan global untuk worker. Contohnya, formatter murni, parser yang tidak menyimpan konteks request, atau service utilitas yang hanya berisi fungsi transformasi.
Namun begitu service mulai memiliki method setter atau cache internal yang dipengaruhi request, pertimbangkan ulang. Aturan praktisnya sederhana:
- Aman untuk singleton: service stateless, pure-ish, immutable.
- Tidak aman untuk singleton: service yang menyimpan user, tenant, request, locale, token, atau hasil request sebelumnya.
Kesimpulan
Ketika pindah ke Laravel Octane, Anda tidak hanya mengubah cara menjalankan aplikasi, tetapi juga mengubah asumsi tentang lifecycle object. Bug “data nyangkut antar request” hampir selalu berakar pada state yang disimpan terlalu lama: singleton yang menyimpan konteks request, static property, container reuse yang tidak disadari, atau service stateful.
Solusi paling efektif biasanya bukan menambahkan reset manual di banyak tempat, melainkan merancang service agar stateless, mengirim konteks secara eksplisit lewat parameter, menghindari static property untuk data dinamis, dan memakai binding non-singleton untuk object yang memang perlu state per request.
Jika setelah migrasi ke Octane Anda melihat user atau konfigurasi request lama ikut terbawa, jangan langsung curiga ke framework. Audit dulu singleton dan state global Anda. Dalam banyak kasus, sumber masalahnya ada di sana.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!