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

# Package container tags and plugin aggregation

> Use container tags in Laravel 13 to aggregate implementations from multiple packages, and examine the lazy resolution of tagged(), how it differs from giveTagged(), duplicate registration, and lifecycles.

When you want multiple packages or the consuming application to contribute functionality to the same aggregating service, you can use the container's `tag()` and `tagged()`. The aggregating side does not need to know every implementation class.

This page targets Laravel 13 and is based on the implementation and official tests of Framework `v13.38.0`. For binding basics, see the [service container](https://kawax.biz/en/service-container.md); for how to distribute providers, see [package development](https://kawax.biz/en/advanced/package-development.md).

## Tags group service registration keys

`tag()` adds container registration keys, such as class names, to a tag's list. Tagging alone does not create the service.

`tagged()` returns an `iterable` for enumerating the services registered under that tag. It is not a feature that automatically discovers classes implementing a given interface; each provider explicitly opts in. It is also unrelated to the tags used for publishable resources with `vendor:publish --tag`.

```mermaid theme={null}
flowchart TD
    A["register() in each provider"] --> B["bind() the implementation and<br>add it to the same tag"]
    B --> C["Resolve the aggregating service"]
    C --> D["Hold the iterable from tagged()"]
    D --> E["Enumerate with foreach"]
    E --> F["make() for each registration key"]
    F --> G["Call the implementation's collect()"]
```

## Building an extensible report package

### Define a contract shared by the implementations

As an example, we will build an `acme/reports` package that gathers multiple reports. It assumes a Composer PSR-4 configuration that maps `Acme\Reports\` to `src/`.

Define the contract that plugins implement in `src/Contracts/Report.php`.

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

namespace Acme\Reports\Contracts;

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

As minimal implementations, create `src/RuntimeReport.php` and `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)];
    }
}
```

### The aggregating service receives an iterable

`src/ReportCollector.php` calls the collection logic as it enumerates each implementation. Defining the public tag name as a constant lets consumers reference the same name.

```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;
    }
}
```

PHP's `iterable` type alone cannot validate element types, and `tag()` itself does not check whether a service implements `Report`. In this example, the aggregating side also checks the contract.

### Register only bindings and tags in register()

Register the default implementations and the aggregating service in `src/ReportsServiceProvider.php`. The aggregating service's factory passes the retrieved `iterable` through as-is.

```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));
        });
    }
}
```

Other packages or the consuming application can create a `DailyReport` or similar class implementing `Report`, and add it to the same tag inside their own provider's `register()`.

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

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

Once all providers have registered, calling `app(ReportCollector::class)->collect()` also aggregates the added implementations. As more implementations join, you do not need to change how `ReportCollector` is created.

## tagged() and giveTagged() create services at different times

In the implementation we examined, `tagged()` for a registered tag returns a `RewindableGenerator`. Each service's `make()` is called during enumeration, and if you stop enumerating partway through, the remaining services are not created.

In addition, `RewindableGenerator` creates a new Generator on every enumeration. It does not cache services; with a regular `bind()`, enumerating again also recreates the instances. Services registered with `singleton()` or `scoped()` follow their respective sharing rules.

On the other hand, the contextual binding `giveTagged()` converts the result of `tagged()` into an array with `iterator_to_array()` at resolution time.

| Injection method | When individual services are resolved | Re-enumerating on the same aggregating object |
| - | - | - |
| Passing the result of `tagged()` as-is | When each element is enumerated | Calls `make()` again |
| An `iterable` parameter with `#[Tag(...)]` | When each element is enumerated | Calls `make()` again |
| Injecting with `giveTagged()` | When the aggregating service's dependencies are resolved | Reuses the elements of the held array |

If you want to resolve the dependencies all at once as an array, you can replace the earlier `bind()` with a factory with the following registration.

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

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

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

Even if `$reports` is typed as `iterable`, the value held with this approach is an array. The parameter type alone does not guarantee lazy resolution. Also, what `giveTagged()` does is resolve instances; it does not call each implementation's `collect()`.

