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

# Casos de uso prácticos de Laravel Pennant

> Se explican patrones prácticos de Laravel Pennant más allá de lo básico. Presentamos scopes por equipo, kill switches de emergencia, dark launches, comandos de gestión y otros conocimientos que no se ven solo con la documentación oficial.

Para el uso básico, consulta la [página de la guía](/es/pennant). En esta página presentamos patrones prácticos que no aparecen en la documentación oficial.

***

## Scope por equipo en un SaaS multitenant

Si quieres gestionar las flags por equipo (tenant) en lugar de por usuario individual, cambia el scope por defecto al equipo.

```php theme={null}
// AppServiceProvider
Feature::resolveScopeUsing(fn () => Auth::user()?->currentTeam);
```

Solo con esto, `Feature::active('billing-v2')` apunta automáticamente al equipo actual. Sea quien sea el miembro del equipo, mientras pertenezca al mismo equipo se devuelve el mismo resultado, con lo que se mantiene la coherencia de la UI.

Ejemplo de rollout progresivo según la fecha de registro del equipo.

```php theme={null}
use App\Models\Team;
use Illuminate\Support\Carbon;
use Illuminate\Support\Lottery;
use Laravel\Pennant\Feature;

Feature::define('new-billing', function (Team $team) {
    // Equipos registrados desde 2024 en adelante: activo de inmediato
    if ($team->created_at->isAfter(new Carbon('2024-01-01'))) {
        return true;
    }

    // Registrados entre 2022 y 2023: rollout del 10 %
    if ($team->created_at->isAfter(new Carbon('2022-01-01'))) {
        return Lottery::odds(1 / 10);
    }

    // Equipos antiguos: prudentemente al 1 %
    return Lottery::odds(1 / 100);
});
```

Si quieres activarlo solo para equipos concretos (entrega anticipada a clientes enterprise, por ejemplo):

```php theme={null}
// Tras el despliegue, activar manualmente solo para equipos específicos
Feature::for($earlyAccessTeam)->activate('new-billing');
```

***

## Kill switch de emergencia (aprovechando el método `before`)

Si se descubre un bug en producción, puedes desactivar la funcionalidad al instante sin hacer rollback de código. Si añades un método `before` a una feature basada en clase, se comprueba antes que el valor de almacenamiento.

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

namespace App\Features;

use App\Models\User;
use Illuminate\Support\Facades\Config;

class NewCheckout
{
    /**
     * Interruptor de emergencia que permite desactivar al instante desde config al ocurrir un bug en producción
     */
    public function before(User $user): mixed
    {
        // Control desde el valor de config/features.php o env
        if (Config::boolean('features.new_checkout_disabled', false)) {
            return false; // Desactivación inmediata para todos
        }

        // Los administradores siempre lo tienen activo (para depuración)
        if ($user->isAdmin()) {
            return true;
        }

        return null; // Devolver null hace que pase a resolve() normal
    }

    public function resolve(User $user): mixed
    {
        return $user->isPremium() || Lottery::odds(1 / 5);
    }
}
```

Con solo establecer la variable de entorno `FEATURES_NEW_CHECKOUT_DISABLED=true`, se puede parar la funcionalidad sin tocar la BD. Es un kill switch sin despliegue.

<Tip>
  Si `before` devuelve `null`, se ejecuta `resolve()`. Si devuelve `false`, se trata inmediatamente como inactivo. Salvo en emergencias, devuelve `null`.
</Tip>

***

## Rollout programado

Un caso en el que quieres publicar automáticamente para todos los usuarios en una fecha/hora concreta. Puede implementarse con el método `before`.

```php theme={null}
public function before(User $user): mixed
{
    $rolloutDate = Config::get('features.new_api.rollout_date');

    if ($rolloutDate && Carbon::parse($rolloutDate)->isPast()) {
        return true; // Al pasar la fecha de rollout, se publica para todos
    }

    return null; // Si aún no, pasa a resolve() normal
}

