라라벨 페넌트
소개
라라벨 페넌트는 군더더기 없이 간단하고 가벼운 기능 플래그 패키지입니다. 기능 플래그를 사용하면 새로운 애플리케이션 기능을 자신 있게 점진적으로 출시하고, 새로운 인터페이스 디자인에 대한 A/B 테스트를 수행하고, 트렁크 기반 개발 전략을 보완하는 등 많은 작업을 수행할 수 있습니다.
설치
먼저, Composer 패키지 관리자를 사용하여 페넌트를 프로젝트에 설치하십시오.
composer require laravel/pennant
다음으로, vendor:publish Artisan 명령어를 사용하여 페넌트 설정 및 마이그레이션 파일을 게시해야 합니다.
php artisan vendor:publish --provider="Laravel\Pennant\PennantServiceProvider"
마지막으로, 애플리케이션의 데이터베이스 마이그레이션을 실행해야 합니다. 이렇게 하면 Pennant가 database 드라이버를 구동하는 데 사용하는 features 테이블이 생성됩니다.
php artisan migrate
구성
Pennant의 에셋을 게시한 후, 구성 파일은 config/pennant.php에 위치합니다. 이 구성 파일을 통해 Pennant가 해결된 기능 플래그 값을 저장하는 데 사용되는 기본 스토리지 메커니즘을 지정할 수 있습니다.
Pennant는 array 드라이버를 통해 메모리 내 배열에 해결된 기능 플래그 값을 저장하는 기능을 지원합니다. 또는 Pennant는 Pennant에서 사용되는 기본 스토리지 메커니즘인 database 드라이버를 통해 관계형 데이터베이스에 해결된 기능 플래그 값을 영구적으로 저장할 수 있습니다.
기능 정의
기능을 정의하려면 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{ /** * Bootstrap any application services. */ public function boot(): void { Feature::define('new-api', fn (User $user) => match (true) { $user->isInternalTeamMember() => true, $user->isHighTrafficCustomer() => false, default => Lottery::odds(1 / 100), }); }}
보시다시피, 저희 기능에는 다음과 같은 규칙이 있습니다.
- 모든 내부 팀 구성원은 새 API를 사용해야 합니다.
- 트래픽이 많은 고객은 새 API를 사용해서는 안 됩니다.
- 그렇지 않으면, 기능은 1/100의 확률로 활성화되도록 사용자에게 무작위로 할당되어야 합니다.
특정 사용자에 대해 new-api 기능이 처음으로 확인되면, 클로저의 결과가 스토리지 드라이버에 저장됩니다. 다음에 동일한 사용자에 대해 기능을 확인하면, 값이 스토리지에서 검색되고 클로저는 호출되지 않습니다.
편의를 위해 기능 정의가 로터리만 반환하는 경우 클로저를 완전히 생략할 수 있습니다.
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{ /** * Resolve the feature's initial value. */ public function resolve(User $user): mixed { return match (true) { $user->isInternalTeamMember() => true, $user->isHighTrafficCustomer() => false, default => Lottery::odds(1 / 100), }; }}
클래스 기반 기능의 인스턴스를 수동으로 확인하려면 Feature 파사드에서 instance 메서드를 호출하면 됩니다.
use Illuminate\Support\Facades\Feature; $instance = Feature::instance(NewApi::class);
기능 클래스는 컨테이너를 통해 해결되므로 필요한 경우 기능 클래스의 생성자에 종속성을 주입할 수 있습니다.
저장된 기능 이름 사용자 정의
기본적으로 Pennant는 기능 클래스의 정규화된 클래스 이름을 저장합니다. 저장된 기능 이름을 애플리케이션의 내부 구조와 분리하고 싶다면 기능 클래스에 $name 속성을 지정할 수 있습니다. 이 속성의 값은 클래스 이름 대신 저장됩니다.
<?php namespace App\Features; class NewApi{ /** * 기능의 저장된 이름입니다. * * @var string */ public $name = 'new-api'; // ...}
기능 확인
기능이 활성 상태인지 확인하려면 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']);
Artisan 명령어 또는 큐에 등록된 작업과 같이 HTTP 컨텍스트 외부에서 Pennant를 사용하는 경우 일반적으로 기능의 범위를 명시적으로 지정해야 합니다. 또는 인증된 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 트레이트
Pennant의 HasFeatures 트레이트는 애플리케이션의 User 모델 (또는 기능을 가진 다른 모델)에 추가하여 모델에서 직접 기능을 확인하는 유연하고 편리한 방법을 제공할 수 있습니다.
<?php namespace App\Models; use Illuminate\Foundation\Auth\User as Authenticatable;use Laravel\Pennant\Concerns\HasFeatures; class User extends Authenticatable{ use HasFeatures; // ...}
트레이트가 모델에 추가되면 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 () => /* ... */,);
블레이드 디렉티브
블레이드에서 기능을 원활하게 확인하기 위해, Pennant는 @feature 및 @featureany 디렉티브를 제공합니다:
@feature('site-redesign') <!-- 'site-redesign'이 활성화됨 -->@else <!-- 'site-redesign'이 비활성화됨 -->@endfeature @featureany(['site-redesign', 'beta']) <!-- '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; /** * Bootstrap any application services. */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; } } // ...}
In-Memory 캐시
기능을 확인할 때 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 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 메서드에 팀을 전달해야 합니다.
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');
Nullable 스코프
기능을 확인할 때 제공하는 스코프가 null이고 해당 기능의 정의가 nullable 타입 또는 union 타입에 null을 포함하여 null을 지원하지 않는 경우, Pennant는 기능의 결과 값으로 자동으로 false를 반환합니다.
따라서 기능에 전달하는 스코프가 잠재적으로 null이고 기능의 값 확인자(value resolver)를 호출하고 싶다면, 기능 정의에서 이를 고려해야 합니다. null 스코프는 Artisan 명령어, 큐에 대기 중인 작업 또는 인증되지 않은 경로 내에서 기능을 확인할 때 발생할 수 있습니다. 이러한 컨텍스트에서는 일반적으로 인증된 사용자가 없으므로 기본 스코프는 null이 됩니다.
항상 기능 스코프를 명시적으로 지정하지 않는 경우, 스코프의 유형이 "nullable"인지 확인하고 기능 정의 로직 내에서 null 스코프 값을 처리해야 합니다.
use App\Models\User;use Illuminate\Support\Lottery;use Laravel\Pennant\Feature; Feature::define('new-api', fn (User $user) => match (true) {Feature::define('new-api', fn (User|null $user) => match (true) { $user === null => true, $user->isInternalTeamMember() => true, $user->isHighTrafficCustomer() => false, default => Lottery::odds(1 / 100),});
스코프 식별
Pennant의 내장된 array 및 database 저장소 드라이버는 모든 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 morph map을 사용하고 있다면, Pennant가 morph map을 사용하여 저장된 기능을 애플리케이션 구조와 분리하도록 선택할 수도 있습니다.
이를 달성하려면 서비스 제공자에서 Eloquent morph map을 정의한 후, 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에서는 풍부한 값도 저장할 수 있습니다.
예를 들어, 애플리케이션의 "지금 구매" 버튼에 대한 세 가지 새로운 색상을 테스트한다고 가정해 보겠습니다. 기능 정의에서 true 또는 false를 반환하는 대신, 문자열을 반환할 수 있습니다:
use Illuminate\Support\Arr;use Laravel\Pennant\Feature; Feature::define('purchase-button', fn (User $user) => Arr::random([ 'blue-sapphire', 'seafoam-green', 'tart-orange',]));
value 메서드를 사용하여 purchase-button 기능의 값을 검색할 수 있습니다:
$color = Feature::value('purchase-button');
Pennant에 포함된 Blade 지시어는 기능의 현재 값에 따라 조건부로 콘텐츠를 렌더링하는 것을 쉽게 만듭니다:
@feature('purchase-button', 'blue-sapphire') <!-- 'blue-sapphire'가 활성화됨 -->@elsefeature('purchase-button', 'seafoam-green') <!-- 'seafoam-green'이 활성화됨 -->@elsefeature('purchase-button', 'tart-orange') <!-- 'tart-orange'가 활성화됨 -->@endfeature
풍부한 값을 사용할 때, 기능은 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();
값 업데이트
기능 값이 처음으로 확인되면 기본 드라이버는 결과를 스토리지에 저장합니다. 이는 종종 요청 전반에 걸쳐 사용자에게 일관된 경험을 보장하는 데 필요합니다. 그러나 때로는 기능의 저장된 값을 수동으로 업데이트해야 할 수 있습니다.
이를 위해 activate 및 deactivate 메서드를 사용하여 기능을 "켜기" 또는 "끄기"로 전환할 수 있습니다.
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');
대량 업데이트
저장된 기능 값을 대량으로 업데이트하려면 activateForEveryone 및 deactivateForEveryone 메서드를 사용할 수 있습니다.
예를 들어, new-api 기능의 안정성을 확신하고 결제 흐름에 가장 적합한 'purchase-button' 색상에 도달했다고 가정해 보겠습니다. 그러면 모든 사용자에 대해 저장된 값을 적절하게 업데이트할 수 있습니다.
use Laravel\Pennant\Feature; Feature::activateForEveryone('new-api'); Feature::activateForEveryone('purchase-button', 'seafoam-green');
또는 모든 사용자에 대해 기능을 비활성화할 수 있습니다.
Feature::deactivateForEveryone('new-api');
이는 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('기능 값을 제어할 수 있습니다', 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"?><phpunit colors="true"> <!-- ... --> <php> <env name="PENNANT_STORE" value="array"/> <!-- ... --> </php></phpunit>
사용자 정의 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를 참조하십시오.
Laravel은 확장을 담을 디렉토리를 제공하지 않습니다. 원하는 곳에 자유롭게 배치할 수 있습니다. 이 예에서는 RedisFeatureDriver를 보관하기 위해 Extensions 디렉토리를 만들었습니다.
드라이버 등록
드라이버가 구현되면 Laravel에 등록할 준비가 된 것입니다. Pennant에 추가 드라이버를 추가하려면 Feature facade에서 제공하는 extend 메서드를 사용할 수 있습니다. 애플리케이션의 서비스 제공자 중 하나의 boot 메서드에서 extend 메서드를 호출해야 합니다.
<?php namespace App\Providers; use App\Extensions\RedisFeatureDriver;use Illuminate\Contracts\Foundation\Application;use Illuminate\Support\ServiceProvider;use Laravel\Pennant\Feature; class AppServiceProvider extends ServiceProvider{ /** * 애플리케이션 서비스 등록. */ 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{ /** * Bootstrap any application services. */ public function boot(): void { Event::listen(function (UnknownFeatureResolved $event) { Log::error("알 수 없는 기능 [{$event->feature}]을(를) 확인 중입니다."); }); }}
Laravel\Pennant\Events\DynamicallyRegisteringFeatureClass
이 이벤트는 요청 중에 클래스 기반 기능이 처음으로 동적으로 확인될 때 발생합니다.
Laravel\Pennant\Events\UnexpectedNullScopeEncountered
이 이벤트는 null을 지원하지 않는 기능 정의에 null 범위가 전달될 때 발생합니다.
이 상황은 정상적으로 처리되며 기능은 false를 반환합니다. 그러나 이 기능의 기본 정상적인 동작을 선택 해제하고 싶다면 애플리케이션의 AppServiceProvider의 boot 메서드에서 이 이벤트에 대한 리스너를 등록할 수 있습니다.
use Illuminate\Support\Facades\Log;use Laravel\Pennant\Events\UnexpectedNullScopeEncountered; /** * Bootstrap any application services. */public function boot(): void{ Event::listen(UnexpectedNullScopeEncountered::class, fn () => abort(500));}
Laravel\Pennant\Events\FeatureUpdated
이 이벤트는 일반적으로 activate 또는 deactivate를 호출하여 범위에 대한 기능을 업데이트할 때 발생합니다.
Laravel\Pennant\Events\FeatureUpdatedForAllScopes
이 이벤트는 일반적으로 activateForEveryone 또는 deactivateForEveryone을 호출하여 모든 범위에 대한 기능을 업데이트할 때 발생합니다.
Laravel\Pennant\Events\FeatureDeleted
이 이벤트는 일반적으로 forget을 호출하여 범위에 대한 기능을 삭제할 때 발생합니다.
Laravel\Pennant\Events\FeaturesPurged
이 이벤트는 특정 기능을 제거할 때 발생합니다.
Laravel\Pennant\Events\AllFeaturesPurged
이 이벤트는 모든 기능을 제거할 때 발생합니다.