イントロダクション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;
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'));
}
クラスベースの機能でも、同様のアプローチが可能です。The same approach may be used for class based features:
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
インスタンスを返すのであれば、便利な利用できるテストヘルパがあります。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\FeatureRetrieved
Laravel\Pennant\Events\FeatureRetrieved
このイベントは、機能をチェックするたびディスパッチします。このイベントは、アプリケーション全体で機能フラグの使用状況に対するメトリクスを作成し、追跡するのに便利でしょう。This event is dispatched whenever a feature is checked[#checking-features]. This event may be useful for creating and tracking metrics against a feature flag's usage throughout your application.
Laravel\Pennant\Events\FeatureResolved
Laravel\Pennant\Events\FeatureResolved
このイベントは、機能の値を特定のスコープで初めて解決したときにディスパッチします。This event is dispatched the first time a feature's value is resolved for a specific scope.
Laravel\Pennant\Events\UnknownFeatureResolved
Laravel\Pennant\Events\UnknownFeatureResolved
このイベントは、未知の機能を特定のスコープで初めて解決したときにディスパッチします。このイベントをリッスンしておくと、機能フラグを削除したつもりが、誤ってアプリケーション全体にその機能への参照を残してしまった場合に便利です。This event is dispatched the first time an unknown feature is resolved for a specific scope. Listening to this event may be useful if you have intended to remove a feature flag but have accidentally left stray references to it throughout your application:
<?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\DynamicallyRegisteringFeatureClass
このイベントは、リクエスト中でクラスベースの機能を初めて動的にチェックしたときにディスパッチします。This event is dispatched when a class based feature[#class-based-features] is dynamically checked for the first time during a request.
Laravel\Pennant\Events\UnexpectedNullScopeEncountered
Laravel\Pennant\Events\UnexpectedNullScopeEncountered
このイベントは、nullをサポートしない機能定義へ、null
スコープを渡したときにディスパッチします。This event is
dispatched when a null
scope is passed to a feature definition
that doesn't support
null[#nullable-scope].
この状況をスムーズに処理し、その機能はfalse
を返します。しかし、この機能のデフォルトのスムーズな動作を外したければ、アプリケーションのAppServiceProvider
のboot
メソッドでこのイベントのリスナを登録してください。This situation is
handled gracefully and the feature will
return false
. However, if
you would like to opt out of this
feature's default graceful behavior, you
may register a listener for this event
in the boot
method of your
application's
AppServiceProvider
:
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
Laravel\Pennant\Events\FeatureUpdated
このイベントは、通常activate
またはdeactivate
を呼び出すことにより、スコープの機能を更新したときにディスパッチします。This event is
dispatched when updating a feature for a
scope, usually by calling
activate
or
deactivate
.
Laravel\Pennant\Events\FeatureUpdatedForAllScopes
Laravel\Pennant\Events\FeatureUpdatedForAllScopes
このイベントは、通常activateForEveryone
またはdeactivateForEveryone
を呼び出すことで、すべてのスコープの機能を更新したときにディスパッチします。This event is
dispatched when updating a feature for
all scopes, usually by calling
activateForEveryone
or
deactivateForEveryone
.
Laravel\Pennant\Events\FeatureDeleted
Laravel\Pennant\Events\FeatureDeleted
このイベントは、スコープの機能を削除するときにディスパッチします。This event is
dispatched when deleting a feature for a
scope, usually by calling
forget
.
Laravel\Pennant\Events\FeaturesPurged
Laravel\Pennant\Events\FeaturesPurged
このイベントは、特定の機能をパージするときにディスパッチします。This event is dispatched when purging specific features.
Laravel\Pennant\Events\AllFeaturesPurged
Laravel\Pennant\Events\AllFeaturesPurged
このイベントは、すべての機能をパージするときにディスパッチします。This event is dispatched when purging all features.