public function resolve(User $user): mixed
{
    // Antes del rollout, solo miembros internos
    return $user->isInternalTeamMember();
}
```

```ini theme={null}
# .env
FEATURES_NEW_API_ROLLOUT_DATE=2025-04-01
```

Con esto, al pasar el `2025-04-01` se publica automáticamente para todos los usuarios. Se automatiza sin despliegue, sin BD y sin comando Artisan.

***

## Dark launch (Shadow Mode)

Es un patrón en el que ejecutas la lógica nueva con datos de producción sin mostrársela al usuario, y comparas los resultados con la lógica antigua. Si no hay problemas, basta con activar la flag para completar la release.

```php theme={null}
class RecommendationController
{
    public function index(Request $request)
    {
        $legacyResult = $this->getLegacyRecommendations($request->user());

        // En modo shadow ejecuta el nuevo algoritmo y compara, pero muestra la lógica antigua
        if (Feature::for($request->user())->active('recommendation-v2-shadow')) {
            try {
                $newResult = $this->getNewRecommendations($request->user());

                // Registra las diferencias en el log (sin mostrárselas al usuario)
                if ($legacyResult !== $newResult) {
                    Log::channel('shadow_mode')->info('recommendation diff', [
                        'user_id' => $request->user()->id,
                        'legacy'  => $legacyResult,
                        'new'     => $newResult,
                    ]);
                }
            } catch (\Throwable $e) {
                Log::error('shadow mode error', ['error' => $e->getMessage()]);
            }
        }

        // La respuesta es siempre el resultado de la lógica antigua
        return response()->json($legacyResult);
    }
}
```

Revisando los logs, cuando ya no queden diferencias, basta con conmutar la flag a `recommendation-v2`.

***

## Recolección de resultados de A/B testing mediante eventos

En la documentación oficial hay una mención al evento `FeatureRetrieved`, pero no se muestran patrones reales de agregación de A/B testing.

El evento `FeatureResolved` se dispara solo la **primera** vez que se resuelve el valor de la feature. Aprovéchalo para registrar la asignación de variante al usuario.

```php theme={null}
use Laravel\Pennant\Events\FeatureResolved;
use Illuminate\Support\Facades\Event;

// Registrar en AppServiceProvider o EventServiceProvider
Event::listen(function (FeatureResolved $event) {
    if ($event->feature !== 'purchase-button') {
        return;
    }

    // Registrar en el momento de la asignación de variante al usuario
    analytics()->identify($event->scope?->id, [
        'ab_purchase_button' => $event->value,
    ]);
});
```

Si además registras por separado cuando ocurre la conversión, puedes agregar la tasa de conversión por variante.

```php theme={null}
// Al completar la compra
Event::dispatch(new PurchaseCompleted($user, $product));
```

```php theme={null}
// Listener PurchaseCompleted
analytics()->track('purchase_completed', [
    'user_id'              => $user->id,
    'ab_purchase_button'   => Feature::for($user)->value('purchase-button'),
]);
```

<Info>
  Diferencia entre `FeatureResolved` y `FeatureRetrieved`: `FeatureResolved` se dispara solo en la primera evaluación; `FeatureRetrieved` se dispara en cada comprobación. Para registrar asignaciones es adecuado `FeatureResolved`, y para seguimiento de vistas de página, `FeatureRetrieved`.
</Info>

***

## Scope en jobs encolados

En los jobs de la cola no hay usuario autenticado, por lo que las comprobaciones de feature pueden comportarse de forma inesperada. Da un scope explícito al job.

```php theme={null}
class SendWeeklyDigest implements ShouldQueue
{
    public function __construct(
        private readonly User $user
    ) {}

