Выкатывать новую функциональность сразу на всех пользователей — рискованно. Если что-то пойдёт не так, придётся срочно откатывать релиз. Feature flags (фиче-флаги) решают эту проблему: код новой функции уже в продакшене, но включается она постепенно — сначала для команды, потом для 1% пользователей, потом для всех. У Laravel для этого есть официальный пакет — Laravel Pennant. Лёгкий, без лишних зависимостей и сторонних сервисов: все значения флагов хранятся в вашей же базе данных. Разберём установку и основные сценарии работы на практике.
Установка и настройка
Ставим пакет через Composer:
composer require laravel/pennant
Публикуем конфиг и миграции, затем запускаем миграции — Pennant создаст таблицу features, в которой будет хранить рассчитанные значения флагов:
php artisan vendor:publish --provider="Laravel\Pennant\PennantServiceProvider"
php artisan migrate
В конфиге config/pennant.php выбирается драйвер хранения. Из коробки их два: database (по умолчанию, значения сохраняются в БД) и array (в памяти, на время запроса — удобно для тестов). При необходимости можно написать свой драйвер, например под Redis.
Определяем первый фиче-флаг
Флаги объявляются через фасад Feature, обычно в boot() сервис-провайдера. Метод define принимает имя фичи и замыкание, которое вычисляет её начальное значение. В замыкание приходит «scope» — чаще всего текущий авторизованный пользователь:
use App\Models\User;
use Illuminate\Support\Lottery;
use Laravel\Pennant\Feature;
public function boot(): void
{
Feature::define('new-api', fn (User $user) => match (true) {
$user->isInternalTeamMember() => true,
$user->isHighTrafficCustomer() => false,
default => Lottery::odds(1 / 100),
});
}
Логика читается легко: сотрудникам компании фича включена всегда, высоконагруженным клиентам — выключена, остальным выдаётся случайно с шансом 1 к 100. Важный момент: замыкание выполняется только при первой проверке флага для конкретного пользователя. Результат сохраняется в таблицу features, и дальше Pennant читает его из хранилища — пользователь не будет «мигать» между вариантами от запроса к запросу.
Class-based фичи
Для крупных фич удобнее отдельный класс — его не нужно регистрировать в провайдере. Генерируем заготовку командой:
php artisan pennant:feature NewApi
Класс появится в app/Features. В нём достаточно описать метод resolve:
namespace App\Features;
use App\Models\User;
use Illuminate\Support\Lottery;
class NewApi
{
public function resolve(User $user): mixed
{
return match (true) {
$user->isInternalTeamMember() => true,
default => Lottery::odds(1 / 100),
};
}
}
У классовых фич есть мощная возможность — метод before. Он выполняется в памяти до обращения к хранилищу, и если вернёт не-null, это значение перекроет сохранённое. Так можно мгновенно отключить фичу для всех (например, при найденном баге), не потеряв уже рассчитанные значения, а после исправления — включить обратно.
Проверяем флаги в коде
Основной метод — Feature::active(). По умолчанию проверка идёт против текущего пользователя:
public function index(Request $request): Response
{
return Feature::active('new-api')
? $this->resolveNewApiResponse($request)
: $this->resolveLegacyApiResponse($request);
}
Есть и условное выполнение через when / unless, и проверка наборов флагов: allAreActive, someAreActive, inactive. Если добавить модели User трейт HasFeatures, проверки становятся ещё компактнее: $user->features()->active('new-api').
В Blade-шаблонах работает директива @feature:
@feature('site-redesign')
<!-- новый дизайн -->
@else
<!-- старый дизайн -->
@endfeature
А для защиты целых маршрутов есть middleware EnsureFeaturesAreActive — если фича выключена для пользователя, маршрут вернёт 400 (ответ можно кастомизировать через whenInactive):
use Laravel\Pennant\Middleware\EnsureFeaturesAreActive;
Route::get('/api/servers', function () {
// ...
})->middleware(EnsureFeaturesAreActive::using('new-api'));
Scope: не только пользователи
Флаг можно проверять против любой сущности. Например, новую систему биллинга логично включать для целых команд, а не отдельных людей:
Feature::define('billing-v2', function (Team $team) {
if ($team->created_at->isAfter(new Carbon('1st Jan, 2023'))) {
return true;
}
return Lottery::odds(1 / 100);
});
if (Feature::for($user->team)->active('billing-v2')) {
return redirect('/billing/v2');
}
Через Feature::resolveScopeUsing() можно задать scope по умолчанию — и тогда везде писать просто Feature::active('billing-v2'). Учтите нюанс: если scope оказался null (артизан-команда, очередь, неавторизованный запрос), а определение фичи null не принимает, Pennant вернёт false. Для таких случаев объявляйте параметр как User|null и обрабатывайте null в логике.
Не только true/false: A/B-тесты
Флаг может хранить любое значение, а не только булево. Классический пример — тестируем три цвета кнопки покупки:
Feature::define('purchase-button', fn (User $user) => Arr::random([
'blue-sapphire',
'seafoam-green',
'tart-orange',
]));
$color = Feature::value('purchase-button');
Каждому пользователю один раз выпадет свой вариант и закрепится за ним. Останется собрать метрики конверсии по вариантам — и зафиксировать победителя для всех: Feature::activateForEveryone('purchase-button', 'seafoam-green').
Управление значениями и производительность
Значениями можно управлять вручную: Feature::activate('new-api') и Feature::deactivate('new-api') переключают флаг для конкретного scope, activateForEveryone / deactivateForEveryone — для всех сразу. Метод Feature::forget() стирает сохранённое значение, и при следующей проверке оно рассчитается заново. Когда фича доехала до 100% и флаг больше не нужен, чистим хранилище:
php artisan pennant:purge new-api
Команду удобно встроить в деплой-пайплайн; флаг --except-registered удалит всё, кроме фич, зарегистрированных в провайдерах.
По производительности: внутри одного запроса Pennant кэширует результаты в памяти, повторные проверки не бьют в базу. Но если вы проверяете флаг в цикле по коллекции пользователей, получите по запросу на каждого. Решение — жадная загрузка:
Feature::for($users)->load(['notifications-beta']);
foreach ($users as $user) {
if (Feature::for($user)->active('notifications-beta')) {
$user->notify(new RegistrationSuccess);
}
}
Тестирование
В тестах фичу проще всего переопределить — заданное значение перекроет логику из провайдера:
public function test_it_can_control_feature_values()
{
Feature::define('purchase-button', 'seafoam-green');
$this->assertSame('seafoam-green', Feature::value('purchase-button'));
}
А в phpunit.xml стоит задать PENNANT_STORE=array, чтобы тесты не писали в базу.
Итоги
Laravel Pennant закрывает практически все типовые задачи фиче-флагов: постепенные раскатки через Lottery, A/B-тесты через rich values, мгновенный «рубильник» через before, флаги на уровне команд и любых других сущностей. При этом не нужен внешний SaaS — достаточно таблицы в вашей БД. Если вы практикуете trunk-based development или просто хотите перестать бояться релизов по пятницам, Pennant — самый естественный способ добавить фиче-флаги в Laravel-проект. Начните с одного флага на ближайшую крупную фичу — и дальше уже не остановитесь.