laravel
10. 软件包
Laravel Pennant

介绍

Laravel Pennant (opens in a new tab) 是一个简单且轻量级的特性标志包 - 没有多余的东西。特性标志使您能够有信心地逐步推出新的应用程序特性,进行 A/B 测试新的界面设计,补充基于主干的开发策略等等。

安装

首先,使用 Composer 包管理器将 Pennant 安装到您的项目中:

composer require laravel/pennant

接下来,您应该使用 vendor:publish Artisan 命令发布 Pennant 的配置和迁移文件:

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

最后,您应该运行应用程序的数据库迁移。这将创建一个 features 表,Pennant 使用它来为其 database 驱动程序提供支持:

php artisan migrate

配置

发布 Pennant 的资产后,其配置文件将位于 config/pennant.php。此配置文件允许您指定 Pennant 用于存储已解析的特性标志值的默认存储机制。

Pennant 包括通过 array 驱动程序将已解析的特性标志值存储在内存数组中的支持。或者,Pennant 可以通过 database 驱动程序将已解析的特性标志值持久地存储在关系数据库中,这是 Pennant 使用的默认存储机制。

定义特性

要定义一个特性,您可以使用 Feature 外观提供的 define 方法。您需要为特性提供一个名称,以及一个闭包,该闭包将被调用来解析特性的初始值。

通常,特性是在服务提供程序中使用 Feature 外观来定义的。闭包将接收特性检查的“范围”。最常见的情况是,范围是当前经过身份验证的用户。在这个示例中,我们将为逐步向应用程序的用户推出新的 API 定义一个特性:

<?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('new-api', fn (User $user) => match (true) {
            $user->isInternalTeamMember() => true,
            $user->isHighTrafficCustomer() => false,
            default => Lottery::odds(1 / 100),
        });
    }
}

如您所见,我们的特性有以下规则:

  • 所有内部团队成员都应该使用新的 API。
  • 任何高流量客户都不应该使用新的 API。
  • 否则,该特性将以 1 / 100 的概率随机分配给用户并处于激活状态。

第一次为给定用户检查 new-api 特性时,闭包的结果将由存储驱动程序存储。下次针对同一用户检查该特性时,将从存储中检索该值,并且不会调用闭包。

为了方便起见,如果特性定义只返回一个彩票,您可以完全省略闭包:

Feature::define('site-redesign', Lottery::odds(1, 1000));

基于类的特性

Pennant 还允许您定义基于类的特性。与基于闭包的特性定义不同,无需在服务提供程序中注册基于类的特性。要创建基于类的特性,您可以调用 pennant:feature Artisan 命令。默认情况下,特性类将放置在您的应用程序的 app/Features 目录中:

php artisan pennant:feature NewApi

在编写特性类时,您只需要定义一个 resolve 方法,该方法将被调用来为给定范围解析特性的初始值。同样,范围通常是当前经过身份验证的用户:

<?php
 
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,
            $user->isHighTrafficCustomer() => false,
            default => Lottery::odds(1 / 100),
        };
    }
}

如果您想手动解析基于类的特性的实例,可以在 Feature 外观上调用 instance 方法:

use Illuminate\Support\Facades\Feature;
 
$instance = Feature::instance(NewApi::class);

[!注意]
特性类是通过 容器 解析的,因此在需要时,您可以将依赖项注入到特性类的构造函数中。

自定义存储的特性名称

默认情况下,Pennant 将存储特性类的完全限定类名。如果您想将存储的特性名称与应用程序的内部结构解耦,可以在特性类上指定一个 $name 属性。该属性的值将被存储以代替类名:

<?php
 
namespace App\Features;
 
class NewApi
{
    /**
     * 特性的存储名称。
     *
     * @var string
     */
    public $name = 'new-api';
 
    //...
}

检查特性

要确定一个特性是否处于激活状态,您可以使用 Feature 外观上的 active 方法。默认情况下,特性是针对当前经过身份验证的用户进行检查的:

<?php
 
namespace App\Http\Controllers;
 
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Laravel\Pennant\Feature;
 
class PodcastController
{
    /**
     * 显示资源的列表。
     */
    public function index(Request $request): Response
    {
        return Feature::active('new-api')
               ? $this->resolveNewApiResponse($request)
                : $this->resolveLegacyApiResponse($request);
    }
 
    //...
}

虽然默认情况下特性是针对当前经过身份验证的用户进行检查的,但您可以很容易地针对其他用户或 范围 检查特性。要实现这一点,可以使用 Feature 外观提供的 for 方法:

return Feature::for($user)->active('new-api')
       ? $this->resolveNewApiResponse($request)
        : $this->resolveLegacyApiResponse($request);

Pennant 还提供了一些其他方便的方法,在确定一个特性是否处于激活状态时可能会很有用:

// 确定给定的所有特性是否都处于激活状态...
Feature::allAreActive(['new-api', 'site-redesign']);
 
// 确定给定的特性中是否有任何一个处于激活状态...
Feature::someAreActive(['new-api', 'site-redesign']);
 
// 确定一个特性是否处于非激活状态...
Feature::inactive('new-api');
 
// 确定给定的所有特性是否都处于非激活状态...
Feature::allAreInactive(['new-api', 'site-redesign']);
 
// 确定给定的特性中是否有任何一个处于非激活状态...
Feature::someAreInactive(['new-api', 'site-redesign']);

[!注意]
在 HTTP 上下文之外使用 Pennant 时,例如在 Artisan 命令或排队作业中,您通常应该 明确指定特性的范围。或者,您可以定义一个 默认范围,以考虑经过身份验证的 HTTP 上下文和未经过身份验证的上下文。

检查基于类的特性

对于基于类的特性,在检查特性时应提供类名:

<?php
 
namespace App\Http\Controllers;
 
use App\Features\NewApi;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Laravel\Pennant\Feature;
 
class PodcastController
{
    /**
     * 显示资源的列表。
     */
    public function index(Request $request): Response
    {
        return Feature::active(NewApi::class)
               ? $this->resolveNewApiResponse($request)
                : $this->resolveLegacyApiResponse($request);
    }
 
    //...
}

条件执行

when 方法可用于在特性处于激活状态时流畅地执行给定的闭包。此外,可以提供第二个闭包,如果特性处于非激活状态,则将执行该闭包:

<?php

namespace App\Http\Controllers;

use App\Features\NewApi;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Laravel\Pennant\Feature;

class PodcastController
{
    /**
     * 显示资源的列表。
     */
    public function index(Request $request): Response
    {
        return Feature::when(NewApi::class,
            fn () => $this->resolveNewApiResponse($request),
            fn () => $this->resolveLegacyApiResponse($request),
        );
    }

    //...
}

unless 方法与 when 方法相反,如果特性处于非激活状态,则执行第一个闭包:

return Feature::unless(NewApi::class,
    fn () => $this->resolveLegacyApiResponse($request),
    fn () => $this->resolveNewApiResponse($request),
);

HasFeatures 特征

Pennant 的 HasFeatures 特征可以添加到您的应用程序的 User 模型(或任何其他具有特性的模型)中,以提供一种流畅、方便的方式直接从模型中检查特性:

<?php
 
namespace App\Models;
 
use Illuminate\Foundation\Auth\User as Authenticatable;
use Laravel\Pennant\Concerns\HasFeatures;
 
class User extends Authenticatable
{
    use HasFeatures;
 
    //...
}

将特征添加到模型后,您可以通过调用 features 方法轻松地检查特性:

if ($user->features()->active('new-api')) {
    //...
}

当然,features 方法提供了许多其他方便的方法来与特性进行交互:

// 值...
$value = $user->features()->value('purchase-button')
$values = $user->features()->values(['new-api', 'purchase-button']);
 
// 状态...
$user->features()->active('new-api');
$user->features()->allAreActive(['new-api', 'server-api']);
$user->features()->someAreActive(['new-api', 'server-api']);
 
