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

# Tags de conteneur et agrégation de plugins d'un package

> Agrégez les implémentations de plusieurs packages avec les tags du conteneur de Laravel 13, et examinez la résolution paresseuse de tagged(), la différence avec giveTagged(), les enregistrements en double et le cycle de vie.

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](https://kawax.biz/fr/service-container.md) ; pour la distribution des providers, consultez le [développement de packages](https://kawax.biz/fr/advanced/package-development.md).

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

```mermaid theme={null}
flowchart TD
    A["register() de chaque provider"] --> B["bind() de l'implémentation<br>et ajout au même tag"]
    B --> C["Résolution du service d'agrégation"]
    C --> D["Conservation de l'iterable de tagged()"]
    D --> E["Énumération avec foreach"]
    E --> F["make() pour chaque clé d'enregistrement"]
    F --> G["Appel de collect() de l'implémentation"]
```

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

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

namespace Acme\Reports\Contracts;

interface Report
{
    public function collect(): array;
}
```

Comme implémentations minimales, préparez `src/RuntimeReport.php` et `src/MemoryReport.php`.

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

namespace Acme\Reports;

use Acme\Reports\Contracts\Report;

class RuntimeReport implements Report
{
    public function collect(): array
    {
        return ['php_version' => PHP_VERSION];
    }
}
```

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

namespace Acme\Reports;

use Acme\Reports\Contracts\Report;

class MemoryReport implements Report
{
    public function collect(): array
    {
        return ['allocated_bytes' => memory_get_usage(true)];
    }
}
```

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

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

namespace Acme\Reports;

use Acme\Reports\Contracts\Report;
use UnexpectedValueException;

class ReportCollector
{
    public const TAG = 'acme.reports.collectors';

    /** @param iterable<Report> $reports */
    public function __construct(private iterable $reports)
    {
    }

    public function collect(): array
    {
        $results = [];

        foreach ($this->reports as $report) {
            if (! $report instanceof Report) {
                throw new UnexpectedValueException('Tagged services must implement Report.');
            }

            $results[] = $report->collect();
        }

        return $results;
    }
}
```

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.

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

namespace Acme\Reports;

use Illuminate\Contracts\Container\Container;
use Illuminate\Support\ServiceProvider;

class ReportsServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->bind(RuntimeReport::class);
        $this->app->bind(MemoryReport::class);

        $this->app->tag([
            RuntimeReport::class,
            MemoryReport::class,
        ], ReportCollector::TAG);

        $this->app->bind(ReportCollector::class, function (Container $app): ReportCollector {
            return new ReportCollector($app->tagged(ReportCollector::TAG));
        });
    }
}
```

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.

```php theme={null}
use Acme\Reports\ReportCollector;
use App\Reports\DailyReport;

$this->app->bind(DailyReport::class);
$this->app->tag(DailyReport::class, ReportCollector::TAG);
```

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.

| Méthode d'injection | Moment de la résolution de chaque service | Nouvelle énumération sur le même objet d'agrégation |
| - | - | - |
| Transmettre tel quel le résultat de `tagged()` | Lors de l'énumération de chaque élément | Nouvel appel à `make()` |
| Argument `iterable` avec `#[Tag(...)]` | Lors de l'énumération de chaque élément | Nouvel appel à `make()` |
| Injection avec `giveTagged()` | Lors de la résolution des dépendances du service d'agrégation | Réutilisation des éléments du tableau conservé |

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.

