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 anacme/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.
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.
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 insrc/ReportsServiceProvider.php. The aggregating service’s factory passes the retrieved iterable through as-is.
DailyReport or similar class implementing Report, and add it to the same tag inside their own provider’s register().
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.
$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.
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 regularbind() 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 totests/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.
Primary sources
- Laravel documentation: Tagging
- Laravel documentation: Binding primitives and variadic tag dependencies
- Laravel documentation: Service providers
- Laravel documentation: Package development
- Container: tag() and tagged()
- ContextualBindingBuilder: giveTagged()
- RewindableGenerator: re-enumeration and count
- Tag attribute: resolve()
- Official tests: ContainerTaggingTest
- Official tests: ContextualBindingTest