- Introducción
- Instalación
- Configuración
- Definición de Features
- Verificación de Features
- Scope
- Valores enriquecidos de Features
- Recuperar múltiples Features
- Eager Loading
- Actualizar valores
- Testing
- Agregar drivers personalizados a Pennant
- Eventos
#Introducción
Laravel Pennant es un paquete simple y ligero para feature flags, sin complicaciones innecesarias. Los feature flags le permiten desplegar nuevas funcionalidades de la aplicación de forma incremental y con confianza, realizar pruebas A/B de nuevos diseños de interfaz, complementar una estrategia de desarrollo trunk-based, y mucho más.
#Instalación
Primero, instale Pennant en su proyecto usando el gestor de paquetes Composer:
composer require laravel/pennant
Luego, debe publicar los archivos de configuración y migración de Pennant usando el comando Artisan vendor:publish:
php artisan vendor:publish --provider="Laravel\Pennant\PennantServiceProvider"
Finalmente, debe ejecutar las migraciones de base de datos de su aplicación. Esto creará una tabla features que Pennant utiliza para su driver database:
php artisan migrate
#Configuración
Después de publicar los assets de Pennant, su archivo de configuración estará ubicado en config/pennant.php. Este archivo le permite especificar el mecanismo de almacenamiento por defecto que Pennant usará para guardar los valores resueltos de los feature flags.
Pennant incluye soporte para almacenar los valores resueltos de los feature flags en un array en memoria mediante el driver array. O bien, Pennant puede almacenar estos valores de forma persistente en una base de datos relacional mediante el driver database, que es el mecanismo de almacenamiento por defecto.
#Definición de Features
Para definir un feature, puede usar el método define que ofrece el facade Feature. Debe proporcionar un nombre para el feature, así como un closure que será invocado para resolver el valor inicial del feature.
Normalmente, los features se definen en un service provider usando el facade Feature. El closure recibirá el "scope" para la verificación del feature. Lo más común es que el scope sea el usuario autenticado actualmente. En este ejemplo, definiremos un feature para desplegar incrementalmente una nueva API a los usuarios de nuestra aplicación:
<?php
namespace App\Providers;
use App\Models\User;
use Illuminate\Support\Lottery;
use Illuminate\Support\ServiceProvider;
use Laravel\Pennant\Feature;
class AppServiceProvider extends ServiceProvider
{
/**
* Inicializar cualquier servicio de la aplicación.
*/
public function boot(): void
{
Feature::define('new-api', fn (User $user) => match (true) {
$user->isInternalTeamMember() => true,
$user->isHighTrafficCustomer() => false,
default => Lottery::odds(1 / 100),
});
}
}
Como puede ver, tenemos las siguientes reglas para nuestro feature:
- Todos los miembros internos del equipo deberían usar la nueva API.
- Los clientes con alto tráfico no deberían usar la nueva API.
- De lo contrario, el feature se asignará aleatoriamente a los usuarios con una probabilidad de 1 en 100 de estar activo.
La primera vez que se verifica el feature new-api para un usuario dado, el resultado del closure será almacenado por el driver de almacenamiento. La próxima vez que se verifique el feature para el mismo usuario, el valor se recuperará del almacenamiento y el closure no será invocado.
Para mayor comodidad, si la definición de un feature solo devuelve una lotería, puede omitir el closure completamente:
Feature::define('site-redesign', Lottery::odds(1, 1000));
#Features basados en clases
Pennant también permite definir features basados en clases. A diferencia de las definiciones basadas en closures, no es necesario registrar un feature basado en clase en un service provider. Para crear un feature basado en clase, puede invocar el comando Artisan pennant:feature. Por defecto, la clase del feature se ubicará en el directorio app/Features de su aplicación:
php artisan pennant:feature NewApi
Al escribir una clase de feature, solo necesita definir un método resolve, que será invocado para resolver el valor inicial del feature para un scope dado. Nuevamente, el scope normalmente será el usuario autenticado actualmente:
<?php
namespace App\Features;
use Illuminate\Support\Lottery;
class NewApi
{
/**
* Resolver el valor inicial del feature.
*/
public function resolve(User $user): mixed
{
return match (true) {
$user->isInternalTeamMember() => true,
$user->isHighTrafficCustomer() => false,
default => Lottery::odds(1 / 100),
};
}
}
Las clases de feature se resuelven a través del container, por lo que puede inyectar dependencias en el constructor de la clase de feature cuando sea necesario.
#Personalizar el nombre almacenado del feature
Por defecto, Pennant almacenará el nombre de clase completamente calificado de la clase del feature. Si desea desacoplar el nombre almacenado del feature de la estructura interna de la aplicación, puede especificar una propiedad $name en la clase del feature. El valor de esta propiedad se almacenará en lugar del nombre de la clase:
<?php
namespace App\Features;
class NewApi
{
/**
* El nombre almacenado del feature.
*
* @var string
*/
public $name = 'new-api';
// ...
}
#Verificación de Features
Para determinar si un feature está activo, puede usar el método active del facade Feature. Por defecto, los features se verifican contra el usuario autenticado actualmente:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Laravel\Pennant\Feature;
class PodcastController
{
/**
* Mostrar una lista del recurso.
*/
public function index(Request $request): Response
{
return Feature::active('new-api')
? $this->resolveNewApiResponse($request)
: $this->resolveLegacyApiResponse($request);
}
// ...
}
Aunque por defecto los features se verifican contra el usuario autenticado, puede fácilmente verificar el feature contra otro usuario o scope. Para ello, use el método for que ofrece el facade Feature:
return Feature::for($user)->active('new-api')
? $this->resolveNewApiResponse($request)
: $this->resolveLegacyApiResponse($request);
Pennant también ofrece algunos métodos adicionales que pueden ser útiles para determinar si un feature está activo o no:
// Determinar si todos los features dados están activos...
Feature::allAreActive(['new-api', 'site-redesign']);
// Determinar si alguno de los features dados está activo...
Feature::someAreActive(['new-api', 'site-redesign']);
// Determinar si un feature está inactivo...
Feature::inactive('new-api');
// Determinar si todos los features dados están inactivos...
Feature::allAreInactive(['new-api', 'site-redesign']);
// Determinar si alguno de los features dados está inactivo...
Feature::someAreInactive(['new-api', 'site-redesign']);
Cuando use Pennant fuera de un contexto HTTP, como en un comando Artisan o un job en cola, normalmente debería especificar explícitamente el scope del feature. Alternativamente, puede definir un scope por defecto que cubra tanto contextos HTTP autenticados como no autenticados.
#Verificación de Features basados en clases
Para features basados en clases, debe proporcionar el nombre de la clase al verificar el feature:
<?php
namespace App\Http\Controllers;
use App\Features\NewApi;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Laravel\Pennant\Feature;
class PodcastController
{
/**
* Mostrar una lista del recurso.
*/
public function index(Request $request): Response
{
return Feature::active(NewApi::class)
? $this->resolveNewApiResponse($request)
: $this->resolveLegacyApiResponse($request);
}
// ...
}
#Ejecución condicional
El método when puede usarse para ejecutar fluidamente un closure dado si un feature está activo. Además, puede proporcionarse un segundo closure que se ejecutará si el feature está inactivo:
<?php
namespace App\Http\Controllers;
use App\Features\NewApi;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Laravel\Pennant\Feature;
class PodcastController
{
/**
* Display a listing of the resource.
*/
public function index(Request $request): Response
{
return Feature::when(NewApi::class,
fn () => $this->resolveNewApiResponse($request),
fn () => $this->resolveLegacyApiResponse($request),
);
}
// ...
}
El método unless funciona como el inverso de when, ejecutando el primer closure si el feature está inactivo:
return Feature::unless(NewApi::class,
fn () => $this->resolveLegacyApiResponse($request),
fn () => $this->resolveNewApiResponse($request),
);
#El trait HasFeatures
El trait HasFeatures de Pennant puede añadirse al modelo User de su aplicación (o a cualquier otro modelo que tenga features) para proporcionar una forma fluida y conveniente de verificar features directamente desde el modelo:
<?php
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Laravel\Pennant\Concerns\HasFeatures;
class User extends Authenticatable
{
use HasFeatures;
// ...
}
Una vez que el trait ha sido añadido a su modelo, puede verificar fácilmente features invocando el método features:
if ($user->features()->active('new-api')) {
// ...
}
Por supuesto, el método features proporciona acceso a muchos otros métodos convenientes para interactuar con features:
// Valores...
$value = $user->features()->value('purchase-button')
$values = $user->features()->values(['new-api', 'purchase-button']);
// Estado...
$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']);
// Ejecución condicional...
$user->features()->when('new-api',
fn () => /* ... */,
fn () => /* ... */,
);
$user->features()->unless('new-api',
fn () => /* ... */,
fn () => /* ... */,
);
#Directiva Blade
Para facilitar la verificación de features en Blade, Pennant ofrece una directiva @feature:
@feature('site-redesign')
<!-- 'site-redesign' está activo -->
@else
<!-- 'site-redesign' está inactivo -->
@endfeature
#Middleware
Pennant también incluye un middleware que puede usarse para verificar que el usuario autenticado actualmente tenga acceso a un feature antes de que se invoque una ruta. Puede asignar el middleware a una ruta y especificar los features requeridos para acceder a ella. Si alguno de los features especificados está inactivo para el usuario autenticado, la ruta devolverá una respuesta HTTP 400 Bad Request. Puede pasar múltiples features al método estático using.
use Illuminate\Support\Facades\Route;
use Laravel\Pennant\Middleware\EnsureFeaturesAreActive;
Route::get('/api/servers', function () {
// ...
})->middleware(EnsureFeaturesAreActive::using('new-api', 'servers-api'));
#Personalizar la respuesta
Si desea personalizar la respuesta que devuelve el middleware cuando uno de los features listados está inactivo, puede usar el método whenInactive que proporciona el middleware EnsureFeaturesAreActive. Normalmente, este método debe invocarse dentro del método boot de uno de los service providers de su aplicación:
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Laravel\Pennant\Middleware\EnsureFeaturesAreActive;
/**
* Inicializar cualquier servicio de la aplicación.
*/
public function boot(): void
{
EnsureFeaturesAreActive::whenInactive(
function (Request $request, array $features) {
return new Response(status: 403);
}
);
// ...
}
#Cache en memoria
Al verificar un feature, Pennant crea una cache en memoria del resultado. Si está usando el driver database, esto significa que volver a verificar el mismo feature dentro de una sola petición no disparará consultas adicionales a la base de datos. Esto también asegura que el feature tenga un resultado consistente durante la duración de la petición.
Si necesita vaciar manualmente la cache en memoria, puede usar el método flushCache que ofrece el facade Feature:
Feature::flushCache();
#Scope
#Especificar el scope
Como se mencionó, normalmente los features se verifican contra el usuario autenticado actualmente. Sin embargo, esto puede no ajustarse siempre a sus necesidades. Por lo tanto, es posible especificar el scope contra el cual desea verificar un feature usando el método for del facade Feature:
return Feature::for($user)->active('new-api')
? $this->resolveNewApiResponse($request)
: $this->resolveLegacyApiResponse($request);
Por supuesto, los scopes de features no se limitan a "usuarios". Imagine que ha construido una nueva experiencia de facturación que está desplegando a equipos completos en lugar de usuarios individuales. Quizás quiera que los equipos más antiguos tengan un despliegue más lento que los equipos nuevos. Su closure de resolución del feature podría verse algo así:
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);
});
Notará que el closure que definimos no espera un User, sino un modelo Team. Para determinar si este feature está activo para el equipo de un usuario, debe pasar el equipo al método for que ofrece el facade Feature:
if (Feature::for($user->team)->active('billing-v2')) {
return redirect()->to('/billing/v2');
}
// ...
#Scope por defecto
También es posible personalizar el scope por defecto que Pennant usa para verificar features. Por ejemplo, tal vez todos sus features se verifiquen contra el equipo del usuario autenticado en lugar del usuario. En lugar de tener que llamar a Feature::for($user->team) cada vez que verifica un feature, puede especificar el equipo como scope por defecto. Normalmente, esto se hace en uno de los service providers de su aplicación:
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\ServiceProvider;
use Laravel\Pennant\Feature;
class AppServiceProvider extends ServiceProvider
{
/**
* Inicializar cualquier servicio de la aplicación.
*/
public function boot(): void
{
Feature::resolveScopeUsing(fn ($driver) => Auth::user()?->team);
// ...
}
}
Si no se proporciona un scope explícito mediante el método for, la verificación del feature ahora usará el equipo del usuario autenticado como scope por defecto:
Feature::active('billing-v2');
// Ahora es equivalente a...
Feature::for($user->team)->active('billing-v2');
#Scope nullable
Si el scope que proporciona al verificar un feature es null y la definición del feature no soporta null mediante un tipo nullable o incluyendo null en un tipo unión, Pennant devolverá automáticamente false como valor del feature.
Por lo tanto, si el scope que pasa a un feature puede ser null y desea que se invoque el resolvedor del valor del feature, debe contemplar esto en la definición del feature. Un scope null puede ocurrir si verifica un feature dentro de un comando Artisan, un job en cola o una ruta no autenticada. Dado que usualmente no hay un usuario autenticado en estos contextos, el scope por defecto será null.
Si no siempre especifica explícitamente el scope de su feature, debe asegurarse de que el tipo del scope sea "nullable" y manejar el valor null dentro de la lógica de definición del feature:
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),
});
#Identificación del scope
Los drivers de almacenamiento incorporados array y database de Pennant saben cómo almacenar correctamente identificadores de scope para todos los tipos de datos PHP así como para modelos Eloquent. Sin embargo, si su aplicación utiliza un driver de Pennant de terceros, ese driver puede no saber cómo almacenar correctamente un identificador para un modelo Eloquent u otros tipos personalizados en su aplicación.
Por ello, Pennant le permite formatear los valores de scope para almacenamiento implementando el contrato FeatureScopeable en los objetos de su aplicación que se usan como scopes en Pennant.
Por ejemplo, imagine que está usando dos drivers de feature diferentes en una sola aplicación: el driver incorporado database y un driver de terceros llamado "Flag Rocket". El driver "Flag Rocket" no sabe cómo almacenar correctamente un modelo Eloquent. En cambio, requiere una instancia FlagRocketUser. Implementando el método toFeatureIdentifier definido por el contrato FeatureScopeable, podemos personalizar el valor de scope almacenable que se proporciona a cada driver usado por nuestra aplicación:
<?php
namespace App\Models;
use FlagRocket\FlagRocketUser;
use Illuminate\Database\Eloquent\Model;
use Laravel\Pennant\Contracts\FeatureScopeable;
class User extends Model implements FeatureScopeable
{
/**
* Convertir el objeto a un identificador de scope para el driver dado.
*/
public function toFeatureIdentifier(string $driver): mixed
{
return match($driver) {
'database' => $this,
'flag-rocket' => FlagRocketUser::fromId($this->flag_rocket_id),
};
}
}
#Serialización del scope
Por defecto, Pennant usará el nombre de clase completamente calificado al almacenar un feature asociado con un modelo Eloquent. Si ya está usando un morph map de Eloquent, puede elegir que Pennant también use el morph map para desacoplar el feature almacenado de la estructura de su aplicación.
Para lograr esto, después de definir su morph map de Eloquent en un service provider, puede invocar el método useMorphMap del facade Feature:
use Illuminate\Database\Eloquent\Relations\Relation;
use Laravel\Pennant\Feature;
Relation::enforceMorphMap([
'post' => 'App\Models\Post',
'video' => 'App\Models\Video',
]);
Feature::useMorphMap();
#Valores enriquecidos de Features
Hasta ahora, hemos mostrado principalmente features en un estado binario, es decir, que están "activos" o "inactivos", pero Pennant también le permite almacenar valores enriquecidos.
Por ejemplo, imagine que está probando tres nuevos colores para el botón "Comprar ahora" de su aplicación. En lugar de devolver true o false desde la definición del feature, puede devolver una cadena:
use Illuminate\Support\Arr;
use Laravel\Pennant\Feature;
Feature::define('purchase-button', fn (User $user) => Arr::random([
'blue-sapphire',
'seafoam-green',
'tart-orange',
]));
Puede recuperar el valor del feature purchase-button usando el método value:
$color = Feature::value('purchase-button');
La directiva Blade incluida en Pennant también facilita renderizar contenido condicionalmente basado en el valor actual del feature:
@feature('purchase-button', 'blue-sapphire')
<!-- 'blue-sapphire' está activo -->
@elsefeature('purchase-button', 'seafoam-green')
<!-- 'seafoam-green' está activo -->
@elsefeature('purchase-button', 'tart-orange')
<!-- 'tart-orange' está activo -->
@endfeature
Cuando usa valores enriquecidos, es importante saber que un feature se considera "activo" cuando tiene cualquier valor distinto de false.
Al invocar el método condicional when, el valor enriquecido de la característica se pasará a la primera función anónima:
Feature::when('purchase-button',
fn ($color) => /* ... */,
fn () => /* ... */,
);
De igual forma, al llamar al método condicional unless, el valor enriquecido del feature se proporcionará al segundo closure opcional:
Feature::unless('purchase-button',
fn () => /* ... */,
fn ($color) => /* ... */,
);
#Recuperar múltiples Features
El método values permite recuperar múltiples features para un scope dado:
Feature::values(['billing-v2', 'purchase-button']);
// [
// 'billing-v2' => false,
// 'purchase-button' => 'blue-sapphire',
// ]
O puede usar el método all para recuperar los valores de todos los features definidos para un scope dado:
Feature::all();
// [
// 'billing-v2' => false,
// 'purchase-button' => 'blue-sapphire',
// 'site-redesign' => true,
// ]
Sin embargo, los features basados en clases se registran dinámicamente y no son conocidos por Pennant hasta que se verifican explícitamente. Esto significa que los features basados en clases de su aplicación pueden no aparecer en los resultados devueltos por el método all si no han sido verificados durante la petición actual.
Si desea asegurarse de que las clases de feature siempre se incluyan al usar el método all, puede usar las capacidades de descubrimiento de features de Pennant. Para comenzar, invoque el método discover en uno de los service providers de su aplicación:
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Laravel\Pennant\Feature;
class AppServiceProvider extends ServiceProvider
{
/**
* Inicializar cualquier servicio de la aplicación.
*/
public function boot(): void
{
Feature::discover();
// ...
}
}
El método discover registrará todas las clases de feature en el directorio app/Features de su aplicación. El método all ahora incluirá estas clases en sus resultados, independientemente de si han sido verificadas durante la petición actual:
Feature::all();
// [
// 'App\Features\NewApi' => true,
// 'billing-v2' => false,
// 'purchase-button' => 'blue-sapphire',
// 'site-redesign' => true,
// ]
#Carga ansiosa (Eager Loading)
Aunque Pennant mantiene una cache en memoria de todos los features resueltos para una sola petición, aún es posible encontrar problemas de rendimiento. Para aliviar esto, Pennant ofrece la capacidad de eager load de valores de features.
Para ilustrar esto, imagine que está verificando si un feature está activo dentro de un bucle:
use Laravel\Pennant\Feature;
foreach ($users as $user) {
if (Feature::for($user)->active('notifications-beta')) {
$user->notify(new RegistrationSuccess);
}
}
Asumiendo que está usando el driver de base de datos, este código ejecutará una consulta a la base de datos por cada usuario en el bucle, ejecutando potencialmente cientos de consultas. Sin embargo, usando el método load de Pennant, podemos eliminar este posible cuello de botella cargando anticipadamente los valores de features para una colección de usuarios o scopes:
Feature::for($users)->load(['notifications-beta']);
foreach ($users as $user) {
if (Feature::for($user)->active('notifications-beta')) {
$user->notify(new RegistrationSuccess);
}
}
Para cargar valores de features solo cuando no hayan sido cargados previamente, puede usar el método loadMissing:
Feature::for($users)->loadMissing([
'new-api',
'purchase-button',
'notifications-beta',
]);
#Actualizar valores
Cuando se resuelve el valor de un feature por primera vez, el driver subyacente almacenará el resultado. Esto es necesario para asegurar una experiencia consistente para sus usuarios a través de las peticiones. Sin embargo, en ocasiones puede querer actualizar manualmente el valor almacenado del feature.
Para ello, puede usar los métodos activate y deactivate para activar o desactivar un feature:
use Laravel\Pennant\Feature;
// Activar el feature para el scope por defecto...
Feature::activate('new-api');
// Desactivar el feature para el scope dado...
Feature::for($user->team)->deactivate('billing-v2');
También es posible establecer manualmente un valor enriquecido para un feature proporcionando un segundo argumento al método activate:
Feature::activate('purchase-button', 'seafoam-green');
Para indicar a Pennant que olvide el valor almacenado de un feature, puede usar el método forget. Cuando el feature se verifique nuevamente, Pennant resolverá el valor desde la definición del feature:
Feature::forget('purchase-button');
#Actualizaciones masivas
Para actualizar valores almacenados de features en masa, puede usar los métodos activateForEveryone y deactivateForEveryone.
Por ejemplo, imagine que ahora está seguro de la estabilidad del feature new-api y ha decidido el mejor color 'purchase-button' para su flujo de checkout — puede actualizar el valor almacenado para todos los usuarios en consecuencia:
use Laravel\Pennant\Feature;
Feature::activateForEveryone('new-api');
Feature::activateForEveryone('purchase-button', 'seafoam-green');
Alternativamente, puede desactivar el feature para todos los usuarios:
Feature::deactivateForEveryone('new-api');
Esto solo actualizará los valores resueltos que han sido almacenados por el driver de almacenamiento de Pennant. También deberá actualizar la definición del feature en su aplicación.
#Purgar Features
A veces puede ser útil purgar un feature completo del almacenamiento. Esto suele ser necesario si ha eliminado el feature de su aplicación o ha realizado ajustes en la definición del feature que desea desplegar a todos los usuarios.
Puede eliminar todos los valores almacenados para un feature usando el método purge:
// Purgar un solo feature...
Feature::purge('new-api');
// Purgar múltiples features...
Feature::purge(['new-api', 'purchase-button']);
Si desea purgar todos los features del almacenamiento, puede invocar el método purge sin argumentos:
Feature::purge();
Como puede ser útil purgar features como parte de la pipeline de despliegue de su aplicación, Pennant incluye un comando Artisan pennant:purge que purgará los features proporcionados del almacenamiento:
php artisan pennant:purge new-api
php artisan pennant:purge new-api purchase-button
También es posible purgar todos los features excepto aquellos en una lista dada. Por ejemplo, imagine que quiere purgar todos los features pero mantener los valores para los features "new-api" y "purchase-button" en el almacenamiento. Para lograr esto, puede pasar esos nombres de features a la opción --except:
php artisan pennant:purge --except=new-api --except=purchase-button
Para mayor comodidad, el comando pennant:purge también soporta una bandera --except-registered. Esta bandera indica que se deben purgar todos los features excepto aquellos registrados explícitamente en un service provider:
php artisan pennant:purge --except-registered
#Testing
Al testear código que interactúa con feature flags, la forma más sencilla de controlar el valor devuelto por el feature flag en sus tests es simplemente redefinir el feature. Por ejemplo, imagine que tiene el siguiente feature definido en uno de los service providers de su aplicación:
use Illuminate\Support\Arr;
use Laravel\Pennant\Feature;
Feature::define('purchase-button', fn () => Arr::random([
'blue-sapphire',
'seafoam-green',
'tart-orange',
]));
Para modificar el valor devuelto del feature en sus tests, puede redefinir el feature al inicio del test. El siguiente test siempre pasará, aunque la implementación de Arr::random() siga presente en el 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'));
}
El mismo enfoque puede usarse para features basados en clases:
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));
}
Si su feature devuelve una instancia de Lottery, hay varios helpers de testing disponibles.
#Configuración de la store
Puede configurar la store que Pennant usará durante las pruebas definiendo la variable de entorno PENNANT_STORE en el archivo phpunit.xml de su aplicación:
<?xml version="1.0" encoding="UTF-8"?>
<phpunit colors="true">
<!-- ... -->
<php>
<env name="PENNANT_STORE" value="array"/>
<!-- ... -->
</php>
</phpunit>
#Agregar drivers personalizados a Pennant
#Implementar el driver
Si ninguno de los drivers de almacenamiento existentes de Pennant se ajusta a las necesidades de su aplicación, puede escribir su propio driver de almacenamiento. Su driver personalizado debe implementar la interfaz 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 {}
}
Ahora, solo necesitamos implementar cada uno de estos métodos usando una conexión Redis. Para un ejemplo de cómo implementar cada uno de estos métodos, consulte el Laravel\Pennant\Drivers\DatabaseDriver en el código fuente de Pennant
Laravel no incluye un directorio para contener sus extensiones. Usted es libre de colocarlas donde prefiera. En este ejemplo, hemos creado un directorio Extensions para alojar el RedisFeatureDriver.
#Registrar el driver
Una vez que su driver ha sido implementado, está listo para registrarlo con Laravel. Para agregar drivers adicionales a Pennant, puede usar el método extend que proporciona el facade Feature. Debe llamar al método extend desde el método boot de uno de los service providers de su aplicación:
<?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
{
/**
* Registrar cualquier servicio de la aplicación.
*/
public function register(): void
{
// ...
}
/**
* Inicializar cualquier servicio de la aplicación.
*/
public function boot(): void
{
Feature::extend('redis', function (Application $app) {
return new RedisFeatureDriver($app->make('redis'), $app->make('events'), []);
});
}
}
Una vez que el driver ha sido registrado, puede usar el driver redis en el archivo de configuración config/pennant.php de su aplicación:
'stores' => [
'redis' => [
'driver' => 'redis',
'connection' => null,
],
// ...
],
#Eventos
Pennant despacha una variedad de eventos que pueden ser útiles para rastrear feature flags a lo largo de su aplicación.
#Laravel\Pennant\Events\RetrievingKnownFeature
Este evento se despacha la primera vez que se recupera un feature conocido durante una petición para un scope específico. Este evento puede ser útil para crear y rastrear métricas sobre los feature flags que se están usando en su aplicación.
#Laravel\Pennant\Events\RetrievingUnknownFeature
Este evento se despacha la primera vez que se recupera un feature desconocido durante una petición para un scope específico. Este evento puede ser útil si tenía la intención de eliminar un feature flag, pero puede que haya dejado referencias dispersas a él en su aplicación.
Por ejemplo, puede ser útil escuchar este evento y report o lanzar una excepción cuando ocurra:
<?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
{
/**
* Registrar cualquier otro evento para su aplicación.
*/
public function boot(): void
{
Event::listen(function (RetrievingUnknownFeature $event) {
report("Resolving unknown feature [{$event->feature}].");
});
}
}
#Laravel\Pennant\Events\DynamicallyDefiningFeature
Este evento se despacha cuando un feature basado en clase está siendo verificado dinámicamente por primera vez durante una petición.