イントロダクションIntroduction
Laravel Pennant(ペナント:三角旗)は無駄がない、シンプルで軽量な機能フラグパッケージです。機能フラグを使うことで、新しいアプリケーションの機能を躊躇なく段階的にロールアウトしたり、新しいインターフェイスデザインをA/Bテストしたり、トランクベースの開発戦略を推奨したり、その他多くのことができるようになります。Laravel Pennant[https://github.com/laravel/pennant] is a simple and light-weight feature flag package - without the cruft. Feature flags enable you to incrementally roll out new application features with confidence, A/B test new interface designs, complement a trunk-based development strategy, and much more.
インストールInstallation
まず、Composerパッケージマネージャを使って、プロジェクトにPennantをインストールします。First, install Pennant into your project using the Composer package manager:
composer require laravel/pennant
次に、vendor:publish
Artisanコマンドを使用し、Pennantの設定ファイルとマイグレーションファイルをリソース公開する必要があります。Next, you should publish the Pennant configuration and migration files using the vendor:publish
Artisan command:
php artisan vendor:publish --provider="Laravel\Pennant\PennantServiceProvider"
最後に、アプリケーションのデータベースマイグレーションを実行してください。これにより、Pennantがdatabase
ドライバを動かすために使う、features
テーブルが作成されます。Finally, you should run your application's database migrations. This will create a features
table that Pennant uses to power its database
driver:
php artisan migrate
設定Configuration
Pennantのリソースを公開すると、その設定ファイルをconfig/pennant.php
へ保存します。この設定ファイルでPennantがデフォルトとして使用する、算出済みの機能フラグ値を保存するストレージメカニズムを指定します。After publishing Pennant's assets, its configuration file will be located at config/pennant.php
. This configuration file allows you to specify the default storage mechanism that will be used by Pennant to store resolved feature flag values.
Pennantは、算出済み機能フラグの値をメモリ内の配列へ格納する、array
ドライバをサポートしています。もしくは、算出済み機能フラグ値を、リレーショナルデータベースに永続的に保存する、database
ドライバも使用できます。(これはPennantで使用する、デフォルト保存メカニズムです。)Pennant includes support for storing resolved feature flag values in an in-memory array via the array
driver. Or, Pennant can store resolved feature flag values persistently in a relational database via the database
driver, which is the default storage mechanism used by Pennant.
機能の定義Defining Features
機能を定義するには、Feature
ファサードが提供する、define
メソッドを使用します。機能の名前と、その機能の初期値を決定するため呼び出す、クロージャを指定する必要があります。To define a feature, you may use the define
method offered by the Feature
facade. You will need to provide a name for the feature, as well as a closure that will be invoked to resolve the feature's initial value.
通常、機能はFeature
ファサードを使用し、サービスプロバイダで定義します。クロージャは、機能チェックのための「スコープ」を引数に取ります。最も一般的なのは、現在認証しているユーザーをスコープにすることでしょう。この例では、アプリケーションのユーザーへ、新しいAPIを段階的に提供する機能を定義しています。Typically, features are defined in a service provider using the Feature
facade. The closure will receive the "scope" for the feature check. Most commonly, the scope is the currently authenticated user. In this example, we will define a feature for incrementally rolling out a new API to our application's users:
<?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),
});
}
}
ご覧の通り、この機能では以下のようなルールを設けています。As you can see, we have the following rules for our feature:
- チーム内メンバーは全員、新しいAPIを使用できる。All internal team members should be using the new API.
- トラフィック量が多い顧客は、新しいAPIを使用できない。Any high traffic customers should not be using the new API.
- それ以外の場合、1/100の確率で、ランダムに機能をアクティブにする。Otherwise, the feature should be randomly assigned to users with a 1 in 100 chance of being active.
最初にnew-api
機能を指定したユーザーに対してチェックしたら、クロージャの実行結果をストレージドライバへ保存します。次回、同じユーザーに対しこの機能をチェックするとき、値はストレージから取り出し、クロージャを呼び出しません。The first time the new-api
feature is checked for a given user, the result of the closure will be stored by the storage driver. The next time the feature is checked against the same user, the value will be retrieved from storage and the closure will not be invoked.
使いやすいように、機能定義が抽選(lottery)を返すだけの場合は、クロージャを完全に省略できます。For convenience, if a feature definition only returns a lottery, you may omit the closure completely:
Feature::define('site-redesign', Lottery::odds(1, 1000));
クラスベースの機能Class Based Features
Pennantでは、クラスベースで機能を定義することもできます。クロージャベースの機能定義とは異なり、クラスベースの機能は、サービスプロバイダに登録する必要がありません。クラスベースの機能を生成するには、pennant:feature
Artisanコマンドを実行します。デフォルトで、機能クラスはアプリケーションのapp/Features
ディレクトリへ配置します。Pennant also allows you to define class based features. Unlike closure based feature definitions, there is no need to register a class based feature in a service provider. To create a class based feature, you may invoke the pennant:feature
Artisan command. By default the feature class will be placed in your application's app/Features
directory:
php artisan pennant:feature NewApi
機能クラスを書く場合、resolve
メソッドのみ定義する必要があります。このメソッドは、 指定したスコープに対する機能の初期値を解決するために呼び出されます。この場合も、スコープは通常、現在認証しているユーザーでしょう。When writing a feature class, you only need to define a resolve
method, which will be invoked to resolve the feature's initial value for a given scope. Again, the scope will typically be the currently authenticated user:
<?php
namespace App\Features;
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),
};
}
}
container, so you may inject dependencies into the feature class's constructor when needed.[!NOTE] Feature classes are resolved via the container[/docs/{{version}}/container], so you may inject dependencies into the feature class's constructor when needed.
Note: Feature classes are resolved via the
機能の保存名のカスタマイズCustomizing the Stored Feature Name
デフォルトで、Pennantは機能クラスの完全修飾クラス名を保存します。保存する機能名をアプリケーションの内部構造から切り離したい場合は、機能クラスで$name
プロパティを指定してください。このプロパティの値をクラス名の代わりに格納します。By default, Pennant will store the feature class's fully qualified class name. If you would like to decouple the stored feature name from the application's internal structure, you may specify a $name
property on the feature class. The value of this property will be stored in place of the class name:
<?php
namespace App\Features;
class NewApi
{
/**
* 機能の保存名
*
* @var string
*/
public $name = 'new-api';
// ...
}
機能のチェックChecking Features
ある機能がアクティブであるかを判断するには、Feature
ファサードのactive
メソッドを使用します。デフォルトで機能は、現在認証しているユーザーを対象にチェックします。To determine if a feature is active, you may use the active
method on the Feature
facade. By default, features are checked against the currently authenticated user:
<?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
メソッドを使用します。Although features are checked against the currently authenticated user by default, you may easily check the feature against another user or scope[#scope]. To accomplish this, use the for
method offered by the Feature
facade:
return Feature::for($user)->active('new-api')
? $this->resolveNewApiResponse($request)
: $this->resolveLegacyApiResponse($request);
Pennantはさらに、機能がアクティブかを判断するのに役立つ、便利なメソッドをいくつか用意しています。Pennant also offers some additional convenience methods that may prove useful when determining if a feature is active or not:
// 指定機能がすべてアクティブであることを判断
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コンテキストと、認証されていないコンテキストの両方を考慮した、デフォルトスコープを定義することもできます。[!NOTE]
Note: PennantをHTTPコンテキスト外で使う場合、例えばArtisanコマンドや、キュー投入したジョブでは、機能のスコープを通常
When using Pennant outside of an HTTP context, such as in an Artisan command or a queued job, you should typically explicitly specify the feature's scope[#specifying-the-scope]. Alternatively, you may define a default scope[#default-scope] that accounts for both authenticated HTTP contexts and unauthenticated contexts.
クラスベース機能のチェックChecking Class Based Features
クラスベースの機能の場合、機能をチェックするときにクラス名を指定する必要があります。For class based features, you should provide the class name when checking the feature:
<?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);
}
// ...
}
条件付き実行Conditional Execution
when
メソッドは、機能がアクティブなときに、スムーズに指定クロージャを実行するために使います。また、2つ目のクロージャを指定し、機能が非アクティブの場合に実行させることもできます。The when
method may be used to fluently execute a given closure if a feature is active. Additionally, a second closure may be provided and will be executed if the feature is inactive:
<?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
メソッドと逆の働きをし、その機能が非アクティブな場合、最初のクロージャを実行します。The unless
method serves as the inverse of the when
method, executing the first closure if the feature is inactive:
return Feature::unless(NewApi::class,
fn () => $this->resolveLegacyApiResponse($request),
fn () => $this->resolveNewApiResponse($request),
);
HasFeatures
トレイトThe HasFeatures
Trait
PennantのHasFeatures
トレイトは、アプリケーションのUser
モデル(あるいは、機能を持つ他のモデル)に追加し、モデルから直接機能をスムーズにチェックする、便利な手法を提供しています。Pennant's HasFeatures
trait may be added to your application's User
model (or any other model that has features) to provide a fluent, convenient way to check features directly from the model:
<?php
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Laravel\Pennant\Concerns\HasFeatures;
class User extends Authenticatable
{
use HasFeatures;
// ...
}
このトレイトをモデルへ追加すれば、features
メソッドを呼び出すことで、簡単に機能を確認できます。Once the trait has been added to your model, you may easily check features by invoking the features
method:
if ($user->features()->active('new-api')) {
// ...
}
もちろん、features
メソッドは、機能を操作する他の多くの便利なメソッドへのアクセスも提供します。Of course, the features
method provides access to many other convenient methods for interacting with 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 Directive
Blade内でも機能をシームレスにチェックするため、Pennantは@feature
ディレクティブを提供します。To make checking features in Blade a seamless experience, Pennant offers a @feature
directive:
@feature('site-redesign')
<!-- 'site-redesign'はアクティブ -->
@else
<!-- 'site-redesign'は非アクティブ -->
@endfeature
ミドルウェアMiddleware
Pennantは、ミドルウェアも用意しています。このミドルウェアは、ルートが呼び出される前に、現在認証されているユーザーがその機能にアクセスできることを確認するために使います。ミドルウェアをルートに割り当て、ルートにアクセスするために必要な機能を指定ができます。指定した機能のどれかが、現在認証されているユーザーにとって無効である場合、ルートは400 Bad Request
HTTPレスポンスを返します。静的メソッドusing
には複数の機能を渡せます。Pennant also includes a middleware[/docs/{{version}}/middleware] that may be used to verify the currently authenticated user has access to a feature before a route is even invoked. You may assign the middleware to a route and specify the features that are required to access the route. If any of the specified features are inactive for the currently authenticated user, a 400 Bad Request
HTTP response will be returned by the route. Multiple features may be passed to the static using
method.
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
リスト中の機能が非アクティブのときにミドルウェアが返すレスポンスをカスタマイズしたい場合は、EnsureFeaturesAreActive
ミドルウェアが提供する、whenInactive
メソッドを利用してください。通常、このメソッドはアプリケーションのサービスプロバイダのboot
メソッド内で呼び出す必要があります。If you would like to customize the response that is returned by the middleware when one of the listed features is inactive, you may use the whenInactive
method provided by the EnsureFeaturesAreActive
middleware. Typically, this method should be invoked within the boot
method of one of your application's service providers:
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);
}
);
// ...
}
メモリ内キャッシュIn-Memory Cache
機能をチェックすると、Pennantはその結果のメモリ内キャッシュを作成します。database
ドライバを使っている場合、これは同じ機能フラグを一つのリクエストで再チェックしても、追加のデータベースクエリが発生しないことを意味します。これはまた、その機能がリクエストの間、一貫した結果を持つことを保証します。When checking a feature, Pennant will create an in-memory cache of the result. If you are using the database
driver, this means that re-checking the same feature flag within a single request will not trigger additional database queries. This also ensures that the feature has a consistent result for the duration of the request.
メモリ内のキャッシュを手作業で消去する必要がある場合は、Feature
ファサードが提供するflushCache
メソッドを使用してください。If you need to manually flush the in-memory cache, you may use the flushCache
method offered by the Feature
facade:
Feature::flushCache();
スコープScope
スコープの指定Specifying the Scope
説明してきたように、機能は通常、現在認証しているユーザーに対してチェックされます。しかし、これは必ずしもあなたのニーズに合うとは限りません。そのため、Feature
ファサードのfor
メソッドでは、ある機能をチェックする対象のスコープを指定できます。As discussed, features are typically checked against the currently authenticated user. However, this may not always suit your needs. Therefore, it is possible to specify the scope you would like to check a given feature against via the Feature
facade's for
method:
return Feature::for($user)->active('new-api')
? $this->resolveNewApiResponse($request)
: $this->resolveLegacyApiResponse($request);
もちろん、機能のスコープは、「ユーザー」に限定されません。新しい課金システムを構築しており、個々のユーザーではなく、チーム全体にロールアウトしていると想像してみてください。たぶん、古いチームには、新しいチームよりもゆっくりとロールアウトしたいと思うでしょう。この機能解決のクロージャは、次のようなものになります。Of course, feature scopes are not limited to "users". Imagine you have built a new billing experience that you are rolling out to entire teams rather than individual users. Perhaps you would like the oldest teams to have a slower rollout than the newer teams. Your feature resolution closure might look something like the following:
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
メソッドへ、チームを渡す必要があります。You will notice that the closure we have defined is not expecting a User
, but is instead expecting a Team
model. To determine if this feature is active for a user's team, you should pass the team to the for
method offered by the Feature
facade:
if (Feature::for($user->team)->active('billing-v2')) {
return redirect()->to('/billing/v2');
}
// ...
デフォルトスコープDefault Scope
Pennant が機能をチェックするのに使うデフォルトのスコープをカスタマイズすることも可能です。たとえば、すべての機能をユーザーではなく、現在認証しているユーザーのチームに対してチェックするとします。機能をチェックするたびに、Feature::for($user->team)
を毎回呼び出す代わりに、チームをデフォルトのスコープとして指定できます。一般に、これはアプリケーションのいずれかのサービスプロバイダで行う必要があります。It is also possible to customize the default scope Pennant uses to check features. For example, maybe all of your features are checked against the currently authenticated user's team instead of the user. Instead of having to call Feature::for($user->team)
every time you check a feature, you may instead specify the team as the default scope. Typically, this should be done in one of your application's service providers:
<?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
メソッドで明示的にスコープを指定しなかった場合、機能チェックでは現在認証しているユーザーのチームをデフォルトのスコープとして使用するようになりました。If no scope is explicitly provided via the for
method, the feature check will now use the currently authenticated user's team as the default scope:
Feature::active('billing-v2');
// 上記コードが以下と同じ動作をするようになった
Feature::for($user->team)->active('billing-v2');
NULL許可のスコープNullable Scope
もし、ある機能をチェックするときに指定したスコープで、nullable型またはnull
を含むunion型により、機能の定義でnull
をサポートしていない場合、Pennantは自動的にその機能の結果値としてfalse
を返します。If the scope you provide when checking a feature is null
and the feature's definition does not support null
via a nullable type or by including null
in a union type, Pennant will automatically return false
as the feature's result value.
したがって、機能に渡すスコープがnull
になる可能性があり、その機能の値リゾルバを呼び出したい場合は、機能の定義でこれを考慮する必要があります。null
スコープは、Artisan コマンド、キュー投入したジョブ、もしくは未認証のルート内で機能をチェックする場合に発生する可能性があります。これらのコンテキストでは通常、認証済みユーザーが存在しないため、デフォルトのスコープは null
になります。So, if the scope you are passing to a feature is potentially null
and you want the feature's value resolver to be invoked, you should account for that in your feature's definition. A null
scope may occur if you check a feature within an Artisan command, queued job, or unauthenticated route. Since there is usually not an authenticated user in these contexts, the default scope will be null
.
もし、常に機能のスコープを明示的に指定しないのであれば、スコープのタイプを"nullable"にし、機能定義のロジック内でnull
スコープ値を確実に処理してください。If you do not always explicitly specify your feature scope[#specifying-the-scope] then you should ensure the scope's type is "nullable" and handle the null
scope value within your feature definition logic:
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我用意しているarray
とdatabase
ストレージドライバは、すべてのPHPデータ型とEloquentモデルのスコープ識別子を適切に保存する方法を知っています。しかし、サードパーティのPennantドライバを使用する場合、そのドライバはEloquentモデルやその他のカスタム型に対する識別子を正しく格納する方法を知らない可能性があります。Pennant's built-in array
and database
storage drivers know how to properly store scope identifiers for all PHP data types as well as Eloquent models. However, if your application utilizes a third-party Pennant driver, that driver may not know how to properly store an identifier for an Eloquent model or other custom types in your application.
こうした観点から、Pennantは、アプリケーションの中でPennantのスコープとして使用するオブジェクトへ、FeatureScopeable
契約を実装することにより、スコープの値を保存用にフォーマットできるようにしました。In light of this, Pennant allows you to format scope values for storage by implementing the FeatureScopeable
contract on the objects in your application that are used as Pennant scopes.
例えば、1つのアプリケーションで2つの異なる機能ドライバを使用しているとします。組み込みのdatabase
ドライバと、サードパーティの"Flag Rocket"ドライバです。"Flag Rocket"ドライバはEloquentモデルを適切に保存する方法を知りません。代わりに、FlagRocketUser
インスタンスを必要とします。FeatureScopeable
が定義するtoFeatureIdentifier
を実装し、アプリケーションで使用する各ドライバに提供する保存可能なスコープの値をカスタマイズできます。For example, imagine you are using two different feature drivers in a single application: the built-in database
driver and a third-party "Flag Rocket" driver. The "Flag Rocket" driver does not know how to properly store an Eloquent model. Instead, it requires a FlagRocketUser
instance. By implementing the toFeatureIdentifier
defined by the FeatureScopeable
contract, we can customize the storable scope value provided to each driver used by our application:
<?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),
};
}
}
スコープのシリアライズSerializing Scope
PennantはEloquentモデルに関連付けた機能を格納するとき、デフォルトで完全修飾クラス名を使います。Eloquentモーフィックマップを使っている場合は、Pennantでもモーフィックマップを使い、保存した機能をアプリケーションの構造から切り離せます。By default, Pennant will use a fully qualified class name when storing a feature associated with an Eloquent model. If you are already using an Eloquent morph map[/docs/{{version}}/eloquent-relationships#custom-polymorphic-types], you may choose to have Pennant also use the morph map to decouple the stored feature from your application structure.
これを行なうには、サービスプロバイダでEloquentモーフマップを定義した後に、Feature
ファサードのuseMorphMap
メソッドを呼び出します。To achieve this, after defining your Eloquent morph map in a service provider, you may invoke the Feature
facade's useMorphMap
method:
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はリッチな値も格納できます。Until now, we have primarily shown features as being in a binary state, meaning they are either "active" or "inactive", but Pennant also allows you to store rich values as well.
例えば、アプリケーションの「Buy now」ボタンに3つの新しい色をテストする場合を考えてみましょう。機能定義からtrue
やfalse
を返す代わりに、文字列を返せます。For example, imagine you are testing three new colors for the "Buy now" button of your application. Instead of returning true
or false
from the feature definition, you may instead return a string:
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
機能の値を取得できます。You may retrieve the value of the purchase-button
feature using the value
method:
$color = Feature::value('purchase-button');
また、Pennantが用意しているBladeディレクティブでは簡単に、機能の現在の値に基づいて、条件付きでコンテンツをレンダできます。Pennant's included Blade directive also makes it easy to conditionally render content based on the current value of the feature:
@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: When using rich values, it is important to know that a feature is considered "active" when it has any value other than
false
.[!NOTE] When using rich values, it is important to know that a feature is considered "active" when it has any value other thanfalse
.
条件付きwhen
メソッドを呼び出すと、その機能のリッチな値が最初のクロージャに提供されます。When calling the conditional when
[#conditional-execution] method, the feature's rich value will be provided to the first closure:
Feature::when('purchase-button',
fn ($color) => /* ... */,
fn () => /* ... */,
);
同様に、条件付きのunless
メソッドを呼び出すと、その機能のリッチな値がオプションの2番目のクロージャへ渡されます。Likewise, when calling the conditional unless
method, the feature's rich value will be provided to the optional second closure:
Feature::unless('purchase-button',
fn () => /* ... */,
fn ($color) => /* ... */,
);
複数の機能の取得Retrieving Multiple Features
value
メソッドで、指定スコープに対する複数の機能を取得できます。The values
method allows the retrieval of multiple features for a given scope:
Feature::values(['billing-v2', 'purchase-button']);
// [
// 'billing-v2' => false,
// 'purchase-button' => 'blue-sapphire',
// ]
もしくは、all
メソッドを使用し、指定スコープに定義されているすべての機能の値を取得することもできます。Or, you may use the all
method to retrieve the values of all defined features for a given scope:
Feature::all();
// [
// 'billing-v2' => false,
// 'purchase-button' => 'blue-sapphire',
// 'site-redesign' => true,
// ]
しかし、クラスベースの機能は動的に登録されますので、明示的にチェックするまでPennantにはわかりません。つまり、アプリケーションのクラスベースの機能は、現在のリクエストでチェックしていない場合、all
メソッドが返す結果に現れない可能性があります。However, class based features are dynamically registered and are not known by Pennant until they are explicitly checked. This means your application's class based features may not appear in the results returned by the all
method if they have not already been checked during the current request.
もし、 all
メソッドを使うときに、特徴クラスが常に含まれるよう確実にしたい場合は、Pennantの機能発見機構を使ってください。最初に、アプリケーションのサービスプロバイダの一つの中で、discover
メソッドを呼び出します。If you would like to ensure that feature classes are always included when using the all
method, you may use Pennant's feature discovery capabilities. To get started, invoke the discover
method in one of your application's service providers:
<?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
メソッドは、現在のリクエストでチェック済みかに関係なく、これらのクラスを結果に含めます。The discover
method will register all of the feature classes in your application's app/Features
directory. The all
method will now include these classes in its results, regardless of whether they have been checked during the current request:
Feature::all();
// [
// 'App\Features\NewApi' => true,
// 'billing-v2' => false,
// 'purchase-button' => 'blue-sapphire',
// 'site-redesign' => true,
// ]
EagerロードEager Loading
Pennantは、一つのリクエストで解決したすべての機能のメモリ内キャッシュを保持しますが、それでも性能上の問題が発生する可能性があります。これを軽減するために、Pennantは機能の値をEagerロードする機能を提供しています。Although Pennant keeps an in-memory cache of all resolved features for a single request, it is still possible to encounter performance issues. To alleviate this, Pennant offers the ability to eager load feature values.
これを理解するため、機能がアクティブであるかをループの中でチェックしていると考えてください。To illustrate this, imagine that we are checking if a feature is active within a loop:
use Laravel\Pennant\Feature;
foreach ($users as $user) {
if (Feature::for($user)->active('notifications-beta')) {
$user->notify(new RegistrationSuccess);
}
}
データベースドライバを使用していると仮定すると、このコードはループ内のすべてのユーザーに対してデータベースクエリを実行することになり、潜在的に数百のクエリを実行することになります。しかし、Pennantのload
メソッドを使えば、ユーザーやスコープのコレクションの値をEagerロードでき、この潜在的なパフォーマンスのボトルネックを取り除けます。Assuming we are using the database driver, this code will execute a database query for every user in the loop - executing potentially hundreds of queries. However, using Pennant's load
method, we can remove this potential performance bottleneck by eager loading the feature values for a collection of users or scopes:
Feature::for($users)->load(['notifications-beta']);
foreach ($users as $user) {
if (Feature::for($user)->active('notifications-beta')) {
$user->notify(new RegistrationSuccess);
}
}
機能の値がまだロードされていないときだけロードするには、loadMissing
メソッドを使用します。To load feature values only when they have not already been loaded, you may use the loadMissing
method:
Feature::for($users)->loadMissing([
'new-api',
'purchase-button',
'notifications-beta',
]);
値の更新Updating Values
機能の値を初めて解決するとき、裏で動作しているドライバはその結果をストレージへ保存します。これは、リクエスト間で一貫したユーザー体験を保証するために、多くの場合必要です。しかし、時には、保存している機能の値を手作業で更新したい場合もあるでしょう。When a feature's value is resolved for the first time, the underlying driver will store the result in storage. This is often necessary to ensure a consistent experience for your users across requests. However, at times, you may want to manually update the feature's stored value.
このために、activate
とdeactivate
メソッドを使用して、機能の"on"と"off"を切り替えます。To accomplish this, you may use the activate
and deactivate
methods to toggle a feature "on" or "off":
use Laravel\Pennant\Feature;
// デフォルトのスコープの機能を有効にする
Feature::activate('new-api');
// 指定のスコープの機能を無効にする
Feature::for($user->team)->deactivate('billing-v2');
または、activate
メソッドへ第2引数を指定し、手作業で機能へリッチな値を設定することも可能です。It is also possible to manually set a rich value for a feature by providing a second argument to the activate
method:
Feature::activate('purchase-button', 'seafoam-green');
Pennantへ、ある機能の保存値を消去するように指示するには、forget
メソッドを使用します。その機能を再びチェックするとき、Pennantはその機能の定義により、値を解決します。To instruct Pennant to forget the stored value for a feature, you may use the forget
method. When the feature is checked again, Pennant will resolve the feature's value from its feature definition:
Feature::forget('purchase-button');
バルク更新Bulk Updates
保存している機能の値を一括で更新するには、activateForEveryone
メソッドとdeactivateForEveryone
メソッドを使用します。To update stored feature values in bulk, you may use the activateForEveryone
and deactivateForEveryone
methods.
例えば、あなたがnew-api
機能の安定性に自信を持ち、チェックアウトフローに最適な'purchase-button'
の色を見つけたとします。それに応じて、全ユーザーの機能値を更新できます。For example, imagine you are now confident in the new-api
feature's stability and have landed on the best 'purchase-button'
color for your checkout flow - you can update the stored value for all users accordingly:
use Laravel\Pennant\Feature;
Feature::activateForEveryone('new-api');
Feature::activateForEveryone('purchase-button', 'seafoam-green');
もしくは、全ユーザーに対し、その機能を非アクティブにすることもできます。Alternatively, you may deactivate the feature for all users:
Feature::deactivateForEveryone('new-api');
[!NOTE] This will only update the resolved feature values that have been stored by Pennant's storage driver. You will also need to update the feature definition in your application.
Note: This will only update the resolved feature values that have been stored by Pennant's storage driver. You will also need to update the feature definition in your application.
機能の削除Purging Features
時には、ストレージから機能全体を取り除くのが、有用な場合があります。これは、アプリケーションからその機能を削除した場合や、その機能の定義に調整を加え、全ユーザーにロールアウトする状況で、典型的に必要になります。Sometimes, it can be useful to purge an entire feature from storage. This is typically necessary if you have removed the feature from your application or you have made adjustments to the feature's definition that you would like to rollout to all users.
ある機能に対して保存されているすべての値を削除するには、purge
メソッドを使用します。You may remove all stored values for a feature using the purge
method:
// 1機能の削除
Feature::purge('new-api');
// 複数機能の削除
Feature::purge(['new-api', 'purchase-button']);
もしストレージから、全機能を削除したい場合は、引数なしでpurge
メソッドを呼び出します。If you would like to purge all features from storage, you may invoke the purge
method without any arguments:
Feature::purge();
アプリケーションのデプロイパイプラインの一貫として、機能を削除できると便利であるため、Pennantは指定した機能をストレージから削除する、pennant:purge
Artisanコマンドを用意しています。As it can be useful to purge features as part of your application's deployment pipeline, Pennant includes a pennant:purge
Artisan command which will purge the provided features from storage:
php artisan pennant:purge new-api
php artisan pennant:purge new-api purchase-button
また、リスト内で指定した機能を除くすべての機能を削除することも可能です。例えば、"new-api"と"purchase-button"機能を保存したまま、他のすべてのフィーチャーを削除したいとします。これを行うには、--except
オプションへこれらの機能名を渡します。It is also possible to purge all features except those in a given feature list. For example, imagine you wanted to purge all features but keep the values for the "new-api" and "purchase-button" features in storage. To accomplish this, you can pass those feature names to the --except
option:
php artisan pennant:purge --except=new-api --except=purchase-button
使いやすいように、pennant:purge
コマンドは、--except-registered
フラグもサポートしています。このフラグは、サービスプロバイダで明示的に登録している機能以外のすべての機能を削除することを意味します。For convenience, the pennant:purge
command also supports an --except-registered
flag. This flag indicates that all features except those explicitly registered in a service provider should be purged:
php artisan pennant:purge --except-registered
テストTesting
機能フラグを操作するコードをテストする場合、機能フラグの返り値をテストでコントロールする最も簡単な方法は、その機能を単純に再定義することです。たとえば、アプリケーションのサービスプロバイダに次のような機能が定義されているとしましょう。When testing code that interacts with feature flags, the easiest way to control the feature flag's returned value in your tests is to simply re-define the feature. For example, imagine you have the following feature defined in one of your application's service provider:
use Illuminate\Support\Arr;
use Laravel\Pennant\Feature;
Feature::define('purchase-button', fn () => Arr::random([
'blue-sapphire',
'seafoam-green',
'tart-orange',
]));
テストの中で、機能の戻り値を変更するには、テストの最初にその機能を再定義します。以下のテストは、サービスプロバイダにArr::random()
の実装が残っていても、常にパスします。To modify the feature's returned value in your tests, you may re-define the feature at the beginning of the test. The following test will always pass, even though the Arr::random()
implementation is still present in the service provider:
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'));
}
クラスベースの機能でも、同様のアプローチが可能です。The same approach may be used for class based features:
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
インスタンスを返すのであれば、便利な利用できるテストヘルパがあります。If your feature is returning a Lottery
instance, there are a handful of useful testing helpers available[/docs/{{version}}/helpers#testing-lotteries].
保存域の設定Store Configuration
アプリケーションのphpunit.xml
ファイルで、PENNANT_STORE
環境変数を定義すれば、テスト中にPennantが使用する保存域を設定できます。You may configure the store that Pennant will use during testing by defining the PENNANT_STORE
environment variable in your application's phpunit.xml
file:
<?xml version="1.0" encoding="UTF-8"?>
<phpunit colors="true">
<!-- ... -->
<php>
<env name="PENNANT_STORE" value="array"/>
<!-- ... -->
</php>
</phpunit>
カスタム機能ドライバの追加Adding Custom Pennant Drivers
ドライバの実装Implementing the Driver
もし、Pennantの既存ストレージドライバが、どれもあなたのアプリケーションのニーズに合わない場合は、独自のストレージドライバを書いてください。カスタムドライバは、Laravel\Pennant\Contracts\Driver
インターフェイスを実装する必要があります。If none of Pennant's existing storage drivers fit your application's needs, you may write your own storage driver. Your custom driver should implement the Laravel\Pennant\Contracts\Driver
interface:
<?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
を見てください。Now, we just need to implement each of these methods using a Redis connection. For an example of how to implement each of these methods, take a look at the Laravel\Pennant\Drivers\DatabaseDriver
in the Pennant source code[https://github.com/laravel/pennant/blob/1.x/src/Drivers/DatabaseDriver.php]
Note: Laravelは、拡張機能を格納するディレクトリを用意していません。好きな場所に自由に配置できます。この例では、
RedisFeatureDriver
を格納するために、Extensions
ディレクトリを作成しました。[!NOTE]
Laravel does not ship with a directory to contain your extensions. You are free to place them anywhere you like. In this example, we have created anExtensions
directory to house theRedisFeatureDriver
.
ドライバの登録Registering the Driver
ドライバを実装したら、Laravelに登録します。Pennantへドライバを追加するには、Feature
ファサードが提供するextend
メソッドをしようします。extend
メソッドは、アプリケーションの[サービスプロバイダ](/docs/{{version}}/providers のboot
メソッドから呼び出す必要があります。Once your driver has been implemented, you are ready to register it with Laravel. To add additional drivers to Pennant, you may use the extend
method provided by the Feature
facade. You should call the extend
method from the boot
method of one of your application's service provider[/docs/{{version}}/providers]:
<?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
ドライバが利用できるようになります。Once the driver has been registered, you may use the redis
driver in your application's config/pennant.php
configuration file:
'stores' => [
'redis' => [
'driver' => 'redis',
'connection' => null,
],
// ...
],
イベントEvents
Pennantは、アプリケーション全体の機能フラグを追跡するときに便利な、さまざまなイベントを発行します。Pennant dispatches a variety of events that can be useful when tracking feature flags throughout your application.
Laravel\Pennant\Events\RetrievingKnownFeature
Laravel\Pennant\Events\RetrievingKnownFeature
このイベントは、特定のスコープに対するリクエスト中に、既知の機能を初めて取得したときに発行します。このイベントは、アプリケーション全体で使用する機能フラグに対するメトリックを作成し、追跡するのに便利です。This event is dispatched the first time a known feature is retrieved during a request for a specific scope. This event can be useful to create and track metrics against the feature flags that are being used throughout your application.
Laravel\Pennant\Events\RetrievingUnknownFeature
Laravel\Pennant\Events\RetrievingUnknownFeature
このイベントは、特定のスコープへのリクエスト中に、未知の機能を初めて取得したときに発行します。このイベントは、ある機能フラグを削除するつもりが、誤ってアプリケーション全体にそのフラグへの参照を残してしまった場合に便利です。This event is dispatched the first time an unknown feature is retrieved during a request for a specific scope. This event can be useful if you have intended to remove a feature flag, but may have accidentally left some stray references to it throughout your application.
例えば、このイベントをリッスンして、それが発生したときに、report
や例外を投げるのが便利でしょう。For example, you may find it useful to listen for this event and report
or throw an exception when it occurs:
<?php
namespace App\Providers;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Event;
use Laravel\Pennant\Events\RetrievingUnknownFeature;
class EventServiceProvider extends ServiceProvider
{
/**
* アプリケーションのその他のイベントの登録
*/
public function boot(): void
{
Event::listen(function (RetrievingUnknownFeature $event) {
report("Resolving unknown feature [{$event->feature}].");
});
}
}
Laravel\Pennant\Events\DynamicallyDefiningFeature
Laravel\Pennant\Events\DynamicallyDefiningFeature
このイベントは、クラスベースの機能をリクエスト中に、初めて動的にチェックするときに発行します。This event is dispatched when a class based feature is being dynamically checked for the first time during a request.