$user->features()->inactive('new-api');
$user->features()->allAreInactive(['new-api', 'server-api']);
$user->features()->someAreInactive(['new-api', 'server-api']);
 
// 条件执行...
$user->features()->when('new-api',
    fn () => /*... */,
    fn () => /*... */,
);
 
$user->features()->unless('new-api',
    fn () => /*... */,
    fn () => /*... */,
);

Blade 指令

为了使在 Blade 中检查特性成为一种无缝的体验,Pennant 提供了一个 @feature 指令:

@feature('site-redesign')
    <!-- 'site-redesign' 处于激活状态 -->
@else
    <!-- 'site-redesign' 处于非激活状态 -->
@endfeature

中间件

Pennant 还包括一个 中间件,可用于在甚至调用路由之前验证当前经过身份验证的用户是否有权访问特性。您可以将中间件分配给路由,并指定访问该路由所需的特性。如果当前经过身份验证的用户的任何指定特性处于非激活状态,则该路由将返回 400 Bad Request HTTP 响应。可以将多个特性传递给静态 using 方法。

use Illuminate\Support\Facades\Route;
use Laravel\Pennant\Middleware\EnsureFeaturesAreActive;
 
Route::get('/api/servers', function () {
    //...
})->middleware(EnsureFeaturesAreActive::using('new-api', 'servers-api'));

自定义响应

如果您希望自定义当列出的功能之一处于非活动状态时中间件返回的响应,可以使用 EnsureFeaturesAreActive 中间件提供的 whenInactive 方法。通常,此方法应在您的应用程序的服务提供者的 boot 方法中调用:

use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Laravel\Pennant\Middleware\EnsureFeaturesAreActive;
 
/**
 * 引导任何应用程序服务。
 */
public function boot(): void
{
    EnsureFeaturesAreActive::whenInactive(
        function (Request $request, array $features) {
            return new Response(status: 403);
        }
    );
 
    //...
}

拦截功能检查

有时,在检索给定功能的存储值之前执行一些内存中检查会很有用。想象一下,您正在功能标志后面开发一个新的 API,并希望能够禁用新的 API,而不会丢失存储中任何已解析的功能值。如果您在新 API 中发现一个错误,您可以轻松地为除内部团队成员之外的所有人禁用它,修复错误,然后为之前有权访问该功能的用户重新启用新 API。

您可以使用基于类的功能的 before 方法来实现此目的。当存在时,before 方法总是在从存储中检索值之前在内存中运行。如果从该方法返回一个非 null 值,它将在请求期间代替功能的存储值使用:

<?php
 
namespace App\Features;
 
use App\Models\User;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Lottery;
 
class NewApi
{
    /**
     * 在检索存储值之前运行始终在内存中的检查。
     */
    public function before(User $user): mixed
    {
        if (Config::get('features.new-api.disabled')) {
            return $user->isInternalTeamMember();
        }
    }
 
    /**
     * 解析功能的初始值。
     */
    public function resolve(User $user): mixed
    {
        return match (true) {
            $user->isInternalTeamMember() => true,
            $user->isHighTrafficCustomer() => false,
            default => Lottery::odds(1 / 100),
        };
    }
}

您还可以使用此功能来安排以前在功能标志后面的功能的全局推出:

<?php
 
namespace App\Features;
 
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Config;
 
class NewApi
{
    /**
     * 在检索存储值之前运行始终在内存中的检查。
     */
    public function before(User $user): mixed
    {
        if (Config::get('features.new-api.disabled')) {
            return $user->isInternalTeamMember();
        }
 
        if (Carbon::parse(Config::get('features.new-api.rollout-date'))->isPast()) {
            return true;
        }
    }
 
    //...
}

内存缓存

在检查功能时,Pennant 会创建结果的内存缓存。如果您使用的是 database 驱动程序,这意味着在单个请求中重新检查相同的功能标志不会触发额外的数据库查询。这也确保了功能在请求期间具有一致的结果。

