> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# Laravel Pennant

> Aprende a gestionar feature flags con Laravel Pennant. Desde la definición y comprobación de las features hasta el uso de scopes y las pruebas.

## Qué es Laravel Pennant

[Laravel Pennant](https://github.com/laravel/pennant) es un paquete ligero y sencillo para gestionar feature flags. Con ellos puedes desplegar nuevas funcionalidades de forma progresiva, realizar tests A/B o complementar el desarrollo trunk-based.

### Qué son las feature flags

```mermaid theme={null}
flowchart TD
    A["Petición"] --> B{"Feature::active('new-api')"}
    B -->|true| C["Nueva lógica de la API"]
    B -->|false| D["Lógica antigua de la API"]
    C --> E["Respuesta"]
    D --> E
```

Las feature flags separan el despliegue del código de su lanzamiento. Puedes desplegar el código en producción y activar o desactivar la funcionalidad únicamente mediante configuración.

***

## Instalación

<Steps>
  <Step title="Instalar el paquete">
    Instala Pennant con Composer.

    ```bash theme={null}
    composer require laravel/pennant
    ```
  </Step>

  <Step title="Publicar la configuración y las migraciones">
    Publica los archivos con el comando Artisan `vendor:publish`.

    ```bash theme={null}
    php artisan vendor:publish --provider="Laravel\Pennant\PennantServiceProvider"
    ```

    Se generan `config/pennant.php` y las migraciones en `database/migrations`.
  </Step>

  <Step title="Ejecutar las migraciones">
    Se crea la tabla `features`, en la que Pennant guarda los valores de las feature flags.

    ```bash theme={null}
    php artisan migrate
    ```
  </Step>
</Steps>

***

## Configuración

En `config/pennant.php` configuras el driver de almacenamiento. Pennant admite dos drivers:

| Driver     | Descripción                                             |
| ---------- | ------------------------------------------------------- |
| `database` | Persiste los valores en una BD relacional (por defecto) |
| `array`    | Almacena en memoria (para tests y usos puntuales)       |

```php theme={null}
// config/pennant.php
'default' => env('PENNANT_STORE', 'database'),
```

***

## Definir features

### Definición basada en closure

Define las features con el método `define` del facade `Feature`. Habitualmente se hace en el `boot` de un service provider. La closure recibe un «scope» (por defecto, el usuario autenticado).

```php theme={null}
<?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),
        });
    }
}
```

Lógica de la feature del ejemplo:

* Los miembros del equipo interno la tienen siempre ON.
* Los clientes de alto tráfico la tienen OFF.
* El resto tienen un 1 % de probabilidad de tenerla ON.

La primera vez que se comprueba la feature se persiste el resultado de la closure. Las siguientes veces se utiliza el valor almacenado.

<Info>
  Si la definición solo devuelve una `Lottery`, puedes omitir la closure.

  ```php theme={null}
  Feature::define('site-redesign', Lottery::odds(1, 1000));
  ```
</Info>

### Definición basada en clase

Pennant también permite definir features como clase. No es necesario registrarlas en el service provider.

```bash theme={null}
php artisan pennant:feature NewApi
```

La clase generada se ubica en `app/Features`. Basta con implementar el método `resolve`.

```php theme={null}
<?php

namespace App\Features;

use App\Models\User;
use Illuminate\Support\Lottery;

class NewApi
{
    /**
     * Resuelve el valor inicial de la feature
     */
    public function resolve(User $user): mixed
    {
        return match (true) {
            $user->isInternalTeamMember() => true,
            $user->isHighTrafficCustomer() => false,
            default => Lottery::odds(1 / 100),
        };
    }
}
```

#### Personalizar el nombre almacenado

Por defecto se guarda el nombre de clase totalmente cualificado. Puedes personalizarlo con el atributo `Name`.

```php theme={null}
use Laravel\Pennant\Attributes\Name;

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

#### Interceptar la comprobación con `before`

En las features basadas en clase puedes definir el método `before`. Se ejecuta en memoria antes de acudir al almacenamiento; si devuelve un valor distinto de `null`, ese será el resultado.

```php theme={null}
class NewApi
{
    public function before(User $user): mixed
    {
        if (Config::get('features.new-api.disabled')) {
            return $user->isInternalTeamMember();
        }
    }

    public function resolve(User $user): mixed
    {
        // ...
    }
}
```

<Tip>
  `before` resulta útil para desactivar una feature de emergencia ante un bug o para programar un lanzamiento a una fecha concreta.
</Tip>

***

## Comprobar features

### `Feature::active()` / `Feature::inactive()`

Con `active` compruebas si la feature está activa. Por defecto se comprueba contra el usuario autenticado.

```php theme={null}
use Laravel\Pennant\Feature;

if (Feature::active('new-api')) {
    // Lógica para la nueva API
}
```

En features basadas en clase pasa el nombre de la clase.

```php theme={null}
use App\Features\NewApi;

if (Feature::active(NewApi::class)) {
    // ...
}
```

Otros métodos útiles:

```php theme={null}
// ¿Todas activas?
Feature::allAreActive(['new-api', 'site-redesign']);

// ¿Alguna activa?
Feature::someAreActive(['new-api', 'site-redesign']);

// ¿Feature inactiva?
Feature::inactive('new-api');

// ¿Todas inactivas?
Feature::allAreInactive(['new-api', 'site-redesign']);

// ¿Alguna inactiva?
Feature::someAreInactive(['new-api', 'site-redesign']);
```

### Ejecución condicional (`when` / `unless`)

Con `when` ejecutas una closure solo si la feature está activa.

```php theme={null}
return Feature::when(NewApi::class,
    fn () => $this->resolveNewApiResponse($request),
    fn () => $this->resolveLegacyApiResponse($request),
);
```

`unless` hace lo contrario: ejecuta la primera closure cuando la feature está inactiva.

```php theme={null}
return Feature::unless(NewApi::class,
    fn () => $this->resolveLegacyApiResponse($request),
    fn () => $this->resolveNewApiResponse($request),
);
```

### Trait `HasFeatures`

Añadiendo `HasFeatures` al modelo `User`, puedes comprobar features directamente desde el modelo.

```php theme={null}
use Laravel\Pennant\Concerns\HasFeatures;

class User extends Authenticatable
{
    use HasFeatures;
}
```

```php theme={null}
if ($user->features()->active('new-api')) {
    // ...
}

// Obtener el valor
$value = $user->features()->value('purchase-button');

// Ejecución condicional
$user->features()->when('new-api',
    fn () => /* ... */,
    fn () => /* ... */,
);
```

### Directivas Blade

En las plantillas Blade dispones de la directiva `@feature`.

```blade theme={null}
@feature('site-redesign')
    {{-- 'site-redesign' está activo --}}
@else
    {{-- 'site-redesign' está inactivo --}}
@endfeature

@featureany(['site-redesign', 'beta'])
    {{-- Alguna de las dos está activa --}}
@endfeatureany
```

### Middleware

El middleware `EnsureFeaturesAreActive` indica que una ruta requiere una feature activa. Si no lo está, devuelve `400 Bad Request`.

```php theme={null}
use Laravel\Pennant\Middleware\EnsureFeaturesAreActive;

Route::get('/api/servers', function () {
    // ...
})->middleware(EnsureFeaturesAreActive::using('new-api', 'servers-api'));
```

Con `whenInactive` puedes personalizar la respuesta.

```php theme={null}
EnsureFeaturesAreActive::whenInactive(
    function (Request $request, array $features) {
        return new Response(status: 403);
    }
);
```

### Caché en memoria

Dentro de una misma petición, Pennant cachea en memoria los resultados de las features. Comprobar la misma flag varias veces no genera consultas adicionales.

Para vaciar la caché manualmente utiliza `flushCache`.

```php theme={null}
Feature::flushCache();
```

***

## Scopes

### Indicar el scope

Por defecto el scope es el usuario autenticado, pero puedes indicar cualquier otro con `for`.

```php theme={null}
// Comprobar para un usuario concreto
Feature::for($user)->active('new-api');

// Comprobar para un equipo
Feature::for($user->team)->active('billing-v2');
```

Ejemplo de gestión por equipo:

```php theme={null}
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);
});
```

### Personalizar el scope por defecto

Con `Feature::resolveScopeUsing` puedes personalizar el scope por defecto.

```php theme={null}
Feature::resolveScopeUsing(fn ($driver) => Auth::user()?->team);
```

Después, si omites `for`, se usa el scope por defecto.

```php theme={null}
Feature::active('billing-v2');
// Equivalente a:
Feature::for($user->team)->active('billing-v2');
```

### Scope opcional (nullable)

Cuando el scope es `null` (rutas sin autenticación, comandos Artisan...), si la definición no lo admite se devuelve automáticamente `false`. Si necesitas gestionar el caso null, tipa el argumento como nullable.

```php theme={null}
Feature::define('new-api', fn (User|null $user) => match (true) {
    $user === null => true,
    $user->isInternalTeamMember() => true,
    $user->isHighTrafficCustomer() => false,
    default => Lottery::odds(1 / 100),
});
```

***

## Valores enriquecidos

Las features pueden devolver valores distintos de un booleano. Por ejemplo, para controlar el color de un botón en un test A/B.

```php theme={null}
Feature::define('purchase-button', fn (User $user) => Arr::random([
    'blue-sapphire',
    'seafoam-green',
    'tart-orange',
]));
```

Para obtener el valor utiliza `value`.

```php theme={null}
$color = Feature::value('purchase-button');
```

En Blade puedes ramificar por valor.

```blade theme={null}
@feature('purchase-button', 'blue-sapphire')
    {{-- blue-sapphire activo --}}
