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, 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
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.
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 danssrc/MessageFormatter.php. Les classes suivantes sont également supposées se trouver chacune dans un fichier PHP du même nom sous src/.
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.
$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().
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.
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. Si vous enregistrez A puis B, le résultat estB(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.
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.
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 derebinding() 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.
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 leregister() 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.
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
Hooks de résolution des services d'un package
Présente callAfterResolving(), qui configure les objets sans les remplacer, et les précautions sur le nombre d’applications.
Conception du cycle de vie des services d'un package
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
- Documentation officielle de Laravel : Service Providers
- Documentation officielle de Laravel : développement de packages et découverte automatique
- Container : extend(), resolve(), rebound(), instance(), forgetExtenders()
- Tests officiels : création différée, enregistrement avant le binding, alias, plusieurs extenders
- ServiceProvider : constructeur et register()