> ## 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

> Gérez les feature flags avec Laravel Pennant : définition, vérification, scopes et tests.

## Qu'est-ce que Laravel Pennant

[Laravel Pennant](https://github.com/laravel/pennant) est un package léger et simple pour les feature flags. Les flags permettent un rollout progressif, des A/B tests et complètent le développement trunk-based.

### Concept

```mermaid theme={null}
flowchart TD
    A["Requête"] --> B{"Feature::active('new-api')"}
    B -->|true| C["Nouveau traitement API"]
    B -->|false| D["Ancien traitement API"]
    C --> E["Réponse"]
    D --> E
```

Les flags séparent déploiement et release : le code est en production, l'activation est contrôlée par configuration.

***

## Installation

<Steps>
  <Step title="Installer le package">
    ```bash theme={null}
    composer require laravel/pennant
    ```
  </Step>

  <Step title="Publier config et migrations">
    ```bash theme={null}
    php artisan vendor:publish --provider="Laravel\Pennant\PennantServiceProvider"
    ```

    Cela crée `config/pennant.php` et une migration dans `database/migrations`.
  </Step>

  <Step title="Migrer">
    Crée la table `features` où Pennant stocke les valeurs.

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

***

## Configuration

Deux drivers de stockage supportés :

| Driver     | Description                          |
| ---------- | ------------------------------------ |
| `database` | Persistance en RDBMS (défaut)        |
| `array`    | En mémoire (tests, usage temporaire) |

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

***

## Définition de features

### Basée sur une closure

Utilisez `Feature::define` dans un service provider `boot`. La closure reçoit le « scope » (généralement l'utilisateur authentifié).

```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),
        });
    }
}
```

Logique :

* Interne → activé
* Client à fort trafic → désactivé
* Sinon → 1 % de chances

La valeur est calculée et persistée au premier check ; les checks suivants réutilisent la valeur.

<Info>
  Si la définition retourne juste une loterie, omettez la closure.

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

### Basée sur une classe

Pas d'enregistrement dans un provider.

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

Fichier créé dans `app/Features` ; implémentez `resolve`.

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

namespace App\Features;

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

class NewApi
{
    /**
     * Résout la valeur initiale.
     */
    public function resolve(User $user): mixed
    {
        return match (true) {
            $user->isInternalTeamMember() => true,
            $user->isHighTrafficCustomer() => false,
            default => Lottery::odds(1 / 100),
        };
    }
}
```

#### Personnaliser le nom stocké

Par défaut, la classe FQN. Utilisez l'attribut `Name`.

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

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

#### Intercepter avec `before`

Cette méthode s'exécute avant la lecture du stockage : si elle retourne autre chose que `null`, cette valeur est utilisée.

```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>
  Utile pour un kill switch en cas de bug ou un rollout programmé.
</Tip>

***

## Vérification

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

Cible l'utilisateur authentifié par défaut.

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

if (Feature::active('new-api')) {
    // Nouveau code
}
```

Avec une classe :

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

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

Autres helpers :

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

### Exécution conditionnelle (`when` / `unless`)

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

`unless` inverse la logique.

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

### Trait `HasFeatures`

Ajoutez-le au modèle `User` pour interroger directement.

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

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

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

$value = $user->features()->value('purchase-button');

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

### Directives Blade

```blade theme={null}
@feature('site-redesign')
    {{-- Si actif --}}
@else
    {{-- Si inactif --}}
@endfeature

@featureany(['site-redesign', 'beta'])
    {{-- L'un ou l'autre actif --}}
@endfeatureany
```

### Middleware

`EnsureFeaturesAreActive` protège une route ; sans le flag, `400 Bad Request`.

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

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

Personnalisez la réponse via `whenInactive`.

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

### Cache en mémoire

Pennant cache les résultats par requête. Vider manuellement :

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

***

## Scopes

### Spécifier un scope

Par défaut, l'utilisateur authentifié. Avec `for`, n'importe quoi.

```php theme={null}
Feature::for($user)->active('new-api');

Feature::for($user->team)->active('billing-v2');
```

Flag par équipe :

```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);
});
```

### Personnaliser le scope par défaut

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

Après cela :

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

### Scope null

Sans authentification (Artisan, etc.), une définition non typée pour `null` retourne automatiquement `false`. Typez avec nullable si vous voulez le gérer :

```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),
});
```

***

## Valeurs riches

Les flags peuvent retourner autre chose qu'un booléen — par exemple pour un A/B test de couleur.

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

Lecture :

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

Blade branché sur la valeur :

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

<Info>
  Avec valeurs riches, tout ce qui n'est pas `false` est considéré comme actif.
</Info>

Avec `when`, la valeur est passée à la première closure.

```php theme={null}
Feature::when('purchase-button',
    fn ($color) => /* $color est la valeur */,
    fn () => /* Inactif */,
);
```

***

## Lecture multiple

`values` récupère plusieurs valeurs.

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

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

`all` récupère toutes les valeurs définies.

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

Pour inclure les classes de feature dans `all()`, appelez `discover` dans un provider.

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

Cela enregistre toutes les classes de `app/Features`.

***

## Eager loading

Dans une boucle, les checks entraînent des requêtes. Chargez à l'avance avec `load`.

```php theme={null}
// KO : requête par itération
foreach ($users as $user) {
    if (Feature::for($user)->active('notifications-beta')) {
        $user->notify(new RegistrationSuccess);
    }
}