@elsefeature('purchase-button', 'seafoam-green')
    {{-- seafoam-green activo --}}
@elsefeature('purchase-button', 'tart-orange')
    {{-- tart-orange activo --}}
@endfeature
```

<Info>
  Cuando se usan valores enriquecidos, cualquier valor distinto de `false` se considera activo.
</Info>

Si el valor enriquecido se pasa a `when`, la primera closure recibe dicho valor.

```php theme={null}
Feature::when('purchase-button',
    fn ($color) => /* $color contiene el valor */,
    fn () => /* Inactivo */,
);
```

***

## Obtener varias features

Con `values` puedes obtener varios valores a la vez.

```php theme={null}
Feature::values(['billing-v2', 'purchase-button']);

// [
//     'billing-v2' => false,
//     'purchase-button' => 'blue-sapphire',
// ]
```

Con `all` obtienes el valor de todas las features definidas.

```php theme={null}
Feature::all();
```

Para incluir las features basadas en clase en el resultado de `all`, llama a `discover` en un service provider.

```php theme={null}
Feature::discover();
```

Con eso se registran todas las clases del directorio `app/Features`.

***

## Eager loading

Si compruebas features dentro de un bucle pueden aparecer problemas de rendimiento. Con `load` puedes precargar los valores.

```php theme={null}
// Mal: se genera una consulta por iteración
foreach ($users as $user) {
    if (Feature::for($user)->active('notifications-beta')) {
        $user->notify(new RegistrationSuccess);
    }
}