如果您需要手动刷新内存缓存,可以使用 Feature 外观提供的 flushCache 方法:

Feature::flushCache();

范围

指定范围

如前所述,功能通常是针对当前经过身份验证的用户进行检查的。然而,这可能并不总是满足您的需求。因此,可以通过 Feature 外观的 for 方法指定您想要检查给定功能的范围:

return Feature::for($user)->active('new-api')
       ? $this->resolveNewApiResponse($request)
        : $this->resolveLegacyApiResponse($request);

当然,功能范围不限于“用户”。想象一下,您已经构建了一个新的计费体验,您正在将其推广到整个团队而不是单个用户。也许您希望最老的团队比新团队的推出速度更慢。您的功能解析闭包可能如下所示:

use App\Models\Team;
use Carbon\Carbon;
use Illuminate\Support\Lottery;
use Laravel\Pennant\Feature;
 
Feature::define('billing-v2', function (Team $team) {
    if ($team->created_at->isAfter(new Carbon('1st Jan, 2023'))) {
        return true;
    }
 
    if ($team->created_at->isAfter(new Carbon('1st Jan, 2019'))) {
        return Lottery::odds(1 / 100);
    }
 
    return Lottery::odds(1 / 1000);
});

您会注意到,我们定义的闭包不是期望一个 User,而是期望一个 Team 模型。要确定此功能是否对用户的团队有效,您应该将团队传递给 Feature 外观提供的 for 方法:

if (Feature::for($user->team)->active('billing-v2')) {
    return redirect('/billing/v2');
}
 
//...

默认范围

还可以自定义 Pennant 用于检查功能的默认范围。例如,也许您的所有功能都是针对当前经过身份验证的用户的团队而不是用户进行检查的。而不是每次检查功能时都调用 Feature::for($user->team),您可以将团队指定为默认范围。通常,这应该在您的应用程序的服务提供者中完成:

<?php
 
namespace App\Providers;
 
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\ServiceProvider;
use Laravel\Pennant\Feature;
 
class AppServiceProvider extends ServiceProvider
{
    /**
     * 引导任何应用程序服务。
     */
    public function boot(): void
    {
        Feature::resolveScopeUsing(fn ($driver) => Auth::user()?->team);
 
        //...
    }
}

如果没有通过 for 方法明确提供范围,则功能检查现在将使用当前经过身份验证的用户的团队作为默认范围:

Feature::active('billing-v2');
 
// 现在等同于...
 
Feature::for($user->team)->active('billing-v2');

可为空的范围

如果您在检查功能时提供的范围为 null,并且功能的定义不通过可为空类型或在联合类型中包含 null 来支持 null,则 Pennant 将自动将 false 作为功能的结果值返回。

因此,如果您传递给功能的范围可能为 null,并且您希望调用功能的值解析器,则应在功能的定义中考虑到这一点。如果您在 Artisan 命令、排队作业或未经过身份验证的路由中检查功能,则可能会出现 null 范围。由于在这些上下文中通常没有经过身份验证的用户,因此默认范围将为 null

如果您不总是明确指定您的功能范围,那么您应该确保范围的类型是“可为空的”,并在您的功能定义逻辑中处理 null 范围值:

use App\Models\User;
use Illuminate\Support\Lottery;
use Laravel\Pennant\Feature;
 
