LaravelDocs(中文)

Pennant

Laravel Pennant 是一個輕量級的功能旗標套件

簡介 (Introduction)

Laravel Pennant 是一個簡單且輕量級的功能旗標套件——沒有多餘的累贅。功能旗標讓你可以自信地逐步推出新的應用程式功能、對新的介面設計進行 A/B 測試、輔助主幹開發策略 (Trunk-based development strategy) 等等。

安裝 (Installation)

首先,使用 Composer 套件管理器將 Pennant 安裝到你的專案中:

composer require laravel/pennant

接下來,你應該使用 vendor:publish Artisan 指令發布 Pennant 的設定檔和遷移檔案:

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

最後,你應該執行應用程式的資料庫遷移。這將建立一個 features 資料表,Pennant 使用該資料表來驅動其 database 驅動:

php artisan migrate

設定 (Configuration)

發布 Pennant 的資源後,其設定檔將位於 config/pennant.php。此設定檔允許你指定 Pennant 用來儲存已解析功能旗標值的預設儲存機制。

Pennant 支援透過 array 驅動將已解析的功能旗標值儲存在記憶體陣列中。或者,Pennant 可以透過 database 驅動將已解析的功能旗標值持久儲存在關聯式資料庫中,這是 Pennant 使用的預設儲存機制。

定義功能 (Defining Features)

要定義一個功能,你可以使用 Feature facade 提供的 define 方法。你需要提供功能的名稱,以及一個閉包 (Closure),該閉包將被呼叫來解析功能的初始值。