    public function handle(): void
    {
        // Mal: sin usuario autenticado, siempre da false
        // if (Feature::active('new-digest-layout')) { ... }

        // Bien: pasar el usuario explícitamente
        if (Feature::for($this->user)->active('new-digest-layout')) {
            $this->sendNewLayout($this->user);
        } else {
            $this->sendLegacyLayout($this->user);
        }
    }
}
```

También es posible evaluar la flag en el momento del dispatch y pasarla al job.

```php theme={null}
// Evaluar en el controlador y pasar al job
$useNewLayout = Feature::for($user)->active('new-digest-layout');

SendWeeklyDigest::dispatch($user, $useNewLayout);
```

***

## UI de gestión mediante un comando Artisan

Si creas un comando Artisan sencillo para gestionar flags, puedes operar las flags de producción sin despliegue.

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

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Laravel\Pennant\Feature;

class ManageFeatureFlag extends Command
{
    protected $signature = 'feature {action : activate|deactivate|status} {feature : Feature name}';
    protected $description = 'Gestiona feature flags';

    public function handle(): void
    {
        $feature = $this->argument('feature');

        match ($this->argument('action')) {
            'activate'   => $this->activate($feature),
            'deactivate' => $this->deactivate($feature),
            'status'     => $this->status($feature),
            default      => $this->error('Acción desconocida'),
        };
    }

    private function activate(string $feature): void
    {
        Feature::activateForEveryone($feature);
        $this->info("✓ Se ha activado {$feature} para todos los usuarios");
    }

    private function deactivate(string $feature): void
    {
        Feature::deactivateForEveryone($feature);
        $this->info("✓ Se ha desactivado {$feature} para todos los usuarios");
    }

    private function status(string $feature): void
    {
        $count = \DB::table('features')
            ->where('name', $feature)
            ->where('value', json_encode(true))
            ->count();

        $this->line("{$feature}: activo en {$count} scopes");
    }
}
```

```shell theme={null}
php artisan feature activate new-checkout
php artisan feature deactivate new-checkout
php artisan feature status new-checkout
```

***

## Refactorización segura del nombre de la feature (atributo `Name`)

Al renombrar una feature basada en clase, si cambia el nombre de la flag guardada en la BD, se resetean todas las flags de los usuarios. Fija el nombre de guardado con el atributo `Name`.

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

// Nombre de clase antiguo: CheckoutV2 → nuevo nombre: NewCheckoutExperience
// En la BD siempre se guarda como 'checkout-v2'
#[Name('checkout-v2')]
class NewCheckoutExperience
{
    public function resolve(User $user): mixed
    {
        return $user->isPremium();
    }
}
```

Con esto puedes refactorizar libremente el nombre de la clase manteniendo los datos de la BD.

***

## Conclusión

Una vez asimilados los fundamentos de la documentación oficial, con los siguientes patrones se aprovecha todo el valor de Pennant.

| Patrón                     | Aplicación                                             |
| -------------------------- | ------------------------------------------------------ |
| Scope de equipo            | SaaS multitenant                                       |
| Kill switch con `before`   | Respuesta urgente a bugs de producción                 |
| Rollout programado         | Automatización de releases con fecha/hora concreta     |
| Dark launch                | Verificación segura de nuevos algoritmos en producción |
| Evento `FeatureResolved`   | Recolección precisa de resultados de A/B testing       |
| Scope explícito en jobs    | Evaluación precisa de flags en entornos de cola        |
| Comando Artisan de gestión | Operar flags de producción sin despliegue              |
| Atributo `Name`            | Refactorización segura de clases                       |

<Card title="Guía de Laravel Pennant" icon="flag" href="/es/pennant">
  Para la instalación y el uso básico, consulta la página de la guía.
</Card>


## Related topics

- [Trait InteractsWithData](/es/advanced/interacts-with-data.md)
- [Contenedor de servicios](/es/service-container.md)
- [Almacenamiento de archivos](/es/filesystem.md)
- [Comparativa de métodos de autenticación - Laravel Bluesky](/es/packages/laravel-bluesky/authentication.md)
- [AST de PHP](/es/advanced/php-ast.md)
