跳到主要內容

Laravel Pennant

📝 此頁面為 Laravel 官方文檔的繁體中文翻譯。查看原始英文版本

Laravel Pennant

簡介

Laravel Pennant 是一個簡單且輕量級的功能開關套件——沒有額外的複雜性。功能開關使您能夠自信地逐步推出新的應用程式功能、對新的介面設計進行 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。
  • 否則,該功能應隨機分配給使用者,活躍的機率為百分之一。

第一次為給定使用者檢查 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);

[!NOTE] 功能類別是透過容器解析的,因此您可以根據需要向功能類別的構造函數注入依賴。

自訂儲存的功能名稱

預設情況下,Pennant 將儲存功能類別的完全限定類別名稱。如果您希望將儲存的功能名稱與應用程式的內部結構解耦,可以在功能類別上添加 Name 屬性。此屬性的值將被儲存以代替類別名稱:

<?php

namespace App\Features;

use Laravel\Pennant\Attributes\Name;

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

檢查功能

要判斷功能是否活躍,可以使用 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']);

[!NOTE] 在 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 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 方法提供了許多其他便利的方法來與功能互動:

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

@feature('site-redesign')
    
@else
    
@endfeature

@featureany(['site-redesign', 'beta'])
    
@endfeatureany

中介層

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

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')
    
@elsefeature('purchase-button', 'seafoam-green')
    
@elsefeature('purchase-button', 'tart-orange')
    
@endfeature

[!NOTE] 使用富值時,重要的是要知道當功能具有除 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($users)->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"?>

    
    
        
        
    

添加自訂 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 原始碼中的 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 的 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
{
    /**
     * 獲取為給定作用域定義的功能。
     */
    public function definedFeaturesForScope(mixed $scope): array {}

    /* ... */
}

definedFeaturesForScope 方法應返回為提供的作用域定義的功能名稱列表。

事件

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("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;

/**
 * 啟動任何應用程式服務。
 */
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

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