// Bien: se cargan todos de antemano
Feature::for($users)->load(['notifications-beta']);

foreach ($users as $user) {
    if (Feature::for($user)->active('notifications-beta')) {
        $user->notify(new RegistrationSuccess);
    }
}
```

Para cargar solo lo que aún no está cargado, utiliza `loadMissing`.

```php theme={null}
Feature::for($users)->loadMissing([
    'new-api',
    'purchase-button',
    'notifications-beta',
]);
```

***

## Actualizar valores

### Actualización manual

`activate` y `deactivate` cambian el estado ON/OFF.

```php theme={null}
// Activar para el scope por defecto
Feature::activate('new-api');

// Desactivar para un scope concreto
Feature::for($user->team)->deactivate('billing-v2');

// Asignar valor enriquecido
Feature::activate('purchase-button', 'seafoam-green');
```

Para olvidar el valor almacenado utiliza `forget`. La próxima comprobación reevaluará la definición.

```php theme={null}
Feature::forget('purchase-button');
```

### Actualización masiva

`activateForEveryone` y `deactivateForEveryone` aplican el valor a todos los scopes almacenados.

```php theme={null}
Feature::activateForEveryone('new-api');
Feature::activateForEveryone('purchase-button', 'seafoam-green');
Feature::deactivateForEveryone('new-api');
```

### Purgar features

Si eliminas o cambias una feature, puedes borrar los valores del almacenamiento con `purge`.

```php theme={null}
// Una sola feature
Feature::purge('new-api');

