Laravel Pennant: Feature Flag Tanpa Deploy Ulang

Diperbaharui 9 November 2026 9:21 AM

Laravel Pennant adalah package feature flag resmi Laravel: saklar untuk menyalakan atau mematikan fitur tanpa mengubah kode dan tanpa deploy ulang. Kalau kamu pernah menahan pull request berhari-hari karena fiturnya belum siap untuk semua user, Pennant menyelesaikan itu. Fitur tetap ikut rilis, tapi hanya aktif untuk scope yang kamu tentukan.

Apa Itu Laravel Pennant

Pennant menyimpan hasil resolusi sebuah feature, bukan definisinya. Saat feature dicek pertama kali untuk seorang user, Pennant menjalankan closure definisi, menyimpan hasilnya, lalu memakai nilai tersimpan itu untuk pengecekan berikutnya. Jadi user tidak berpindah-pindah tampilan hanya karena lottery dijalankan ulang tiap request.

Ada dua driver: database menyimpan nilai di tabel features (default), array menyimpan di memori untuk test. Menurut composer.json di repo laravel/pennant, dependensinya hanya komponen illuminate/* dan symfony/*, dengan requirement PHP ^8.1 dan dukungan Laravel 10–13. Versi terbaru di Packagist: v1.26.0; laravel/framework sudah v13.40.0 (butuh PHP ^8.3).

Instalasi dan Migrasi

Tiga perintah cukup. Perintah kedua mempublikasikan config/pennant.php sekaligus migration tabel features.

composer require laravel/pennant

php artisan vendor:publish --provider="Laravel\Pennant\PennantServiceProvider"

php artisan migrate

Setelah itu buka config/pennant.php kalau kamu ingin mengganti driver default. Untuk aplikasi biasa, biarkan database. Untuk test suite, ubah lewat environment variable PENNANT_STORE=array di phpunit.xml supaya setiap test mulai dari keadaan bersih tanpa menyentuh database.

Mendefinisikan Feature: Closure dan Class

Definisi feature adalah tempat kamu menulis aturan siapa yang dapat fitur apa. Cara paling singkat adalah closure di AppServiceProvider. Closure menerima satu argumen: scope, yang umumnya user yang sedang login.

<?php

namespace App\Providers;

use App\Models\User;
use Illuminate\Support\Lottery;
use Illuminate\Support\ServiceProvider;
use Laravel\Pennant\Feature;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Feature::define('editor-baru', fn (User $user) => match (true) {
            $user->isInternalTeamMember() => true,
            $user->isHighTrafficCustomer() => false,
            default => Lottery::odds(1 / 100),
        });

        // Kalau definisinya murni lottery, closure boleh dihilangkan.
        Feature::define('homepage-redesign', Lottery::odds(1, 1000));
    }
}

Aturan di atas menghasilkan tiga kelompok: tim internal selalu dapat fitur, pelanggan dengan trafik tinggi sengaja ditahan, sisanya ikut undian 1 banding 100. Pennant mengevaluasi urutan match dari atas ke bawah, jadi letakkan pengecualian di awal.

Kalau jumlah feature mulai banyak, pindahkan tiap definisi ke class sendiri lewat php artisan pennant:feature NamaFeature. Class hasil generator ada di app/Features dan cukup punya method resolve(). Bedanya dengan closure: class tidak perlu didaftarkan di service provider, dan pengecekannya memakai nama class — Feature::active(NamaFeature::class). Class juga bisa menambahkan method before() yang selalu dievaluasi di memori sebelum nilai tersimpan dibaca, berguna untuk mematikan fitur darurat lewat config tanpa menghapus nilai lama.

Mengecek Feature di Controller dan Blade

Untuk pengecekan kondisional, facade Feature menyediakan active(), inactive(), allAreActive(), someAreActive(), dan varian kebalikannya. Kalau butuh menjalankan aksi berbeda tanpa if bersarang, pakai when() atau unless().

use Laravel\Pennant\Feature;

public function index(Request $request)
{
    return Feature::when(
        'editor-baru',
        fn () => $this->renderEditorBaru($request),
        fn () => $this->renderEditorLama($request),
    );
}

Di Blade, gunakan direktif @feature; untuk beberapa feature sekaligus ada @featureany, dan untuk rich value ada @elsefeature. Kalau kamu menambahkan trait HasFeatures ke model User, pengecekan bisa ditulis lebih dekat ke model: $user->features()->active('editor-baru').

Middleware: Kunci Route Sebelum Controller Jalan

Pengecekan di dalam controller artinya controller sudah dieksekusi. Untuk endpoint yang benar-benar tidak boleh diakses, pasang middleware EnsureFeaturesAreActive di route. Middleware ini mengembalikan HTTP 400 kalau salah satu feature tidak aktif, dan responsnya bisa diganti lewat EnsureFeaturesAreActive::whenInactive() — misalnya jadi 403 supaya lebih masuk akal untuk masalah izin:

Route::get('/api/servers', ...)->middleware(EnsureFeaturesAreActive::using('new-api', 'servers-api'));

Scope: User, Tim, sampai Global

Scope adalah “siapa” dalam pengecekan feature. Default-nya user yang login, tapi bisa apa saja — tim, organisasi, bahkan tenant. Cukup panggil Feature::for($user->team)->active('billing-v2'), dan definisi feature-nya menerima Team sebagai argumen.

Kalau hampir semua feature dicek terhadap tim, jangan tulis for() berulang kali. Set default scope sekali dengan Feature::resolveScopeUsing(fn ($driver) => Auth::user()?->team) di boot(). Untuk flag menyeluruh, gunakan Feature::globally()->active('maintenance-mode').

Satu jebakan penting: kalau scope yang kamu oper bernilai null sementara definisi feature hanya menerima tipe User, Pennant langsung mengembalikan false tanpa memanggil closure. Ini sering terjadi di queued job dan Artisan command. Solusinya, tulis tipe parameternya nullable dan tangani sendiri: fn (User|null $user) => $user === null ? true : ....

Rich Values untuk Eksperimen UI

Feature flag tidak harus boolean. Definisi boleh mengembalikan string, angka, atau array — misalnya tiga varian warna tombol beli. Ambil nilainya dengan Feature::value('purchase-button'). Aturannya: feature dianggap aktif selama nilainya bukan false, jadi varian 'tart-orange' tetap “aktif” meski bukan true.

Feature::define('purchase-button', fn (User $user) => Arr::random([
    'blue-sapphire',
    'seafoam-green',
    'tart-orange',
]));

$warna = Feature::value('purchase-button');

// Blade: cabang per nilai
// @feature('purchase-button', 'seafoam-green') ... @elsefeature('purchase-button', 'tart-orange') ... @endfeature

Karena hasilnya disimpan per scope, user yang sama akan melihat varian yang sama sampai nilainya dihapus. Ini yang membuat A/B test bisa diukur: tanpa penyimpanan, angka konversi tiap varian akan bercampur.

Mengubah Nilai Flag Tanpa Deploy

Untuk mengaktifkan atau mematikan flag dari kode, ada activate() dan deactivate(). Untuk memaksa semua scope memakai satu nilai, pakai activateForEveryone() — cocok ketika rollout selesai. forget() menghapus nilai satu scope sehingga definisi dijalankan ulang, sedangkan purge() membersihkan seluruh nilai untuk satu atau beberapa feature.

# Bersihkan nilai feature yang sudah tidak dipakai
php artisan pennant:purge editor-baru

# Bersihkan semua, kecuali yang masih terdaftar di service provider
php artisan pennant:purge --except-registered

Perintah pennant:purge enak dipasang di pipeline deploy. Setelah flag dihapus dari kode, nilai lamanya ikut dibersihkan sehingga tabel features tidak menumpuk baris mati.

Eager Loading agar Tidak N+1

Pennant menyimpan cache di memori selama satu request, tapi cache itu per scope. Kalau kamu mengecek feature di dalam loop terhadap 500 user dengan driver database, Pennant akan menjalankan ratusan query. Solusinya load() sebelum loop, atau loadMissing() kalau sebagian nilai sudah terbaca. Untuk memuat semua feature yang terdaftar, gunakan loadAll().

Feature::for($users)->load(['notifikasi-beta']);

Pola yang sama berguna saat mengirim notifikasi massal atau broadcast event real-time — misalnya pada setup Laravel Reverb yang mengecek flag per user sebelum mengirim event. Memuat nilai di awal jauh lebih murah daripada query per iterasi.

Kapan Feature Flag Justru Bikin Repot

Feature flag bukan default untuk semua perubahan. Kalau fitur hanya punya dua keadaan (selesai atau belum) dan tidak perlu dirilis bertahap, branch biasa lebih sederhana. Flag mulai berguna ketika kamu butuh rollout bertahap ke persentase user, kill switch untuk fitur berisiko, atau eksperimen yang harus dibandingkan antar varian.

Biaya yang sering dilupakan adalah kombinasi: dua flag boolean jadi empat jalur kode, lima flag jadi tiga puluh dua. Setiap flag yang dibiarkan hidup setelah rollout selesai menambah cabang yang harus dites selamanya. Tentukan sejak awal tanggal “lulus atau mati” tiap flag, lalu hapus definisinya dan jalankan pennant:purge.

Dua catatan produksi. Jangan menyimpan data sensitif di nilai feature, karena isinya ada di tabel. Jangan pula menaruh logika berat di resolve() — closure itu hanya dijalankan saat nilai belum ada, lalu hasilnya dipakai terus sampai di-purge. Untuk integrasi mahal seperti Laravel AI SDK atau Prism PHP, flag sebaiknya cuma menentukan boleh atau tidaknya fitur dipanggil.

FAQ

Apakah Pennant butuh Redis atau layanan tambahan?

Tidak. Default-nya memakai database aplikasi dengan satu tabel features. Kalau butuh driver lain, Pennant menyediakan kontrak Laravel\Pennant\Contracts\Driver untuk driver kustom.

Bisakah feature flag dipakai di queued job?

Bisa, tapi scope harus eksplisit. Di job tidak ada user login, jadi tulis Feature::for($user)->active('...') atau definisikan default scope yang valid di luar HTTP. Kalau scope null dan definisi tidak menerima null, hasilnya selalu false.

Bagaimana cara mengetes kode yang bergantung pada flag?

Definisikan ulang feature di dalam test dengan nilai tetap, misalnya Feature::define('purchase-button', 'seafoam-green'), lalu assert nilai yang diharapkan. Tambahkan PENNANT_STORE=array di phpunit.xml supaya test tidak menulis ke tabel features.

Kesimpulan

Laravel Pennant memberi feature flag tanpa layanan pihak ketiga: satu tabel, satu facade, definisi berupa closure atau class. Alur hariannya: definisikan aturan rollout di service provider, cek lewat facade atau direktif Blade, tentukan scope saat pengecekan terjadi di luar HTTP, lalu bersihkan flag begitu rollout selesai. Selama disiplin menghapus flag lama, Pennant menghemat banyak deploy darurat.

Referensi

susantokun avatar
susantokun
Hanya seorang programmer yang fokus di bidang web development. Tidak nyaman dengan keramaian dan suka akan keindahan.
Kebijakan Berkomentar :
1. Dilarang berkomentar yang mengandung SPAM, SARA, HOAX, PORNO.2. Mohon sertakan informasi detail saat terjadi error (pesan error, sreenshoot, code, logs, dsb.).