Skip to main content
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; for how to distribute providers, see package development.

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.

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.
As minimal implementations, create src/RuntimeReport.php and src/MemoryReport.php.

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’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.
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().
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. 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.
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, 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.
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, and how providers are loaded at install time in package discovery.

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.

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 for tests that cover application integration and auto-discovery.
In addition, check the following combinations when updating the package.

Primary sources

Last modified on October 26, 2026