// OK : chargement en amont
Feature::for($users)->load(['notifications-beta']);

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

`loadMissing` ne charge que ce qui manque.

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

***

## Modifier les valeurs

### Manuel

`activate` / `deactivate` :

```php theme={null}
Feature::activate('new-api');

Feature::for($user->team)->deactivate('billing-v2');

Feature::activate('purchase-button', 'seafoam-green');
```

`forget` supprime la valeur stockée ; elle sera recalculée.

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

### En masse

`activateForEveryone` / `deactivateForEveryone` s'appliquent à tous les scopes en stock.

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

### Purge

Retirez les valeurs quand vous supprimez ou modifiez une feature.

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

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

Feature::purge();
```

Version Artisan (utile en CI/CD) :

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

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

# Purger tout sauf certaines
php artisan pennant:purge --except=new-api --except=purchase-button

# Purger tout sauf ce qui est enregistré dans un provider
php artisan pennant:purge --except-registered
```

***

## Tests

### Redéfinir en test

Dans un test, redéfinissez pour contrôler la valeur.

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

```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'));
}
```

Idem avec les classes.

```php tab=Pest theme={null}
test('it can control feature values', 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));
}
```

### Store pour les tests

Réglez dans `phpunit.xml`.

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

***

## Driver personnalisé

Implémentez `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 {}
}
```

Enregistrez avec `extend`.

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

Puis dans `config/pennant.php` :

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

***

## Récapitulatif

| Objectif        | Méthode                                       |
| --------------- | --------------------------------------------- |
| Installer       | `composer require laravel/pennant`            |
| Définir         | `Feature::define('name', fn ($user) => ...)`  |
| Vérifier        | `Feature::active('name')`                     |
| Dans Blade      | `@feature('name') ... @endfeature`            |
| Modifier        | `Feature::activate('name')` / `deactivate`    |
| Tous les scopes | `Feature::activateForEveryone('name')`        |
| En test         | Redéfinir via `Feature::define('name', true)` |
| Purger          | `Feature::purge('name')`                      |

## Prochaines étapes

<Columns cols={2}>
  <Card title="Gestion des erreurs" icon="circle-x" href="/fr/error-handling">
    Traitement des exceptions et rapports.
  </Card>

  <Card title="Laravel Pulse" icon="chart-line" href="/fr/pulse">
    Tableau de bord de performance.
  </Card>
</Columns>


## Related topics

- [Cas d'usage pratiques de Laravel Pennant](/fr/blog/laravel-pennant.md)
- [Laravel Boost](/fr/boost.md)
- [Laravel Telescope](/fr/telescope.md)
- [Laravel Octane](/fr/octane.md)
- [Laravel Bluesky](/fr/packages/laravel-bluesky/index.md)