Feature::define('new-api', fn (User $user) => match (true) {// [tl! remove]
Feature::define('new-api', fn (User|null $user) => match (true) {// [tl! add]
    $user === null => true,// [tl! add]
    $user->isInternalTeamMember() => true,
    $user->isHighTrafficCustomer() => false,
    default => Lottery::odds(1 / 100),
});

识别范围

Pennant 的内置 arraydatabase 存储驱动程序知道如何正确地为所有 PHP 数据类型以及 Eloquent 模型存储范围标识符。然而,如果您的应用程序使用第三方 Pennant 驱动程序,该驱动程序可能不知道如何正确地为您的应用程序中的 Eloquent 模型或其他自定义类型存储标识符。

鉴于此,Pennant 允许您通过在您的应用程序中用作 Pennant 范围的对象上实现 FeatureScopeable 契约来格式化范围值以进行存储。

例如,想象一下您在单个应用程序中使用两个不同的功能驱动程序:内置的 database 驱动程序和第三方的“Flag Rocket”驱动程序。“Flag Rocket”驱动程序不知道如何正确地存储 Eloquent 模型。相反,它需要一个 FlagRocketUser 实例。通过实现 FeatureScopeable 契约定义的 toFeatureIdentifier,我们可以自定义提供给我们的应用程序使用的每个驱动程序的可存储范围值:

<?php
 
namespace App\Models;
 
use FlagRocket\FlagRocketUser;
use Illuminate\Database\Eloquent\Model;
use Laravel\Pennant\Contracts\FeatureScopeable;
 
class User extends Model implements FeatureScopeable
{
    /**
     * 将对象转换为给定驱动程序的功能范围标识符。
     */
    public function toFeatureIdentifier(string $driver): mixed
    {
        return match($driver) {
            'database' => $this,
            'flag-rocket' => FlagRocketUser::fromId($this->flag_rocket_id),
        };
    }
}

序列化范围

默认情况下,Pennant 在存储与 Eloquent 模型相关的功能时将使用完全限定的类名。如果您已经在使用Eloquent 多态映射,您可以选择让 Pennant 也使用多态映射来将存储的功能与您的应用程序结构解耦。

要实现此目的,在服务提供者中定义您的 Eloquent 多态映射后,您可以调用 Feature 外观的 useMorphMap 方法:

use Illuminate\Database\Eloquent\Relations\Relation;
use Laravel\Pennant\Feature;
 
Relation::enforceMorphMap([
    'post' => 'App\Models\Post',
    'video' => 'App\Models\Video',
]);
 
Feature::useMorphMap();

丰富的功能值

到目前为止,我们主要将功能显示为处于二元状态,这意味着它们要么“活动”要么“非活动”,但 Pennant 还允许您存储丰富的值。

例如,想象一下您正在为您的应用程序的“立即购买”按钮测试三种新颜色。您可以从功能定义中返回一个字符串,而不是返回 truefalse

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

您可以使用 value 方法检索 purchase-button 功能的值:

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

Pennant 包含的 Blade 指令还可以根据功能的当前值轻松有条件地呈现内容:

@feature('purchase-button', 'blue-sapphire')
    <!-- 'blue-sapphire' 是活动的 -->
@elsefeature('purchase-button', 'seafoam-green')
    <!-- 'seafoam-green' 是活动的 -->
@elsefeature('purchase-button', 'tart-orange')
    <!-- 'tart-orange' 是活动的 -->
@endfeature

[!注意]
当使用丰富的值时,重要的是要知道,当功能具有除 false 以外的任何值时,该功能被认为是“活动的”。

当调用条件 when方法时,功能的丰富值将提供给第一个闭包:

Feature::when('purchase-button',
    fn ($color) => /*... */,
    fn () => /*... */,
);

同样,当调用条件 unless 方法时,功能的丰富值将提供给可选的第二个闭包:

Feature::unless('purchase-button',
    fn () => /*... */,
    fn ($color) => /*... */,
);

检索多个功能

values 方法允许为给定范围检索多个功能:

Feature::values(['billing-v2', 'purchase-button']);
 
// [
//     'billing-v2' => false,
//     'purchase-button' => 'blue-sapphire',
// ]

或者,您可以使用 all 方法检索给定范围的所有已定义功能的值:

Feature::all();
 
// [
//     'billing-v2' => false,
//     'purchase-button' => 'blue-sapphire',
//     'site-redesign' => true,
// ]

但是,基于类的功能是动态注册的,并且在 Pennant 明确检查它们之前是未知的。这意味着,如果在当前请求期间尚未检查您的应用程序的基于类的功能,则它们可能不会出现在 all 方法返回的结果中。

如果您希望在使用 all 方法时始终包含功能类,可以使用 Pennant 的功能发现功能。要开始使用,请在您的应用程序的服务提供者中调用 discover 方法:

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Laravel\Pennant\Feature;

class AppServiceProvider extends ServiceProvider
{
    /**
     * 引导任何应用程序服务。
     */
    public function boot(): void
    {
        Feature::discover();

        //...
    }
}

discover 方法将注册您的应用程序的 app/Features 目录中的所有功能类。现在,all 方法将在其结果中包含这些类,无论它们在当前请求期间是否已被检查:

Feature::all();
 
// [
//     'App\Features\NewApi' => true,
//     'billing-v2' => false,
//     'purchase-button' => 'blue-sapphire',
//     'site-redesign' => true,
// ]

预加载

尽管 Pennant 会为单个请求的所有已解析特性在内存中保留一个缓存,但仍有可能遇到性能问题。为了缓解此问题,Pennant 提供了预加载特性值的能力。

为了说明这一点,想象一下我们在一个循环中检查一个特性是否处于激活状态:

use Laravel\Pennant\Feature;
 
foreach ($users as $user) {
    if (Feature::for($user)->active('notifications-beta')) {
        $user->notify(new RegistrationSuccess);
    }
}

假设我们使用的是数据库驱动程序,这段代码将为循环中的每个用户执行一个数据库查询 - 可能会执行数百个查询。然而,使用 Pennant 的 load 方法,我们可以通过为用户或范围的集合预加载特性值来消除这个潜在的性能瓶颈:

Feature::for($users)->load(['notifications-beta']);
 
foreach ($users as $user) {
    if (Feature::for($user)->active('notifications-beta')) {
        $user->notify(new RegistrationSuccess);
    }
}

要仅在特性值尚未加载时加载它们,可以使用 loadMissing 方法:

Feature::for($users)->loadMissing([
    'new-api',
    'purchase-button',
    'notifications-beta',
]);

您可以使用 loadAll 方法加载所有已定义的特性:

Feature::for($user)->loadAll();

更新值

当首次解析一个特性的值时,底层驱动程序会将结果存储在存储中。这通常是为了确保您的用户在不同请求中获得一致的体验。然而,有时您可能想要手动更新特性的存储值。

要实现此目的,您可以使用 activatedeactivate 方法来切换特性的“开启”或“关闭”状态:

use Laravel\Pennant\Feature;
 
// 为默认范围激活特性...
Feature::activate('new-api');
 
// 为给定范围停用特性...
Feature::for($user->team)->deactivate('billing-v2');

还可以通过向 activate 方法提供第二个参数来为特性手动设置一个丰富的值:

Feature::activate('purchase-button', 'seafoam-green');

要指示 Pennant 忘记一个特性的存储值,可以使用 forget 方法。当再次检查该特性时,Pennant 将从其特性定义中解析该特性的值:

Feature::forget('purchase-button');

批量更新

要批量更新存储的特性值,可以使用 activateForEveryonedeactivateForEveryone 方法。

例如,假设您现在对 new-api 特性的稳定性有信心,并且已经为您的结账流程确定了最佳的 'purchase-button' 颜色 - 您可以相应地为所有用户更新存储值:

use Laravel\Pennant\Feature;
 
Feature::activateForEveryone('new-api');
 
Feature::activateForEveryone('purchase-button', 'seafoam-green');

或者,您可以为所有用户停用该特性:

Feature::deactivateForEveryone('new-api');

[!NOTE]
这只会更新 Pennant 的存储驱动程序已存储的已解析特性值。您还需要在应用程序中更新特性定义。

清除特性

有时,从存储中清除整个特性可能会很有用。如果您已从应用程序中删除该特性,或者您对特性的定义进行了调整并希望将其推广到所有用户,那么通常需要这样做。

您可以使用 purge 方法删除一个特性的所有存储值:

// 清除单个特性...
Feature::purge('new-api');
 
// 清除多个特性...
Feature::purge(['new-api', 'purchase-button']);

如果您想从存储中清除 所有 特性,可以不带任何参数调用 purge 方法:

Feature::purge();

由于在应用程序的部署管道中清除特性可能会很有用,Pennant 包含一个 pennant:purge Artisan 命令,该命令将从存储中清除提供的特性:

php artisan pennant:purge new-api
 
php artisan pennant:purge new-api purchase-button

也可以清除除给定特性列表中的所有特性。例如,假设您想要清除所有特性,但保留 "new-api" 和 "purchase-button" 特性的值在存储中。要实现此目的,您可以将这些特性名称传递给 --except 选项:

php artisan pennant:purge --except=new-api --except=purchase-button

为了方便起见,pennant:purge 命令还支持 --except-registered 标志。此标志表示应清除除在服务提供者中明确注册的特性之外的所有特性:

php artisan pennant:purge --except-registered

测试

当测试与特性标志交互的代码时,在测试中控制特性标志返回值的最简单方法是简单地重新定义该特性。例如,假设您在应用程序的一个服务提供者中有以下特性定义:

use Illuminate\Support\Arr;
use Laravel\Pennant\Feature;
 
Feature::define('purchase-button', fn () => Arr::random([
    'blue-sapphire',
    'seafoam-green',
    'tart-orange',
]));

要在测试中修改特性的返回值,可以在测试开始时重新定义该特性。以下测试将始终通过,即使 Arr::random() 实现仍然存在于服务提供者中:

use Laravel\Pennant\Feature;
 
test('it can control feature values', function () {
    Feature::define('purchase-button','seafoam-green');
 
    expect(Feature::value('purchase-button'))->toBe('seafoam-green');
});
use Laravel\Pennant\Feature;
 
public function test_it_can_control_feature_values()
{
    Feature::define('purchase-button','seafoam-green');
 
    $this->assertSame('seafoam-green', Feature::value('purchase-button'));
}

对于基于类的特性,也可以使用相同的方法:

use Laravel\Pennant\Feature;
 
test('it can control feature values', function () {
    Feature::define(NewApi::class, true);
 
    expect(Feature::value(NewApi::class))->toBeTrue();
});
use App\Features\NewApi;
use Laravel\Pennant\Feature;
 
public function test_it_can_control_feature_values()
{
    Feature::define(NewApi::class, true);
 
    $this->assertTrue(Feature::value(NewApi::class));
}

如果您的特性返回一个 Lottery 实例,有一些有用的 测试助手可用

存储配置

您可以通过在应用程序的 phpunit.xml 文件中定义 PENNANT_STORE 环境变量来配置 Pennant 在测试期间将使用的存储:

<?xml version="1.0" encoding="UTF-8"?>
<phpunit colors="true">
    <!--... -->
    <php>
        <env name="PENNANT_STORE" value="array"/>
        <!--... -->
    </php>
</phpunit>

添加自定义 Pennant 驱动程序

实现驱动程序

如果 Pennant 的现有存储驱动程序都不符合您的应用程序需求,您可以编写自己的存储驱动程序。您的自定义驱动程序应该实现 Laravel\Pennant\Contracts\Driver 接口:

<?php
 
namespace App\Extensions;
 
use Laravel\Pennant\Contracts\Driver;
 
class RedisFeatureDriver implements Driver
{
    public function define(string $feature, callable $resolver): void {}
    public function defined(): array {}
    public function getAll(array $features): array {}
    public function get(string $feature, mixed $scope): mixed {}
    public function set(string $feature, mixed $scope, mixed $value): void {}
    public function setForAllScopes(string $feature, mixed $value): void {}
    public function delete(string $feature, mixed $scope): void {}
    public function purge(array|null $features): void {}
}

现在,我们只需要使用 Redis 连接来实现这些方法。有关如何实现这些方法的示例,请查看 Pennant 源代码 (opens in a new tab) 中的 Laravel\Pennant\Drivers\DatabaseDriver

[!NOTE]
Laravel 不会附带一个目录来包含您的扩展。您可以将它们放在您喜欢的任何地方。在这个例子中,我们创建了一个 Extensions 目录来容纳 RedisFeatureDriver

注册驱动程序

一旦您的驱动程序实现完成,您就可以准备将其注册到 Laravel 中。要向 Pennant 添加其他驱动程序,您可以使用 Feature 外观提供的 extend 方法。您应该从应用程序的 服务提供者boot 方法中调用 extend 方法:

<?php
 
namespace App\Providers;
 
use App\Extensions\RedisFeatureDriver;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Support\ServiceProvider;
use Laravel\Pennant\Feature;
 
class AppServiceProvider extends ServiceProvider
{
    /**
     * 注册任何应用程序服务。
     */
    public function register(): void
    {
        //...
    }
 
    /**
     * 引导任何应用程序服务。
     */
    public function boot(): void
    {
        Feature::extend('redis', function (Application $app) {
            return new RedisFeatureDriver($app->make('redis'), $app->make('events'), []);
        });
    }
}

一旦驱动程序注册完成,您可以在应用程序的 config/pennant.php 配置文件中使用 redis 驱动程序:

'stores' => [

    'redis' => [
        'driver' => 'redis',
        'connection' => null,
    ],

    //...

],

事件

Pennant 会分发各种事件,在整个应用程序中跟踪特性标志时可能会很有用。

Laravel\Pennant\Events\FeatureRetrieved

每当 检查特性 时,都会分发此事件。此事件对于针对整个应用程序中特性标志的使用情况创建和跟踪指标可能很有用。

Laravel\Pennant\Events\FeatureResolved

当首次为特定范围解析特性的值时,会分发此事件。

Laravel\Pennant\Events\UnknownFeatureResolved

当首次为特定范围解析未知特性时,会分发此事件。如果您打算删除一个特性标志,但不小心在整个应用程序中留下了对它的孤立引用,那么监听此事件可能会很有用:

<?php
 
namespace App\Providers;
 
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Log;
use Laravel\Pennant\Events\UnknownFeatureResolved;
 
class AppServiceProvider extends ServiceProvider
{
    /**
     * 引导任何应用程序服务。
     */
    public function boot(): void
    {
        Event::listen(function (UnknownFeatureResolved $event) {
            Log::error("正在解析未知特性 [{$event->feature}]。");
        });
    }
}

Laravel\Pennant\Events\DynamicallyRegisteringFeatureClass

当在请求期间首次动态检查 基于类的特性 时,会分发此事件。

Laravel\Pennant\Events\UnexpectedNullScopeEncountered

当将 null 范围传递给 不支持 null 的特性定义时,会分发此事件。

这种情况会被妥善处理,特性将返回 false。但是,如果您想要选择退出此特性的默认妥善行为,可以在应用程序的 AppServiceProviderboot 方法中为该事件注册一个监听器:

use Illuminate\Support\Facades\Log;
use Laravel\Pennant\Events\UnexpectedNullScopeEncountered;
 
/**
 * 引导任何应用程序服务。
 */
public function boot(): void
{
    Event::listen(UnexpectedNullScopeEncountered::class, fn () => abort(500));
}
 

Laravel\Pennant\Events\FeatureUpdated

当为范围更新特性时(通常通过调用 activatedeactivate),会分发此事件。

Laravel\Pennant\Events\FeatureUpdatedForAllScopes

当为所有范围更新特性时(通常通过调用 activateForEveryonedeactivateForEveryone),会分发此事件。

Laravel\Pennant\Events\FeatureDeleted

当为范围删除特性时(通常通过调用 forget),会分发此事件。

Laravel\Pennant\Events\FeaturesPurged

当清除特定特性时,会分发此事件。

Laravel\Pennant\Events\AllFeaturesPurged

当清除所有特性时,会分发此事件。