The `resolve()` method of the `Tag` attribute returns `tagged()` as-is. When using the attribute, apply it to a constructor parameter as described in [contextual attributes](https://kawax.biz/en/service-container.md#contextual-attributes), and let the container construct the class. The attribute is not automatically applied to `new` inside your own factory.

## Don't break the plugin registration contract

### Make duplicate registration and ordering explicit

`tag()` appends to a list, and adding the same registration key again does not remove duplicates. If the same implementation is enumerated twice, `collect()` is called twice, even if a singleton returns the same object.

Keep tag registration in a single place: the provider responsible for each implementation. `bindIf()` is an API for avoiding overwriting a binding; it does not prevent subsequent duplicate `tag()` calls.

The enumeration order is the order in which entries were added to the tag's list. Tags have no priority sorting, so do not make the processing order across different packages depend solely on provider load order. If ordering is part of the public specification, include a priority or identifier in the contract and design the aggregating side to sort explicitly.

### Register the providers that add tags first

`tagged()` for an unregistered tag returns an empty array rather than throwing an exception. Tag registrations made later are not reflected in an empty array that was already received, or in an array already created by `giveTagged()`.

Even for lazy enumeration of registered tags, do not rely on the list changing during execution. The `tagged()` we examined passes the count at retrieval time, while it reads the tag list at enumeration time. Designing your package to use tags only after all providers have registered also avoids mismatches between the count and the enumerated items.

> [!WARNING]
>
> Enumerating a tag does not look up not-yet-loaded providers by tag name. If the provider that adds a tag is a `DeferrableProvider`, the tag registration does not run until that service is resolved through some other path, and the implementation may be missing from the list. Use a regular provider to register tags for implementations that should always participate, and design it so that implementations are not created at registration time.

You can review when deferred providers are loaded in [DeferrableProvider](https://kawax.biz/en/advanced/deferred-provider.md), and how providers are loaded at install time in [package discovery](https://kawax.biz/en/advanced/package-discovery.md).

### Separate tags from service lifecycles

Tagging a service does not automatically make it a singleton or scoped. This example uses a regular `bind()` and does not store collected results on the aggregating object.

In particular, if you hold an array resolved with `giveTagged()` in a singleton aggregating service, scoped services inside the array may also remain held. Resetting a scope does not replace references that are already stored in another object. For designs used with Octane or queue workers, also see [package service lifetime design](https://kawax.biz/en/advanced/package-service-lifetimes.md).

## Testing lazy resolution and array conversion

With the package's Composer autoloading and a PHPUnit environment, you can add the following test to `tests/ReportCollectorTest.php`. It calls the provider's registration on a standalone container and verifies how many times services are created. Supplement it with [Orchestra Testbench](https://kawax.biz/en/advanced/package-testing.md) for tests that cover application integration and auto-discovery.

```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;
    }
}
```

In addition, check the following combinations when updating the package.

| Case | What to verify |
| - | - |
| Registering an additional implementation under the same tag | It is included in the aggregation and passes the contract type check |
| Registering the same implementation twice | Duplicate collection can be detected |
| Unregistered tag or wrong tag name | It is clear whether an empty list is allowed or the package raises an error |
| Reuse across requests or jobs | Scoped services and collected results are not carried over |
| Changing the tag name or contract | Compatibility is evaluated as a public API referenced by consumers' providers |

## Primary sources

* [Laravel documentation: Tagging](https://github.com/laravel/docs/blob/13.x/container.md#tagging)
* [Laravel documentation: Binding primitives and variadic tag dependencies](https://github.com/laravel/docs/blob/13.x/container.md#binding-primitives)
* [Laravel documentation: Service providers](https://github.com/laravel/docs/blob/13.x/providers.md)
* [Laravel documentation: Package development](https://github.com/laravel/docs/blob/13.x/packages.md)
* [Container: tag() and 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: re-enumeration and count](https://github.com/laravel/framework/blob/v13.38.0/src/Illuminate/Container/RewindableGenerator.php)
* [Tag attribute: resolve()](https://github.com/laravel/framework/blob/v13.38.0/src/Illuminate/Container/Attributes/Tag.php)
* [Official tests: ContainerTaggingTest](https://github.com/laravel/framework/blob/v13.38.0/tests/Container/ContainerTaggingTest.php)
* [Official tests: ContextualBindingTest](https://github.com/laravel/framework/blob/v13.38.0/tests/Container/ContextualBindingTest.php)


## Related topics

- [Service container](https://kawax.biz/en/service-container.md)
- [Advanced Topics](https://kawax.biz/en/advanced/index.md)
- [Plugin Directories](https://kawax.biz/en/packages/laravel-copilot-sdk/plugin-directories.md)
- [Laravel Console Starter](https://kawax.biz/en/packages/laravel-console-starter/index.md)
- [Package public assets and updates](https://kawax.biz/en/advanced/package-assets.md)


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