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 packageacme/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.
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.
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()
Danssrc/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.
DailyReport implémentant Report, puis l’ajoute au même tag dans le register() de son propre provider.
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.
$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.
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 unbind() 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 danstests/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.
Sources primaires consultées
- Documentation officielle de Laravel : Tagging
- Documentation officielle de Laravel : Binding primitives・Variadic tag dependencies
- Documentation officielle de Laravel : fournisseurs de services
- Documentation officielle de Laravel : développement de packages
- Container : tag(), tagged()
- ContextualBindingBuilder : giveTagged()
- RewindableGenerator : nouvelle énumération et nombre d’éléments
- Attribut Tag : resolve()
- Test officiel : ContainerTaggingTest
- Test officiel : ContextualBindingTest