通常,功能是在服務提供者中使用 Feature facade 定義的。閉包將接收功能檢查的「範圍 (scope)」。最常見的情況下,範圍是當前經過驗證的使用者。在這個範例中,我們將定義一個功能,以便逐步向我們的應用程式使用者推出新的 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
{
    /**
     * Bootstrap any application services.
     */
    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 功能時,閉包的結果將由儲存驅動儲存。下次針對同一使用者檢查該功能時,將從儲存中檢索該值,並且不會呼叫閉包。

為了方便起見,如果功能定義僅返回一個樂透 (lottery),你可以完全省略閉包:

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

基於類別的功能 (Class Based Features)

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
{
    /**
     * Resolve the feature's initial value.
     */
    public function resolve(User $user): mixed
    {
        return match (true) {
            $user->isInternalTeamMember() => true,
            $user->isHighTrafficCustomer() => false,
            default => Lottery::odds(1 / 100),
        };
    }
}

如果你想手動解析基於類別的功能的實例,可以在 Feature facade 上呼叫 instance 方法:

use Illuminate\Support\Facades\Feature;

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

[!NOTE] 功能類別是透過 container 解析的,因此你可以在需要時將依賴項目注入到功能類別的建構函式中。

自訂儲存的功能名稱 (Customizing the Stored Feature Name)

預設情況下,Pennant 將儲存功能類別的完全限定類別名稱 (FQCN)。如果你想將儲存的功能名稱與應用程式的內部結構解耦,可以在功能類別上指定 $name 屬性。該屬性的值將被儲存,以代替類別名稱:

<?php

namespace App\Features;

class NewApi
{
    /**
     * The stored name of the feature.
     *
     * @var string
     */
    public $name = 'new-api';

    // ...
}

檢查功能 (Checking Features)

要確定某個功能是否啟用,可以使用 Feature facade 上的 active 方法。預設情況下,會針對當前經過驗證的使用者檢查功能:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Laravel\Pennant\Feature;

class PodcastController
{
    /**
     * Display a listing of the resource.
     */
    public function index(Request $request): Response
    {
        return Feature::active('new-api')
            ? $this->resolveNewApiResponse($request)
            : $this->resolveLegacyApiResponse($request);
    }

    // ...
}

雖然預設情況下是針對當前經過驗證的使用者檢查功能,但你可以輕鬆地針對其他使用者或範圍檢查功能。為此,請使用 Feature facade 提供的 for 方法:

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

Pennant 還提供了一些額外的便利方法,在確定功能是否啟用時可能會證明是有用的:

// Determine if all of the given features are active...
Feature::allAreActive(['new-api', 'site-redesign']);

// Determine if any of the given features are active...
Feature::someAreActive(['new-api', 'site-redesign']);

// Determine if a feature is inactive...
Feature::inactive('new-api');

// Determine if all of the given features are inactive...
Feature::allAreInactive(['new-api', 'site-redesign']);

// Determine if any of the given features are inactive...
Feature::someAreInactive(['new-api', 'site-redesign']);

[!NOTE] 當在 HTTP 上下文之外使用 Pennant 時,例如在 Artisan 指令或隊列任務中,通常應該顯式指定功能的範圍。或者,你可以定義一個預設範圍,該範圍同時考慮了經過驗證的 HTTP 上下文和未經驗證的上下文。

檢查基於類別的功能 (Checking Class Based Features)

對於基於類別的功能,你應該在檢查功能時提供類別名稱:

<?php

namespace App\Http\Controllers;

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

class PodcastController
{
    /**
     * Display a listing of the resource.
     */
    public function index(Request $request): Response
    {
        return Feature::active(NewApi::class)
            ? $this->resolveNewApiResponse($request)
            : $this->resolveLegacyApiResponse($request);
    }

    // ...
}

條件執行 (Conditional Execution)

when 方法可用於在功能啟用時流暢地執行給定的閉包。此外,還可以提供第二個閉包,如果功能未啟用,則執行該閉包:

<?php

namespace App\Http\Controllers;

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

class PodcastController
{
    /**
     * Display a listing of the resource.
     */
    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 Trait

Pennant 的 HasFeatures trait 可以添加到你的應用程式的 User 模型(或任何其他具有功能的模型)中,以提供一種流暢、方便的方式直接從模型檢查功能:

<?php

namespace App\Models;

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

class User extends Authenticatable
{
    use HasFeatures;

    // ...
}

一旦 trait 被添加到你的模型中,你可以透過呼叫 features 方法輕鬆檢查功能:

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

當然,features 方法提供了許多其他方便的方法來與功能互動:

// Values...
$value = $user->features()->value('purchase-button')
$values = $user->features()->values(['new-api', 'purchase-button']);

// State...
$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']);

// Conditional execution...
$user->features()->when('new-api',
    fn () => /* ... */,
    fn () => /* ... */,
);

$user->features()->unless('new-api',
    fn () => /* ... */,
    fn () => /* ... */,
);

Blade 指令 (Blade Directive)

為了讓在 Blade 中檢查功能成為無縫的體驗,Pennant 提供了 @feature@featureany 指令:

@feature('site-redesign')
    <!-- 'site-redesign' is active -->
@else
    <!-- 'site-redesign' is inactive -->
@endfeature

@featureany(['site-redesign', 'beta'])
    <!-- 'site-redesign' or `beta` is active -->
@endfeatureany

Middleware

Pennant 還包含一個 middleware,可用於在路由被呼叫之前驗證當前經過驗證的使用者是否有權存取某個功能。你可以將 middleware 分配給路由,並指定存取路由所需的功能。如果當前經過驗證的使用者的任何指定功能未啟用,路由將返回 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'));

自訂回應 (Customizing the Response)

如果你想自訂當列出的功能之一未啟用時 middleware 返回的回應,可以使用 EnsureFeaturesAreActive middleware 提供的 whenInactive 方法。通常,此方法應在你的應用程式的服務提供者之一的 boot 方法中呼叫:

use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Laravel\Pennant\Middleware\EnsureFeaturesAreActive;

/**
 * Bootstrap any application services.
 */
public function boot(): void
{
    EnsureFeaturesAreActive::whenInactive(
        function (Request $request, array $features) {
            return new Response(status: 403);
        }
    );

    // ...
}

攔截功能檢查 (Intercepting Feature Checks)

有時在檢索給定功能的儲存值之前執行一些記憶體檢查可能會很有用。想像一下,你正在開發一個隱藏在功能旗標後面的新 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
{
    /**
     * Run an always-in-memory check before the stored value is retrieved.
     */
    public function before(User $user): mixed
    {
        if (Config::get('features.new-api.disabled')) {
            return $user->isInternalTeamMember();
        }
    }

    /**
     * Resolve the feature's initial value.
     */
    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
{
    /**
     * Run an always-in-memory check before the stored value is retrieved.
     */
    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;
        }
    }

    // ...
}

記憶體快取 (In-Memory Cache)

檢查功能時,Pennant 會建立結果的記憶體快取。如果你使用 database 驅動,這意味著在單個請求中重新檢查相同的功能旗標不會觸發額外的資料庫查詢。這也確保了功能在請求期間具有一致的結果。

如果你需要手動清除記憶體快取,可以使用 Feature facade 提供的 flushCache 方法:

Feature::flushCache();

範圍 (Scope)

指定範圍 (Specifying the Scope)

如前所述,功能通常是針對當前經過驗證的使用者進行檢查的。然而,這可能並不總是符合你的需求。因此,可以透過 Feature facade 的 for 方法指定你想要檢查給定功能的範圍:

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

當然,功能範圍不限於「使用者」。想像一下,你建立了一個新的計費體驗,你正在向整個團隊而不是單個使用者推出。也許你希望最老的團隊比新團隊更慢地推出。你的功能解析閉包可能看起來像這樣:

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 facade 提供的 for 方法:

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

// ...

預設範圍 (Default Scope)

也可以自訂 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
{
    /**
     * Bootstrap any application services.
     */
    public function boot(): void
    {
        Feature::resolveScopeUsing(fn ($driver) => Auth::user()?->team);

        // ...
    }
}

如果沒有透過 for 方法顯式提供範圍,功能檢查現在將使用當前經過驗證的使用者的團隊作為預設範圍:

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

// Is now equivalent to...

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

可為空的範圍 (Nullable Scope)

如果你在檢查功能時提供的範圍是 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),
});

