Tagging bindings and resolving tagged groups
Concept
Tagging allows you to group multiple container bindings under a label and resolve all of them at once as an array. This is the container's way of implementing the composite or collection injection pattern — when a service needs all implementations of an interface rather than just one.
The tag(abstracts, tags) method associates one or more abstracts with one or more tag strings. The tagged(tag) method resolves all abstracts that were tagged with that label, returning a Countable lazy iterator of resolved instances. The laziness matters: the instances are not constructed until you iterate over the tagged result, which prevents eager construction of all tagged services just to potentially use only one.
The classic use case is a multi-driver system where you want all registered implementations to act together: report exporters (CSV, PDF, Excel all tagged 'report.exporters'), payment processors, notification channels, or validators. Another common pattern is plugin systems: packages register their services under a known tag, and the host application resolves all tagged services to build a pipeline.
Tagging is often combined with contextual binding (->giveTagged('tag.name')) to inject all tagged implementations into a service that aggregates them.
In Illuminate\Container\Container, tags are stored in $this->tags — an array where keys are tag strings and values are arrays of abstract names. tagged($tag) returns a Illuminate\Container\RewindableGenerator which uses yield to lazily instantiate each tagged abstract on iteration.
A practical limitation: tagged bindings are difficult to introspect or document. The IDE doesn't know what types app()->tagged('report.exporters') returns. Using a typed wrapper (a dedicated collection class or injecting them as an iterable of a known interface) improves type safety.
Code Example
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class ReportServiceProvider extends ServiceProvider
{
public function register(): void
{
// Register individual exporters
$this->app->bind(\App\Reports\CsvExporter::class, function ($app) {
return new \App\Reports\CsvExporter(
$app->make(\League\Csv\Writer::class)
);
});
$this->app->bind(\App\Reports\PdfExporter::class, function ($app) {
return new \App\Reports\PdfExporter(config('reports.pdf_engine'));
});
$this->app->bind(\App\Reports\ExcelExporter::class);
// Tag all exporters under a single label
$this->app->tag(
[
\App\Reports\CsvExporter::class,
\App\Reports\PdfExporter::class,
\App\Reports\ExcelExporter::class,
],
'report.exporters'
);
// The ReportManager gets ALL tagged exporters injected
$this->app->bind(\App\Reports\ReportManager::class, function ($app) {
// tagged() returns a lazy RewindableGenerator — instances created on iteration
return new \App\Reports\ReportManager($app->tagged('report.exporters'));
});
// Contextual binding using giveTagged()
$this->app->when(\App\Services\DataExportService::class)
->needs(\App\Contracts\ExporterCollectionInterface::class)
->giveTagged('report.exporters');
}
}<?php
// The service consuming tagged bindings
namespace App\Reports;
use App\Contracts\ReportExporterInterface;
class ReportManager
{
/** @var ReportExporterInterface[] */
private array $exporters;
public function __construct(iterable $exporters)
{
// Convert the lazy generator to array — triggers construction of all exporters
$this->exporters = iterator_to_array($exporters);
}
public function export(array $data, string $format): mixed
{
foreach ($this->exporters as $exporter) {
if ($exporter->supports($format)) {
return $exporter->export($data);
}
}
throw new \InvalidArgumentException("No exporter supports format: {$format}");
}
public function exportAll(array $data): array
{
return array_map(
fn(ReportExporterInterface $e) => $e->export($data),
$this->exporters
);
}
}
// Checking what tags exist at runtime (useful for debugging)
// Container doesn't expose this publicly, but you can introspect:
$container = app();
// $container->tags is a protected property — access with Reflection in debug/testing<?php
// Plugins registering themselves under a known tag
// (Package service provider pattern)
class AnalyticsPackageServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->bind(GoogleAnalyticsTracker::class, function ($app) {
return new GoogleAnalyticsTracker(config('analytics.google.id'));
});
// This tag is defined by the host application — the package registers into it
$this->app->tag(GoogleAnalyticsTracker::class, 'analytics.trackers');
}
}
// Host application's ReportServiceProvider already defined the tag
// and the AnalyticsManager was already consuming it
// This plugin "plugs in" to the existing system without modifying host codeInterview Q&A
Q: What is container tagging and when would you use it over a regular binding?
Tagging is for situations where you need all implementations of a concept, not just one. A regular bind(Interface::class, ConcreteClass::class) means "when anyone asks for Interface, give them this one concrete." Tagging says "here are five implementations of this interface; I want a service to receive all five at once." Use tagging when building pipelines (all middleware implementations), export systems (all exporters), notification systems (all notification channels), or plugin architectures. The key use case is when the number of implementations is open-ended — new packages or providers can add their implementations to the tagged group without modifying the aggregating service.
Q: Why does app()->tagged('tag') return a lazy generator rather than an array?
Lazy evaluation prevents eagerly constructing every tagged service when only a subset might be used. If you have 10 registered exporters and the user requests CSV export, you don't want to construct all 10 PDF, Excel, and JSON exporters when you'll only use the first one that supports('csv'). The RewindableGenerator constructs each tagged instance only when the iteration reaches it. This also means the order of iteration matters — implementations are yielded in the order they were tagged. If you do need all instances immediately, iterator_to_array(app()->tagged('tag')) triggers full construction.
Q: How would you implement a plugin system in Laravel where third-party packages can register their implementations into a host application's pipeline?
Define a known tag in the host application's service provider (e.g., 'payments.processors'). Document this tag in your package README. Each plugin package creates its own service provider that binds its implementation and tags it: $this->app->tag(MyPaymentProcessor::class, 'payments.processors'). The host application's aggregating class (e.g., PaymentRouter) is bound using a closure that calls $app->tagged('payments.processors'). With Composer's package auto-discovery (extra.laravel.providers), plugin providers are auto-registered, and their implementations automatically appear in the tagged group. The host code never changes; it simply iterates the tag. This is exactly how Laravel's own cash-system and macro-registrations work.