Skip to content

Laravel Pennant

简介

Laravel Pennant 是一个简单轻量的功能标志包 - 没有多余的累赘。功能标志使您能够自信地逐步推出新应用程序功能、对新界面设计进行 A/B 测试、补充基于主干的开发策略等。

安装

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

shell
composer require laravel/pennant

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

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

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

shell
php artisan migrate

配置

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

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

定义功能

要定义功能,您可以使用 Feature 门面提供的 define 方法。您需要为功能提供名称,以及将被调用以解析功能初始值的闭包。

通常,功能是使用 Feature 门面在服务提供者中定义的。闭包将接收功能检查的「作用域」。最常见的情况是,作用域是当前经过身份验证的用户。在此示例中,我们将定义一个功能,用于向应用程序用户逐步推出新 API:

php
<?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 目录中:

shell
php artisan pennant:feature NewApi

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

php
<?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 方法:

php
use Illuminate\Support\Facades\Feature;

$instance = Feature::instance(NewApi::class);

NOTE

功能类通过 容器 解析,因此您可以在需要时将依赖项注入功能类的构造函数中。

自定义存储的功能名称

默认情况下,Pennant 将存储功能类的完全限定类名。如果您希望将存储的功能名称与应用程序的内部结构解耦,可以在功能类上添加 Name 属性。此属性的值将代替类名存储:

php
<?php

namespace App\Features;

use Laravel\Pennant\Attributes\Name;

#[Name('new-api')]
class NewApi
{
    // ...
}

检查功能

要确定功能是否处于活动状态,可以使用 Feature 门面上的 active 方法。默认情况下,功能是针对当前经过身份验证的用户检查的:

php
<?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 方法:

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

Pennant 还提供了一些额外的便捷方法,在确定功能是否处于活动状态时可能很有用:

php
// 确定所有给定功能是否处于活动状态...
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']);

NOTE

在 HTTP 上下文之外使用 Pennant 时,例如在 Artisan 命令或队列作业中,您通常应该 显式指定功能的作用域。或者,您可以定义一个 默认作用域,该作用域同时考虑经过身份验证的 HTTP 上下文和未经身份验证的上下文。

检查基于类的功能

对于基于类的功能,您应该在检查功能时提供类名:

php
<?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
<?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 方法相反,如果功能处于非活动状态,则执行第一个闭包:

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

HasFeatures Trait

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

php
<?php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Laravel\Pennant\Concerns\HasFeatures;

class User extends Authenticatable
{
    use HasFeatures;

    // ...
}

将 trait 添加到模型后,您可以通过调用 features 方法轻松检查功能:

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

当然,features 方法提供了对许多其他与功能交互的便捷方法的访问:

php
// 值...
$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@featureany 指令:

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

@featureany(['site-redesign', 'beta'])
    <!-- 'site-redesign' 或 `beta` 处于活动状态 -->
@endfeatureany

中间件

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

php
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 方法中调用:

php
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
<?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
<?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 方法:

php
Feature::flushCache();

作用域

指定作用域

如前所述,功能通常是针对当前经过身份验证的用户检查的。但是,这可能并不总是适合您的需求。因此,可以通过 Feature 门面的 for 方法指定您希望检查给定功能的作用域:

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

当然,功能作用域不限于「用户」。想象一下,您构建了一个新的计费体验,正在向整个团队而不是单个用户推出。也许您希望最老的团队比新团队有更慢的推出速度。您的功能解析闭包可能如下所示:

php
use App\Models\Team;
use Illuminate\Support\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 方法:

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

// ...

默认作用域

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

php
<?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 方法显式提供作用域,功能检查现在将使用当前经过身份验证用户的团队作为默认作用域:

php
Feature::active('billing-v2');

// 现在等同于...

Feature::for($user->team)->active('billing-v2');

可空作用域

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

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

如果您不总是 显式指定功能作用域,则应确保作用域的类型是「可空的」,并在功能定义逻辑中处理 null 作用域值:

php
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
<?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 方法:

php
use Illuminate\Database\Eloquent\Relations\Relation;
use Laravel\Pennant\Feature;

Relation::enforceMorphMap([
    'post' => 'App\Models\Post',
    'video' => 'App\Models\Video',
]);

Feature::useMorphMap();

丰富功能值

到目前为止,我们主要展示的功能处于二进制状态,意味着它们要么「活动」要么「非活动」,但 Pennant 也允许您存储丰富值。

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

php
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 功能的值:

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

Pennant 包含的 Blade 指令也使根据功能的当前值有条件地渲染内容变得容易:

blade
@feature('purchase-button', 'blue-sapphire')
    <!-- 'blue-sapphire' 处于活动状态 -->
@elsefeature('purchase-button', 'seafoam-green')
    <!-- 'seafoam-green' 处于活动状态 -->
@elsefeature('purchase-button', 'tart-orange')
    <!-- 'tart-orange' 处于活动状态 -->
@endfeature

NOTE

使用丰富值时,重要的是要知道,当功能具有除 false 以外的任何值时,该功能被视为「活动」。

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

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

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

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

检索多个功能

values 方法允许检索给定作用域的多个功能:

php
Feature::values(['billing-v2', 'purchase-button']);

// [
//     'billing-v2' => false,
//     'purchase-button' => 'blue-sapphire',
// ]

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

php
Feature::all();

// [
//     'billing-v2' => false,
//     'purchase-button' => 'blue-sapphire',
//     'site-redesign' => true,
// ]

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

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

php
<?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 方法现在将在其结果中包含这些类,无论它们在当前请求期间是否已被检查:

php
Feature::all();

// [
//     'App\Features\NewApi' => true,
//     'billing-v2' => false,
//     'purchase-button' => 'blue-sapphire',
//     'site-redesign' => true,
// ]

预加载

虽然 Pennant 为单个请求的所有已解析功能保留内存缓存,但仍可能遇到性能问题。为了缓解这种情况,Pennant 提供了预加载功能值的能力。

为了说明这一点,想象我们正在循环中检查功能是否处于活动状态:

php
use Laravel\Pennant\Feature;

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

假设我们使用数据库驱动,此代码将为循环中的每个用户执行数据库查询 - 可能执行数百个查询。但是,使用 Pennant 的 load 方法,我们可以通过预加载用户或作用域集合的功能值来消除此潜在性能瓶颈:

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

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

要仅在尚未加载时加载功能值,可以使用 loadMissing 方法:

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

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

php
Feature::for($users)->loadAll();

更新值

当第一次解析功能的值时,底层驱动程序会将结果存储在存储中。这通常是确保用户在请求间获得一致体验所必需的。但是,有时您可能希望手动更新功能的存储值。

为此,您可以使用 activatedeactivate 方法来切换功能「开」或「关」:

php
use Laravel\Pennant\Feature;

// 为默认作用域激活功能...
Feature::activate('new-api');

// 为给定作用域停用功能...
Feature::for($user->team)->deactivate('billing-v2');

也可以通过向 activate 方法提供第二个参数来手动设置功能的丰富值:

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

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

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

批量更新

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

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

php
use Laravel\Pennant\Feature;

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

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

或者,您可以为所有用户停用功能:

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

NOTE

这只会更新 Pennant 存储驱动存储的已解析功能值。您还需要更新应用程序中的功能定义。

清除功能

有时,从存储中清除整个功能可能很有用。如果您已从应用程序中删除功能或对功能定义进行了调整并希望向所有用户推出,这通常是必要的。

您可以使用 purge 方法删除功能的所有存储值:

php
// 清除单个功能...
Feature::purge('new-api');

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

如果您希望从存储中清除 所有 功能,可以不带任何参数调用 purge 方法:

php
Feature::purge();

由于作为应用程序部署管道的一部分清除功能可能很有用,Pennant 包含一个 pennant:purge Artisan 命令,它将从存储中清除提供的功能:

shell
php artisan pennant:purge new-api

php artisan pennant:purge new-api purchase-button

也可以清除除给定功能列表中的功能以外的所有功能。例如,想象您想清除所有功能但保留存储中「new-api」和「purchase-button」功能的值。为此,您可以将这些功能名称传递给 --except 选项:

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

为方便起见,pennant:purge 命令还支持 --except-registered 标志。此标志表示应清除除在服务提供者中显式注册的功能以外的所有功能:

shell
php artisan pennant:purge --except-registered

测试

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

php
use Illuminate\Support\Arr;
use Laravel\Pennant\Feature;

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

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

php
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');
});
php
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'));
}

同样的方法也可用于基于类的功能:

php
use Laravel\Pennant\Feature;

test('it can control feature values', function () {
    Feature::define(NewApi::class, true);

    expect(Feature::value(NewApi::class))->toBeTrue();
});
php
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
<?xml version="1.0" encoding="UTF-8"?>
<phpunit colors="true">
    <!-- ... -->
    <php>
        <env name="PENNANT_STORE" value="array"/>
        <!-- ... -->
</phpunit>