Laravel特性开关怎么实现

wen PHP项目 1

本文目录导读:

Laravel特性开关怎么实现

  1. 使用 Laravel Pennant(官方推荐)
  2. 基于数据库的特性开关
  3. 基于配置文件的方式
  4. 使用环境变量(简单场景)
  5. 自定义中间件
  6. Blade 指令
  7. 最佳实践建议

我来详细介绍 Laravel 特性开关(Feature Flags)的几种实现方式:

使用 Laravel Pennant(官方推荐)

Laravel 8.x 及以上版本提供了 Pennant 包,是官方推荐的特性开关解决方案。

安装配置

composer require laravel/pennant
php artisan vendor:publish --provider="Laravel\Pennant\PennantServiceProvider"
php artisan migrate

定义特性

// app/Features/NewCheckout.php
use Laravel\Pennant\Contracts\Feature;
class NewCheckout implements Feature
{
    public function resolve(mixed $scope): mixed
    {
        // 基于用户分组
        if ($scope instanceof User) {
            return $scope->isBetaTester();
        }
        // 基于百分比
        return random_int(1, 100) <= 50;
    }
}

使用特性开关

// 检查特性是否开启
if (Feature::active('new-checkout')) {
    // 使用新结账流程
}
// 基于用户检查
if (Feature::for($user)->active('new-checkout')) {
    // 该用户可以使用新功能
}
// 在 Blade 模板中使用
@feature('new-checkout')
    <div>新结账界面</div>
@endfeature

基于数据库的特性开关

创建表结构和模型

// 迁移文件
Schema::create('feature_flags', function (Blueprint $table) {
    $table->id();
    $table->string('name')->unique();
    $table->boolean('enabled')->default(false);
    $table->json('conditions')->nullable();
    $table->timestamps();
});
// FeatureFlag 模型
class FeatureFlag extends Model
{
    protected $fillable = ['name', 'enabled', 'conditions'];
    protected $casts = [
        'enabled' => 'boolean',
        'conditions' => 'array'
    ];
}

辅助类

namespace App\Services;
class FeatureFlagService
{
    private static $cache = [];
    public static function isEnabled(string $feature, $user = null): bool
    {
        $flag = self::getFlag($feature);
        if (!$flag || !$flag->enabled) {
            return false;
        }
        // 检查用户条件
        if ($user && $flag->conditions) {
            return self::checkConditions($flag->conditions, $user);
        }
        return true;
    }
    private static function getFlag(string $feature): ?FeatureFlag
    {
        if (!isset(self::$cache[$feature])) {
            self::$cache[$feature] = FeatureFlag::where('name', $feature)->first();
        }
        return self::$cache[$feature];
    }
    private static function checkConditions(array $conditions, $user): bool
    {
        foreach ($conditions as $type => $value) {
            switch ($type) {
                case 'user_ids':
                    if (in_array($user->id, $value)) return true;
                    break;
                case 'roles':
                    if ($user->hasRole($value)) return true;
                    break;
                case 'percentage':
                    if (crc32($user->email) % 100 < $value) return true;
                    break;
            }
        }
        return false;
    }
}

基于配置文件的方式

配置文件

// config/features.php
return [
    'new_checkout' => [
        'enabled' => env('FEATURE_NEW_CHECKOUT', false),
        'users' => [], // 允许的用户邮箱
        'roles' => ['admin', 'beta_tester'],
        'percentage' => 50, // 50%的用户
    ],
    'dark_mode' => [
        'enabled' => true,
        'users' => [],
        'roles' => [],
        'percentage' => 100,
    ],
];

服务类

namespace App\Services;
use Illuminate\Support\Facades\Cache;
class FeatureService
{
    public static function isActive(string $feature, $user = null): bool
    {
        $config = config("features.{$feature}");
        if (!$config || !$config['enabled']) {
            return false;
        }
        if (!$user) {
            return $config['enabled'];
        }
        // 检查用户白名单
        if (!empty($config['users'])) {
            if (in_array($user->email, $config['users'])) {
                return true;
            }
        }
        // 检查角色
        if (!empty($config['roles'])) {
            foreach ($config['roles'] as $role) {
                if ($user->hasRole($role)) {
                    return true;
                }
            }
        }
        // 百分比控制
        if (!empty($config['percentage'])) {
            $hash = crc32($user->email . $feature);
            return ($hash % 100) < $config['percentage'];
        }
        return true;
    }
    public static function all(): array
    {
        $features = config('features');
        $result = [];
        foreach ($features as $name => $config) {
            $result[$name] = [
                'enabled' => $config['enabled'],
                'active' => self::isActive($name),
            ];
        }
        return $result;
    }
}

使用环境变量(简单场景)

// .env 或 .env.development
FEATURE_NEW_CHECKOUT=true
FEATURE_DARK_MODE=false
FEATURE_ANALYTICS_V2=true
// 使用
if (env('FEATURE_NEW_CHECKOUT', false)) {
    // 新功能代码
}

自定义中间件

namespace App\Http\Middleware;
use Closure;
use App\Services\FeatureService;
class FeatureFlag
{
    public function handle($request, Closure $next, $feature)
    {
        if (!FeatureService::isActive($feature, $request->user())) {
            abort(404); // 或返回其他响应
        }
        return $next($request);
    }
}
// 在路由中使用
Route::middleware(['feature:new_checkout'])->group(function () {
    Route::get('/checkout/new', 'CheckoutController@new');
});

Blade 指令

// AppServiceProvider 中注册
use Illuminate\Support\Facades\Blade;
use App\Services\FeatureService;
Blade::if('feature', function ($feature) {
    return FeatureService::isActive($feature, auth()->user());
});
// 在模板中使用
@feature('new_checkout')
    <!-- 新结账界面 -->
@else
    <!-- 原有结账界面 -->
@endfeature

最佳实践建议

  1. 命名规范:使用清晰的命名约定,如 feature-namefeature_name

  2. 清理策略:定期清理不再需要的特性开关

  3. 监控和日志:记录特性使用情况

  4. 分级策略:从内测到小范围,再到全量发布

  5. 缓存:对不频繁变化的特性开关进行缓存

// 缓存示例
public static function isActive(string $feature, $user = null): bool
{
    return Cache::remember("feature.{$feature}." . ($user->id ?? 'global'), 3600, function () use ($feature, $user) {
        return self::checkFeature($feature, $user);
    });
}

根据项目复杂度选择合适的实现方式,简单项目用配置或环境变量,复杂项目建议使用 Pennant 或数据库方案。

抱歉,评论功能暂时关闭!