識別範圍 (Identifying Scope)

Pennant 內建的 arraydatabase 儲存驅動知道如何正確儲存所有 PHP 資料類型以及 Eloquent 模型的範圍識別碼。然而,如果你的應用程式使用第三方 Pennant 驅動,該驅動可能不知道如何正確儲存 Eloquent 模型或應用程式中其他自訂類型的識別碼。

鑑於此,Pennant 允許你透過在應用程式中用作 Pennant 範圍的物件上實作 FeatureScopeable contract 來格式化儲存的範圍值。

例如,想像一下你在單個應用程式中使用兩個不同的功能驅動:內建的 database 驅動和第三方「Flag Rocket」驅動。「Flag Rocket」驅動不知道如何正確儲存 Eloquent 模型。相反,它需要一個 FlagRocketUser 實例。透過實作 FeatureScopeable contract 定義的 toFeatureIdentifier,我們可以自訂提供給應用程式使用的每個驅動的可儲存範圍值:

<?php

namespace App\Models;

use FlagRocket\FlagRocketUser;
use Illuminate\Database\Eloquent\Model;
use Laravel\Pennant\Contracts\FeatureScopeable;

class User extends Model implements FeatureScopeable
{
    /**
     * Cast the object to a feature scope identifier for the given driver.
     */
    public function toFeatureIdentifier(string $driver): mixed
    {
        return match($driver) {
            'database' => $this,
            'flag-rocket' => FlagRocketUser::fromId($this->flag_rocket_id),
        };
    }
}

序列化範圍 (Serializing Scope)

預設情況下,當儲存與 Eloquent 模型關聯的功能時,Pennant 將使用完全限定類別名稱。如果你已經在使用 Eloquent morph map,你可以選擇讓 Pennant 也使用 morph map 將儲存的功能與應用程式結構解耦。

為此,在服務提供者中定義 Eloquent morph map 後,你可以呼叫 Feature facade 的 useMorphMap 方法:

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

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

Feature::useMorphMap();

豐富的功能值 (Rich Feature Values)

到目前為止,我們主要展示了處於二元狀態的功能,這意味著它們要麼是「啟用」,要麼是「未啟用」,但 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' is active -->
@elsefeature('purchase-button', 'seafoam-green')
    <!-- 'seafoam-green' is active -->
@elsefeature('purchase-button', 'tart-orange')
    <!-- 'tart-orange' is active -->
@endfeature

[!NOTE] 當使用豐富值時,重要的是要知道,當功能具有除 false 以外的任何值時,該功能被視為「啟用」。

當呼叫條件 when 方法時,功能的豐富值將提供給第一個閉包:

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

同樣,當呼叫條件 unless 方法時,功能的豐富值將提供給可選的第二個閉包:

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