```php theme={null}
use Acme\Reports\ReportCollector;

$this->app->bind(ReportCollector::class);

$this->app->when(ReportCollector::class)
    ->needs('$reports')
    ->giveTagged(ReportCollector::TAG);
```

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](https://kawax.biz/fr/service-container.md#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.

> [!WARNING]
>
> 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](https://kawax.biz/fr/advanced/deferred-provider.md), et le chargement à l'installation dans la [découverte automatique des packages](https://kawax.biz/fr/advanced/package-discovery.md).

### 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](https://kawax.biz/fr/advanced/package-service-lifetimes.md).

## 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](https://kawax.biz/fr/advanced/package-testing.md) pour les tests incluant l'intégration à l'application et la découverte automatique.

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

namespace Acme\Reports\Tests;

use Acme\Reports\ReportCollector;
use Acme\Reports\ReportsServiceProvider;
use Acme\Reports\RuntimeReport;
use Illuminate\Container\Container;
use PHPUnit\Framework\TestCase;

class ReportCollectorTest extends TestCase
{
    public function test_tagged_reports_are_lazy_and_can_be_resolved_again(): void
    {
        $created = 0;
        $app = $this->makeContainer($created);
        $collector = $app->make(ReportCollector::class);

        $this->assertSame(0, $created);
        $this->assertCount(2, $collector->collect());
        $this->assertSame(1, $created);
        $this->assertCount(2, $collector->collect());
        $this->assertSame(2, $created);
    }

    public function test_give_tagged_resolves_an_array_once(): void
    {
        $created = 0;
        $app = $this->makeContainer($created);
        $app->bind(ReportCollector::class);
        $app->when(ReportCollector::class)
            ->needs('$reports')
            ->giveTagged(ReportCollector::TAG);

        $collector = $app->make(ReportCollector::class);

        $this->assertSame(1, $created);
        $this->assertCount(2, $collector->collect());
        $this->assertCount(2, $collector->collect());
        $this->assertSame(1, $created);
    }

    private function makeContainer(int &$created): Container
    {
        $app = new Container;
        (new ReportsServiceProvider($app))->register();
        $app->bind(RuntimeReport::class, function () use (&$created): RuntimeReport {
            $created++;

            return new RuntimeReport;
        });

        return $app;
    }
}
```

En outre, lors des mises à jour du package, vérifiez les combinaisons suivantes.

| Cas | Points à vérifier |
| - | - |
| Enregistrement d'une implémentation supplémentaire sous le même tag | Elle est incluse dans l'agrégation et passe la vérification de type du contrat |
| Enregistrement en double de la même implémentation | La duplication du traitement de collecte peut être détectée |
| Tag non enregistré ou nom de tag erroné | Il est clair si une liste vide est acceptée ou si le package lève une erreur |
| Réutilisation dans une autre requête ou un autre job | Les services scoped et les résultats de collecte ne sont pas reportés |
| Modification du nom du tag ou du contrat | La compatibilité est évaluée comme pour une API publique référencée par les providers du code appelant |

## Sources primaires consultées

* [Documentation officielle de Laravel : Tagging](https://github.com/laravel/docs/blob/13.x/container.md#tagging)
* [Documentation officielle de Laravel : Binding primitives・Variadic tag dependencies](https://github.com/laravel/docs/blob/13.x/container.md#binding-primitives)
* [Documentation officielle de Laravel : fournisseurs de services](https://github.com/laravel/docs/blob/13.x/providers.md)
* [Documentation officielle de Laravel : développement de packages](https://github.com/laravel/docs/blob/13.x/packages.md)
* [Container : tag(), tagged()](https://github.com/laravel/framework/blob/v13.38.0/src/Illuminate/Container/Container.php)
* [ContextualBindingBuilder : giveTagged()](https://github.com/laravel/framework/blob/v13.38.0/src/Illuminate/Container/ContextualBindingBuilder.php)
* [RewindableGenerator : nouvelle énumération et nombre d'éléments](https://github.com/laravel/framework/blob/v13.38.0/src/Illuminate/Container/RewindableGenerator.php)
* [Attribut Tag : resolve()](https://github.com/laravel/framework/blob/v13.38.0/src/Illuminate/Container/Attributes/Tag.php)
* [Test officiel : ContainerTaggingTest](https://github.com/laravel/framework/blob/v13.38.0/tests/Container/ContainerTaggingTest.php)
* [Test officiel : ContextualBindingTest](https://github.com/laravel/framework/blob/v13.38.0/tests/Container/ContextualBindingTest.php)


## Related topics

- [Service container](https://kawax.biz/fr/service-container.md)
- [Sujets avancés](https://kawax.biz/fr/advanced/index.md)
- [Extensions Blade d'un package et cache de compilation](https://kawax.biz/fr/advanced/package-blade-extensions.md)
- [Conception du cycle de vie des services d'un package](https://kawax.biz/fr/advanced/package-service-lifetimes.md)
- [Assets publics d'un package et mises à jour](https://kawax.biz/fr/advanced/package-assets.md)


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