Passer au contenu principal
Si vous souhaitez que plusieurs packages ou l’application qui les utilise puissent ajouter des fonctionnalités à un même service d’agrégation, vous pouvez utiliser tag() et tagged() du conteneur. Le service qui agrège n’a pas besoin de connaître toutes les classes d’implémentation. Cette page cible Laravel 13 et s’appuie sur l’implémentation et les tests officiels de Framework v13.38.0. Pour les bases des liaisons, consultez le conteneur de services ; pour la distribution des providers, consultez le développement de packages.

Un tag regroupe les clés d’enregistrement des services

tag() ajoute des clés d’enregistrement du conteneur, comme des noms de classe, à la liste d’un tag. Attribuer un tag ne crée pas le service. tagged() renvoie un iterable permettant d’énumérer les services enregistrés sous ce tag. Il ne s’agit pas d’une recherche automatique des classes implémentant une interface donnée, mais d’un mécanisme auquel chaque provider participe explicitement. Ces tags sont également distincts des tags de ressources publiées par vendor:publish --tag.

Créer un package de rapports extensible

Préparer le contrat partagé par les implémentations

À titre d’exemple, créons un package acme/reports qui rassemble plusieurs rapports. On suppose une configuration PSR-4 de Composer où Acme\Reports\ correspond à src/. Dans src/Contracts/Report.php, définissez le contrat que les plugins implémentent.
Comme implémentations minimales, préparez src/RuntimeReport.php et src/MemoryReport.php.

Le service d’agrégation reçoit un iterable

src/ReportCollector.php appelle le traitement de collecte au fil de l’énumération de chaque implémentation. En définissant le nom du tag public comme constante, le code appelant peut référencer le même nom.
Le type iterable de PHP ne permet pas à lui seul de vérifier le type des éléments, et tag() ne vérifie pas non plus qu’il s’agit d’une implémentation de Report. Dans cet exemple, le service d’agrégation vérifie donc lui aussi le contrat.

N’enregistrer que les liaisons et les tags dans register()

Dans src/ReportsServiceProvider.php, enregistrez les implémentations par défaut et le service d’agrégation. La factory du service d’agrégation transmet telle quelle l’iterable obtenu.
Un autre package ou l’application qui l’utilise prépare par exemple un DailyReport implémentant Report, puis l’ajoute au même tag dans le register() de son propre provider.
Une fois tous les providers enregistrés, l’appel à app(ReportCollector::class)->collect() agrège également les implémentations ajoutées. Même si le nombre d’implémentations participantes augmente, il n’est pas nécessaire de modifier la création de ReportCollector.

tagged() et giveTagged() créent les services à des moments différents

Dans l’implémentation examinée, tagged() sur un tag enregistré renvoie un RewindableGenerator. Le make() de chaque service est appelé lors de l’énumération ; si vous interrompez l’énumération en cours de route, les services restants ne sont pas créés. De plus, RewindableGenerator crée un nouveau Generator à chaque énumération. Il ne met pas en cache les services obtenus : avec un bind() classique, une nouvelle énumération recrée aussi les instances. Les services singleton() ou scoped() suivent leurs propres règles de partage. En revanche, le giveTagged() du contextual binding convertit le résultat de tagged() en tableau avec iterator_to_array() au moment de la résolution. Si vous souhaitez résoudre les dépendances en une seule fois sous forme de tableau, vous pouvez remplacer le bind() avec factory précédent par l’enregistrement suivant.
Même si $reports est typé iterable, la valeur conservée avec cette méthode est un tableau. Le type de l’argument ne suffit pas à garantir une résolution paresseuse. Par ailleurs, giveTagged() se charge de résoudre les instances, et non d’appeler le collect() de chaque implémentation. Le resolve() de l’attribut Tag renvoie directement tagged(). Pour utiliser l’attribut, placez-le sur un argument du constructeur comme dans les attributs contextuels, et laissez le conteneur construire la classe. L’attribut ne s’applique pas automatiquement à un new dans votre propre factory.

Ne pas rompre le contrat d’enregistrement des plugins

Rendre explicites les doublons et l’ordre

tag() ajoute simplement à une liste : ajouter à nouveau la même clé d’enregistrement ne supprime pas le doublon. Si la même implémentation est énumérée deux fois, collect() est appelé deux fois, même si un singleton renvoie le même objet. Centralisez l’enregistrement du tag à un seul endroit, dans le provider responsable de chaque implémentation. bindIf() est une API qui évite d’écraser une liaison ; elle n’empêche pas les doublons lors d’un tag() ultérieur. L’ordre d’énumération correspond à l’ordre d’ajout dans la liste du tag. Les tags n’ont aucun mécanisme de tri par priorité ; ne faites donc pas dépendre l’ordre de traitement entre différents packages du seul ordre de chargement des providers. Si l’ordre fait partie de la spécification publique, incluez une priorité ou un identifiant dans le contrat et triez explicitement côté agrégation.

Enregistrer d’abord les providers chargés d’ajouter les tags

tagged() sur un tag non enregistré ne lève pas d’exception et renvoie un tableau vide. Les enregistrements de tags effectués ultérieurement ne sont pas répercutés sur un tableau vide déjà reçu, ni sur un tableau déjà créé par giveTagged(). Même pour l’énumération paresseuse d’un tag enregistré, ne comptez pas sur une modification de la liste en cours d’exécution. Dans l’implémentation examinée, tagged() transmet le nombre d’éléments au moment de l’obtention, tandis que l’énumération consulte la liste du tag. En concevant l’utilisation après la fin de l’enregistrement de tous les providers, vous évitez aussi toute incohérence entre ce nombre et les éléments énumérés.
L’énumération d’un tag ne recherche pas les providers non chargés à partir du nom du tag. Si le provider chargé d’ajouter un tag est un DeferrableProvider, l’enregistrement du tag n’est pas exécuté tant que ce service n’est pas résolu par un autre chemin, et l’implémentation peut manquer dans la liste. Pour les implémentations qui doivent toujours participer, utilisez un provider classique pour l’enregistrement du tag, et ne créez pas les implémentations lors de l’enregistrement.
Les conditions de démarrage des providers différés sont décrites dans DeferrableProvider, et le chargement à l’installation dans la découverte automatique des packages.

Séparer les tags du cycle de vie des services

Attribuer un tag ne rend pas automatiquement un service singleton ou scoped. Cet exemple utilise un bind() classique et ne stocke pas les résultats de collecte dans l’objet d’agrégation. En particulier, si vous conservez le tableau résolu par giveTagged() dans un service d’agrégation singleton, les services scoped de ce tableau peuvent eux aussi rester conservés. La réinitialisation de la portée ne remplace pas les références déjà stockées dans d’autres objets. Pour une utilisation avec Octane ou des workers de file d’attente, consultez aussi Conception du cycle de vie des services d’un package.

Tester la résolution paresseuse et la conversion en tableau

Avec l’autoload Composer du package et un environnement PHPUnit, vous pouvez ajouter le test suivant dans tests/ReportCollectorTest.php. Il appelle l’enregistrement du provider sur un conteneur seul et vérifie le nombre de créations. Complétez avec Orchestra Testbench pour les tests incluant l’intégration à l’application et la découverte automatique.
En outre, lors des mises à jour du package, vérifiez les combinaisons suivantes.

Sources primaires consultées

Dernière modification le 26 octobre 2026