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

# Décoration de services et extend dans un package

> Analysez Container::extend() de Laravel 13 pour comprendre les décorateurs qui respectent le contrat, l'enregistrement avant et après la résolution, la durée de conservation de la décoration d'un singleton, la double décoration et les tests lors des mises à jour.

Un package doit parfois ajouter un comportement tout en conservant l'implémentation du service choisie par l'application consommatrice. Avec `extend()` du conteneur, vous pouvez envelopper le service résolu dans un décorateur sans redéfinir la logique de création d'origine.

Cette page s'appuie sur la branche par défaut `13.x` de la documentation officielle de Laravel ainsi que sur l'implémentation et les tests officiels de la dernière version du Framework, `v13.40.0`, vérifiés le 9 novembre 2026. En plus de la différence avec les [hooks de résolution des services](https://kawax.biz/fr/advanced/package-service-resolution.md), elle explique la durée de conservation d'une décoration, qui change selon **qu'une instance est déjà stockée ou non au moment de l'enregistrement**.

## Séparer l'ajout de configuration du remplacement de l'objet

| API | Objectif | Valeur de retour du callback |
| - | - | - |
| `bind()`, `singleton()` | Enregistrer la façon de créer un service et s'il est partagé | Le service à créer |
| `afterResolving()` | Appliquer une configuration ou autre au service résolu | N'est pas utilisée pour remplacer le service |
| `extend()` | Conserver la logique de création et décorer ou modifier le service | Doit toujours renvoyer le service modifié |

Le callback de `extend()` reçoit le service cible et le conteneur. Vous pouvez configurer le même objet et le renvoyer, ou renvoyer un nouveau décorateur. Si vous oubliez le `return`, `null` est transmis à la suite de la résolution.

> [!NOTE]
>
> Il s'agit ici de `Illuminate\Container\Container::extend()`. C'est une API différente du `extend()` des managers comme Cache ou Mail, qui enregistre un nom de driver et sa logique de création.

## Créer un décorateur qui implémente le même contrat

### Envelopper l'implémentation d'origine sans la remplacer

Pour l'exemple, placez le contrat suivant de formatage des messages dans `src/MessageFormatter.php`. Les classes suivantes sont également supposées se trouver chacune dans un fichier PHP du même nom sous `src/`.

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

namespace Acme\Messages;

interface MessageFormatter
{
    public function format(string $message): string;
}
```

L'implémentation non décorée renvoie l'entrée telle quelle.

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

namespace Acme\Messages;

final class PlainMessageFormatter implements MessageFormatter
{
    public function format(string $message): string
    {
        return $message;
    }
}
```

Le décorateur implémente la même interface et délègue au service qu'il reçoit. Dans cet exemple, il ajoute un préfixe propre au package à la valeur renvoyée par le délégué.

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

namespace Acme\Messages;

final class PrefixedMessageFormatter implements MessageFormatter
{
    public function __construct(
        private readonly MessageFormatter $inner,
    ) {}

    public function format(string $message): string
    {
        return '[Acme] '.$this->inner->format($message);
    }
}
```

Si les consommateurs typent la classe concrète `PlainMessageFormatter`, un décorateur qui implémente seulement la même interface ne peut pas la remplacer. Exposez `MessageFormatter` comme dépendance et respectez le contrat sur les arguments, les valeurs de retour, les exceptions, etc. Le simple transfert dynamique de méthodes ne remplace pas l'implémentation d'une interface PHP.

### Enregistrer la décoration dans un provider

`src/PrefixServiceProvider.php`, qui fournit la décoration, ne redéfinit pas la façon de créer le service. Il se contente d'enregistrer une extension dans le conteneur. Pour le chargement du provider, consultez [Fonctionnement interne de la découverte automatique des packages](https://kawax.biz/fr/advanced/package-discovery.md).

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

namespace Acme\Messages;

use Illuminate\Support\ServiceProvider;

final class PrefixServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->extend(
            MessageFormatter::class,
            static fn (MessageFormatter $formatter): MessageFormatter =>
                new PrefixedMessageFormatter($formatter),
        );
    }
}
```

Le callback reçoit aussi le conteneur en deuxième argument, mais cet exemple n'en a pas besoin, il n'est donc pas déclaré. Utilisez l'argument `$formatter` comme service cible. Appeler de nouveau `make()` sur la même clé dans le callback peut provoquer une résolution circulaire.

Liez l'implémentation d'origine dans l'application consommatrice, par exemple dans `AppServiceProvider::register()`.

```php theme={null}
use Acme\Messages\MessageFormatter;
use Acme\Messages\PlainMessageFormatter;

$this->app->bind(MessageFormatter::class, PlainMessageFormatter::class);
```

Vous pouvez aussi enregistrer `extend()` avant le `bind()` de la cible. Si la cible n'a pas été résolue et qu'aucune instance n'est stockée, l'enregistrement seul ne crée pas le service d'origine. Les tests officiels vérifient aussi la création différée et l'enregistrement avant le binding.

Ensuite, `app(MessageFormatter::class)->format('Expédié.')` renvoie `[Acme] Expédié.`. Même si l'implémentation d'origine est remplacée par une autre implémentation de `MessageFormatter`, le décorateur enveloppe cette implémentation.

> [!WARNING]
>
> En règle générale, enregistrez avant la première résolution. Un autre provider peut utiliser le service en premier, donc déplacer simplement l'enregistrement dans `boot()` n'empêche pas une résolution antérieure. De plus, si vous rendez différé à la légère un provider qui ne fait qu'enregistrer des extensions, l'enregistrement lui-même peut perdre sa condition de déclenchement.

## Comprendre la cible et l'ordre de la décoration

Lors d'une résolution normale, le conteneur crée le service d'origine, le remplace par la valeur de retour de chaque extender dans l'ordre d'enregistrement, le stocke comme instance partagée, puis appelle les hooks post-résolution.

```mermaid theme={null}
flowchart LR
    A["Création d'origine"] --> B["Service"]
    B --> C["extender A<br>Envelopper avec Wrapper A"]
    C --> D["extender B<br>Envelopper avec Wrapper B"]
    D --> E["Stocker si partagé"]
    E --> F["Hooks post-résolution et consommateurs"]
```

Si vous enregistrez A puis B, le résultat est `B(A(service d'origine))`. Les appels de méthodes des consommateurs commencent par le B extérieur : ne confondez pas l'ordre d'enregistrement et l'ordre de démarrage du traitement. Si vous dépendez de l'ordre de décoration de plusieurs packages, rendez l'ordre explicite dans l'application consommatrice et testez la combinaison.

Les extenders s'appliquent à la **clé de résolution** après que le conteneur a normalisé les alias. Un enregistrement sur `MessageFormatter::class` ne s'applique pas lorsque `PlainMessageFormatter::class` est résolu directement simplement parce que les types correspondent. Passez systématiquement par l'interface.

## La décoration d'une instance stockée est conservée différemment

`Container::extend()` dans `v13.40.0` se comporte différemment selon qu'une instance de la cible est stockée ou non dans le conteneur.

| État à l'enregistrement | Comportement de `extend()` | Conservé pour les futures créations |
| - | - | - |
| Pas encore résolu et aucune instance stockée | Ajoute l'extender au tableau | Appliqué lors des créations normales suivantes |
| Un `bind()` normal a déjà été résolu, mais aucune instance n'est stockée | Ajoute l'extender et tente une notification rebound | Appliqué lors des créations normales suivantes |
| Un objet singleton ou `instance()` est stocké | Le décore immédiatement, remplace la valeur stockée par la valeur de retour et envoie une notification rebound | **Cet extender n'est pas ajouté au tableau** |

La dernière ligne est importante. Si vous décorez un singleton déjà résolu puis appelez `forgetInstance()` ou liez de nouveau la même clé, **une décoration appliquée seulement sur le moment** n'est pas réappliquée à la création suivante. C'est différent d'un enregistrement dans le tableau des extenders avant la première résolution.

`scoped()` utilise aussi des instances stockées : ne supposez pas qu'une décoration enregistrée tardivement au milieu d'un scope reste enregistrée pour le scope suivant. Vérifiez-le avec la [conception du cycle de vie des services d'un package](https://kawax.biz/fr/advanced/package-service-lifetimes.md).

Par ailleurs, si vous stockez plus tard un objet directement avec `instance()`, il ne passe pas par la boucle qui applique les extenders lors des créations normales. Y compris pour les substitutions en test, « c'est enregistré, donc l'objet que je passe est décoré automatiquement » n'est pas toujours vrai.

### Les références déjà distribuées ne sont pas remplacées automatiquement

Lorsque vous renvoyez un nouveau wrapper, comme dans cet exemple, l'objet d'origine déjà stocké dans une propriété d'un autre service ne change pas, même si la valeur stockée dans le conteneur change. La notification de `rebinding()` transmet le nouveau service aux callbacks enregistrés. Elle ne recherche ni ne réécrit toutes les dépendances.

L'enregistrement d'un callback de notification avec `rebinding()` n'est pas non plus toujours différé. Si la cible est déjà liée, le résultat de `make()` est renvoyé dès l'enregistrement. Sur le chemin où vous appliquez `extend()` à un binding normal déjà résolu, `make()` peut aussi être appelé pour la notification s'il existe un callback rebound. Ne supposez pas de manière générale que l'initialisation et les effets de bord ont lieu « à la prochaine utilisation ».

## Éviter la double décoration et l'état reporté

`extend()` ne déduplique pas automatiquement les enregistrements de la même closure ou du même décorateur. Si le `register()` de ce provider est appliqué deux fois au même conteneur, le préfixe peut aussi être ajouté deux fois.

* Regroupez l'enregistrement des décorations dans le chemin de démarrage fixe du provider, et ne le répétez pas à chaque requête ou job.
* N'empêchez pas les doublons avec un unique drapeau statique partagé par tous les conteneurs. Les tests et les nouvelles applications perdraient des enregistrements nécessaires.
* La décoration d'un singleton est conservée pendant la même durée partagée. Ne figez pas l'utilisateur courant, la requête ou un état similaire dans le wrapper.
* `forgetExtenders()` ne fait qu'effacer le tableau des enregistrements. Il ne rétablit pas les wrappers déjà créés.

Cet exemple modifie la chaîne de sortie. Le préfixe, le nombre d'appels délégués et la gestion des exceptions font aussi partie du comportement public du package : ne les traitez pas comme un simple « changement interne d'implémentation » et vérifiez la compatibilité.

## Tester le moment de l'enregistrement avec le même conteneur

L'exemple PHPUnit suivant suppose que les classes ci-dessus peuvent être chargées via l'autoload du package. Il applique directement le `register()` du provider à un Container et vérifie la durée de conservation de la décoration sans HTTP ni services externes. Vérifiez séparément la découverte automatique et le démarrage complet de l'application avec [Tester des packages Laravel avec Orchestra Testbench](https://kawax.biz/fr/advanced/package-testing.md).

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

namespace Acme\Messages\Tests;

use Acme\Messages\MessageFormatter;
use Acme\Messages\PlainMessageFormatter;
use Acme\Messages\PrefixedMessageFormatter;
use Acme\Messages\PrefixServiceProvider;
use Illuminate\Container\Container;
use PHPUnit\Framework\TestCase;

final class ServiceDecorationTest extends TestCase
{
    public function test_extension_is_lazy_and_can_precede_binding(): void
    {
        $container = new Container;
        $builds = 0;

        (new PrefixServiceProvider($container))->register();
        $container->bind(MessageFormatter::class, function () use (&$builds) {
            $builds++;

            return new PlainMessageFormatter;
        });

        $this->assertSame(0, $builds);

        $first = $container->make(MessageFormatter::class);
        $second = $container->make(MessageFormatter::class);

        $this->assertSame('[Acme] ready', $first->format('ready'));
        $this->assertSame('[Acme] ready', $second->format('ready'));
        $this->assertNotSame($first, $second);
        $this->assertSame(2, $builds);
    }

    public function test_early_extension_survives_singleton_recreation(): void
    {
        $container = new Container;
        $container->singleton(MessageFormatter::class, PlainMessageFormatter::class);
        (new PrefixServiceProvider($container))->register();

        $first = $container->make(MessageFormatter::class);

        $this->assertInstanceOf(PrefixedMessageFormatter::class, $first);
        $this->assertSame($first, $container->make(MessageFormatter::class));

        $container->forgetInstance(MessageFormatter::class);
        $second = $container->make(MessageFormatter::class);

        $this->assertNotSame($first, $second);
        $this->assertSame('[Acme] ready', $second->format('ready'));
    }

    public function test_late_singleton_extension_is_not_retained_as_a_callback(): void
    {
        $container = new Container;
        $container->singleton(MessageFormatter::class, PlainMessageFormatter::class);
        $original = $container->make(MessageFormatter::class);

        (new PrefixServiceProvider($container))->register();
        $decorated = $container->make(MessageFormatter::class);

        $this->assertInstanceOf(PrefixedMessageFormatter::class, $decorated);
        $this->assertNotSame($original, $decorated);
        $this->assertSame('ready', $original->format('ready'));
        $this->assertSame('[Acme] ready', $decorated->format('ready'));

        $container->forgetInstance(MessageFormatter::class);
        $recreated = $container->make(MessageFormatter::class);

        $this->assertInstanceOf(PlainMessageFormatter::class, $recreated);
        $this->assertSame('ready', $recreated->format('ready'));
    }
}
```

Le dernier test ne recommande pas l'enregistrement tardif : il rend explicite la limite dans la version vérifiée. Ne concevez pas `forgetInstance()` seul comme une API de suppression ou de réenregistrement des décorations.

Avant une version, vérifiez aussi l'ordre de plusieurs extenders, les enregistrements en double, la substitution par `instance()` et les références déjà détenues par les consommateurs. Lorsque vous ajoutez des versions de Laravel prises en charge, relisez les branches de `extend()` et les tests officiels, en plus des clés de résolution et des contrats de types.

## Pages associées

> [card] [Hooks de résolution des services d'un package](https://kawax.biz/fr/advanced/package-service-resolution.md)
>
> Présente callAfterResolving(), qui configure les objets sans les remplacer, et les précautions sur le nombre d'applications.

> [card] [Conception du cycle de vie des services d'un package](https://kawax.biz/fr/advanced/package-service-lifetimes.md)
>
> Présente bind, singleton et scoped, ainsi que les dépendances conservées par les services déjà créés.

## Sources primaires consultées

* [Documentation officielle de Laravel : Extending bindings et Rebinding](https://github.com/laravel/docs/blob/13.x/container.md#extending-bindings)
* [Documentation officielle de Laravel : Service Providers](https://github.com/laravel/docs/blob/13.x/providers.md)
* [Documentation officielle de Laravel : développement de packages et découverte automatique](https://github.com/laravel/docs/blob/13.x/packages.md)
* [Container : extend(), resolve(), rebound(), instance(), forgetExtenders()](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Container/Container.php)
* [Tests officiels : création différée, enregistrement avant le binding, alias, plusieurs extenders](https://github.com/laravel/framework/blob/v13.40.0/tests/Container/ContainerExtendTest.php)
* [ServiceProvider : constructeur et register()](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Support/ServiceProvider.php)


## Related topics

- [Hooks de résolution des services d'un package](https://kawax.biz/fr/advanced/package-service-resolution.md)
- [Sujets avancés](https://kawax.biz/fr/advanced/index.md)
- [Enregistrer et tester des transports de mail dans un package](https://kawax.biz/fr/advanced/package-mail-transports.md)
- [Canaux de notification personnalisés dans un package et vérification de l'envoi](https://kawax.biz/fr/advanced/package-notification-channels.md)
- [Étendre et résoudre à nouveau les canaux de log d'un package](https://kawax.biz/fr/advanced/package-logging.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.