// Varias
Feature::purge(['new-api', 'purchase-button']);

// Todas
Feature::purge();
```

También puedes hacerlo desde Artisan, ideal para integrarlo en la pipeline de despliegue.

```bash theme={null}
php artisan pennant:purge new-api

# Varias
php artisan pennant:purge new-api purchase-button

# Todas salvo las indicadas
php artisan pennant:purge --except=new-api --except=purchase-button

# Todas menos las registradas en un service provider
php artisan pennant:purge --except-registered
```

***

## Tests

### Redefinir features

En los tests puedes controlar el valor devuelto redefiniendo la feature con `Feature::define`.

```php tab=Pest theme={null}
use Laravel\Pennant\Feature;

test('permite controlar el valor de la feature', function () {
    Feature::define('purchase-button', 'seafoam-green');

    expect(Feature::value('purchase-button'))->toBe('seafoam-green');
});
```

```php tab=PHPUnit theme={null}
use Laravel\Pennant\Feature;

public function test_it_can_control_feature_values(): void
{
    Feature::define('purchase-button', 'seafoam-green');

    $this->assertSame('seafoam-green', Feature::value('purchase-button'));
}
```

Lo mismo con features basadas en clase.

```php tab=Pest theme={null}
test('permite controlar el valor de la feature', function () {
    Feature::define(NewApi::class, true);

    expect(Feature::value(NewApi::class))->toBeTrue();
});
```

```php tab=PHPUnit theme={null}
use App\Features\NewApi;

public function test_it_can_control_feature_values(): void
{
    Feature::define(NewApi::class, true);

    $this->assertTrue(Feature::value(NewApi::class));
}
```

### Configurar el store para pruebas

Puedes indicar el store con una variable de entorno en `phpunit.xml`.

```xml theme={null}
<?xml version="1.0" encoding="UTF-8"?>
<phpunit colors="true">
    <php>
        <env name="PENNANT_STORE" value="array"/>
    </php>
</phpunit>
```

***

## Drivers personalizados

Si los drivers integrados no cumplen tus necesidades, puedes crear uno personalizado implementando `Laravel\Pennant\Contracts\Driver`.

```php theme={null}
<?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 {}
}
```

Regístralo con `extend` en el método `boot` de un service provider.

```php theme={null}
Feature::extend('redis', function (Application $app) {
    return new RedisFeatureDriver($app->make('redis'), $app->make('events'), []);
});
```

Después indícalo en `config/pennant.php`.

```php theme={null}
'stores' => [
    'redis' => [
        'driver' => 'redis',
        'connection' => null,
    ],
],
```

***

## Resumen

| Objetivo                   | Cómo hacerlo                                  |
| -------------------------- | --------------------------------------------- |
| Instalar Pennant           | `composer require laravel/pennant`            |
| Definir una feature        | `Feature::define('name', fn ($user) => ...)`  |
| Comprobar una feature      | `Feature::active('name')`                     |
| Comprobar en Blade         | `@feature('name') ... @endfeature`            |
| Actualizar el valor        | `Feature::activate('name')` / `deactivate`    |
| Aplicar a todos los scopes | `Feature::activateForEveryone('name')`        |
| Controlarla en tests       | Redefinir con `Feature::define('name', true)` |
| Purgar del almacenamiento  | `Feature::purge('name')`                      |

## Próximos pasos

<Columns cols={2}>
  <Card title="Debug y gestión de errores" icon="circle-x" href="/es/error-handling">
    Aprende cómo maneja Laravel las excepciones y los informes de errores.
  </Card>

  <Card title="Laravel Pulse" icon="chart-line" href="/es/pulse">
    Añade a tu aplicación un panel de monitorización de rendimiento.
  </Card>
</Columns>


## Related topics

- [Casos de uso prácticos de Laravel Pennant](/es/blog/laravel-pennant.md)
- [Laravel Boost](/es/boost.md)
- [Laravel Telescope](/es/telescope.md)
- [Laravel Bluesky](/es/packages/laravel-bluesky/index.md)
- [Laravel Octane](/es/octane.md)
