Laravel Pennant: Feature Flags Đúng Cách
Deploy feature cho 100% user ngay từ ngày đầu là đánh bạc. Feature flags cho phép bạn kiểm soát ai thấy gì, mà không cần re-deploy code. Laravel Pennant là giải pháp first-party — đơn giản, tích hợp Eloquent, và được xây dựng chính xác cho mục đích này.
Tại Sao Cần Feature Flags?
Không có feature flags:
1. Viết code → 2. Deploy → 3. Hy vọng không lỗi → 4. Rollback nếu lỗi
Có feature flags:
1. Viết code → 2. Deploy (flag TẮT) → 3. Bật cho 5% → 4. Monitor → 5. Mở 100%
Tình huống thực tế:
- Rollout checkout flow mới cho 10% khách hàng trả phí
- Bật tính năng beta chỉ cho team nội bộ
- A/B test trang pricing
- Kill switch: tắt ngay tính năng bị lỗi
Cài Đặt
composer require laravel/pennant
php artisan vendor:publish --provider="Laravel\Pennant\PennantServiceProvider"
php artisan migrate
Lệnh này tạo bảng features để lưu trạng thái feature đã resolve.
Định Nghĩa Features
Features được định nghĩa trong AppServiceProvider:
// app/Providers/AppServiceProvider.php
use Laravel\Pennant\Feature;
use App\Models\User;
public function boot(): void
{
Feature::define('new-dashboard', function (User $user): bool {
return $user->is_admin;
});
Feature::define('redesigned-checkout', function (User $user): bool {
// Rollout dần: 20% users
return $user->id % 5 === 0;
});
Feature::define('dark-mode', function (User $user): bool {
return $user->created_at->isAfter('2026-01-01');
});
}
Closure nhận scope (thường là User model) và trả về true/false. Một khi đã resolve cho user, kết quả được lưu trong database và không thay đổi cho đến khi bạn cập nhật rõ ràng.
Kiểm Tra Features
Trong Controllers
use Laravel\Pennant\Feature;
class DashboardController extends Controller
{
public function index()
{
if (Feature::active('new-dashboard')) {
return view('dashboard.v2');
}
return view('dashboard.v1');
}
}
Trong Blade
@feature('new-dashboard')
<x-dashboard-v2 />
@else
<x-dashboard-v1 />
@endfeature
Trong Middleware
// routes/web.php
use Laravel\Pennant\Middleware\EnsureFeatureIsActive;
Route::get('/beta/reports', ReportController::class)
->middleware(EnsureFeatureIsActive::using('advanced-reports'));
Nếu feature không active, Pennant trả về 403 mặc định. Bạn có thể tùy chỉnh:
// AppServiceProvider
Feature::whenInactive('advanced-reports', function () {
abort(404); // Ẩn hoàn toàn thay vì 403
});
Rich Feature Values
Features không bị giới hạn boolean. Trả về bất kỳ giá trị nào cho A/B testing:
Feature::define('checkout-flow', function (User $user): string {
return match (true) {
$user->id % 3 === 0 => 'variant-a',
$user->id % 3 === 1 => 'variant-b',
default => 'control',
};
});
$variant = Feature::value('checkout-flow');
return match ($variant) {
'variant-a' => view('checkout.single-page'),
'variant-b' => view('checkout.multi-step'),
default => view('checkout.classic'),
};
Class-Based Features
Cho logic phức tạp, dùng dedicated feature classes:
php artisan pennant:feature NewOnboardingFlow
// app/Features/NewOnboardingFlow.php
namespace App\Features;
use App\Models\User;
use Illuminate\Support\Lottery;
class NewOnboardingFlow
{
public function resolve(User $user): bool
{
if ($user->is_admin) {
return true;
}
if ($user->created_at->isAfter(now()->subDays(7))) {
return Lottery::odds(1, 4)->choose(); // 25% cho user mới
}
return false;
}
}
use App\Features\NewOnboardingFlow;
if (Feature::active(NewOnboardingFlow::class)) {
// Flow mới
}
Gradual Rollouts với Lottery
Lottery là chìa khóa cho rollout theo phần trăm:
use Illuminate\Support\Lottery;
Feature::define('new-search', function (User $user): bool {
return Lottery::odds(1, 10)->choose(); // 10% xác suất
});
Quan trọng: Một khi đã resolve, kết quả được lưu lại. User sẽ không bị nhảy random giữa các trạng thái. Để mở rộng rollout:
// Kích hoạt cho TẤT CẢ users chưa được resolve
Feature::activateForEveryone('new-search');
// Hoặc chọn lọc
Feature::for($user)->activate('new-search');
// Tắt cho user cụ thể
Feature::for($user)->deactivate('new-search');
Purge & Đánh Giá Lại
Khi bạn thay đổi logic rollout, giá trị cũ vẫn còn. Purge để đánh giá lại:
// Đánh giá lại cho tất cả users
Feature::purge('new-search');
// Đánh giá lại cho user cụ thể
Feature::for($user)->forget('new-search');
php artisan pennant:purge new-search
Scopes Ngoài Users
Feature flags không chỉ giới hạn cho users. Kiểm tra với teams, organizations, hoặc bất kỳ model nào:
Feature::define('advanced-analytics', function (Team $team): bool {
return $team->plan === 'enterprise';
});
Feature::for($team)->active('advanced-analytics');
Eager Loading Cho Performance
Tránh N+1 queries khi check features cho nhiều users:
Feature::for($users)->loadAll();
Feature::for($users)->load(['new-dashboard', 'dark-mode']);
Testing
public function test_new_dashboard_is_shown_when_feature_active(): void
{
Feature::activate('new-dashboard');
$response = $this->actingAs($this->user)->get('/dashboard');
$response->assertViewIs('dashboard.v2');
}
public function test_old_dashboard_is_shown_when_feature_inactive(): void
{
Feature::deactivate('new-dashboard');
$response = $this->actingAs($this->user)->get('/dashboard');
$response->assertViewIs('dashboard.v1');
}
Luôn dùng Feature::activate() / Feature::deactivate() trong tests — đừng phụ thuộc vào closure logic.
Quy Trình Production
Ngày 1: Deploy với flag TẮT
Ngày 2: Bật cho team nội bộ
Ngày 3: Bật cho 10% users
Ngày 5: Monitor OK → 50%
Ngày 7: Full rollout
Ngày 14: Xóa flag, dọn dẹp code
Kết Luận
Feature flags biến việc deploy từ "tất cả hoặc không" thành phát hành dần dần, có đo lường. Laravel Pennant cho bạn khả năng này mà không cần dependency bên ngoài.
Takeaway:
- Định nghĩa features bằng closures hoặc classes
- Dùng
Lotterycho rollout theo phần trăm - Purge khi thay đổi logic rollout
- Eager load để tránh N+1
- Luôn dọn dẹp flags cũ — feature flag debt là có thật