檢索多個功能 (Retrieving Multiple Features)

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
{
    /**
     * Bootstrap any application services.
     */
    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,
// ]

預先載入 (Eager Loading)

雖然 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($users)->loadAll();

更新值 (Updating Values)

當功能的值第一次被解析時,底層驅動會將結果儲存在儲存中。這通常是必要的,以確保你的使用者在請求之間獲得一致的體驗。然而,有時你可能想要手動更新功能的儲存值。

為此,你可以使用 activatedeactivate 方法將功能切換為「開啟」或「關閉」:

use Laravel\Pennant\Feature;

// Activate the feature for the default scope...
Feature::activate('new-api');

// Deactivate the feature for the given scope...
Feature::for($user->team)->deactivate('billing-v2');

也可以透過向 activate 方法提供第二個參數來手動為功能設定豐富值:

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

要指示 Pennant 忘記功能的儲存值,可以使用 forget 方法。當再次檢查功能時,Pennant 將從功能定義中解析功能的值:

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

批次更新 (Bulk Updates)

要批次更新儲存的功能值,可以使用 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 儲存驅動已儲存的已解析功能值。你還需要在應用程式中更新功能定義。

清除功能 (Purging Features)

有時,從儲存中清除整個功能可能會很有用。如果你已從應用程式中刪除該功能,或者你已對功能的定義進行了調整,並且希望將其推廣給所有使用者,這通常是必要的。

你可以使用 purge 方法刪除功能的所有儲存值:

// Purging a single feature...
Feature::purge('new-api');

// Purging multiple features...
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

測試 (Testing)

當測試與功能旗標互動的程式碼時,控制功能旗標返回值的最簡單方法是簡單地重新定義功能。例如,想像一下你在應用程式的一個服務提供者中定義了以下功能:

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 實例,則有一些有用的測試輔助函式可用

儲存設定 (Store Configuration)

你可以透過在應用程式的 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 驅動 (Adding Custom Pennant Drivers)

實作驅動 (Implementing the Driver)

如果 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 原始碼中的 Laravel\Pennant\Drivers\DatabaseDriver

[!NOTE] Laravel 不附帶包含你的擴充功能的目錄。你可以自由地將它們放在任何你喜歡的地方。在這個範例中,我們建立了一個 Extensions 目錄來存放 RedisFeatureDriver

註冊驅動 (Registering the Driver)

一旦你的驅動被實作,你就可以將其註冊到 Laravel。要將其他驅動添加到 Pennant,可以使用 Feature facade 提供的 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
{
    /**
     * Register any application services.
     */
    public function register(): void
    {
        // ...
    }

    /**
     * Bootstrap any application services.
     */
    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,
    ],

    // ...

],

外部定義功能 (Defining Features Externally)

如果你的驅動是第三方功能旗標平台的包裝器,你可能會在平台上定義功能,而不是使用 Pennant 的 Feature::define 方法。如果是這種情況,你的自訂驅動還應該實作 Laravel\Pennant\Contracts\DefinesFeaturesExternally 介面:

<?php

namespace App\Extensions;

use Laravel\Pennant\Contracts\Driver;
use Laravel\Pennant\Contracts\DefinesFeaturesExternally;

class FeatureFlagServiceDriver implements Driver, DefinesFeaturesExternally
{
    /**
     * Get the features defined for the given scope.
     */
    public function definedFeaturesForScope(mixed $scope): array {}

    /* ... */
}

definedFeaturesForScope 方法應返回為提供的範圍定義的功能名稱列表。

事件 (Events)

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
{
    /**
     * Bootstrap any application services.
     */
    public function boot(): void
    {
        Event::listen(function (UnknownFeatureResolved $event) {
            Log::error("Resolving unknown feature [{$event->feature}].");
        });
    }
}

Laravel\Pennant\Events\DynamicallyRegisteringFeatureClass

當在請求期間第一次動態檢查基於類別的功能時,會分派此事件。

Laravel\Pennant\Events\UnexpectedNullScopeEncountered

當將 null 範圍傳遞給不支援 null 的功能定義時,會分派此事件。

這種情況會被優雅地處理,功能將返回 false。然而,如果你想退出此功能的預設優雅行為,可以在應用程式的 AppServiceProviderboot 方法中為此事件註冊一個監聽器:

use Illuminate\Support\Facades\Log;
use Laravel\Pennant\Events\UnexpectedNullScopeEncountered;

/**
 * Bootstrap any application services.
 */
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

當清除所有功能時,會分派此事件。