A lot of developers still believe Symfony is “too complex.” They say, “You have to edit YAML for everything!”. But the truth is — modern Symfony (especially version 6.4 and beyond) is far more declarative and developer-friendly than ever.
You can now configure almost everything using PHP attributes — right inside your code.
No more hunting through services.yaml. No more guessing what’s tagged where.
And two of the most powerful attributes you’ll ever use are:
#[AutowireIterator]#[AutowireLocator]
They let you inject multiple related services or lazy service locators directly into your classes — fully type-safe, autoconfigured, and crystal clear. Let’s examine how they work and when to use each — the modern Symfony way.
Why We Need These Attributes
Imagine you’re building a payment system with multiple gateways:
- PayPal
- Stripe
- Flutterwave
Each implements the same interface:
namespace App\Payment;
interface PaymentGatewayInterface
{
public function process(float $amount): void;
}You might want to:
- Loop through all gateways and run them in sequence or
- Choose one dynamically (e.g.,
$paymentManager->get('paypal'))
That’s exactly where #[AutowireIterator] and #[AutowireLocator] shine.
Setting Up: Auto-Configuring Services with Attributes
Instead of writing YAML tags, Symfony lets you declare service tags directly in your class using #[AutoconfigureTag].
Example:
namespace App\Payment;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
#[AutoconfigureTag('app.payment_gateway')]
class PayPalGateway implements PaymentGatewayInterface
{
public function process(float $amount): void
{
echo "Processing $amount via PayPal...\n";
}
}#[AutoconfigureTag('app.payment_gateway')]
class StripeGateway implements PaymentGatewayInterface
{
public function process(float $amount): void
{
echo "Processing $amount via Stripe...\n";
}
}#[AutoconfigureTag('app.payment_gateway')]
class FlutterwaveGateway implements PaymentGatewayInterface
{
public function process(float $amount): void
{
echo "Processing $amount via Flutterwave...\n";
}
}Now Symfony automatically knows these classes are tagged under app.payment_gateway.
You don’t need to define anything in YAML. Pretty neat, right?
Adding #[AutoconfigureTag] to each class works — but there’s an even simpler method. You can tag the interface itself, and Symfony will automatically apply the tag to all classes that implements the interface. For example:
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
#[AutoconfigureTag('app.payment_gateway')]
interface PaymentGatewayInterface
{
public function process(float $amount): void;
}
Now every class that implements this interface is automatically tagged. You don’t need to repeat the attribute in each gateway class. This keeps your code cleaner, especially when you have many implementations.
Using #[AutowireIterator] — Inject All Tagged Services
When you want to work with all the services at once (for example, loop over all gateways), use the #[AutowireIterator].
namespace App\Service;
use Symfony\Component\DependencyInjection\Attribute\AutowireIterator;
use App\Payment\PaymentGatewayInterface;
class PaymentManager
{
public function __construct(
#[AutowireIterator('app.payment_gateway')]
private iterable $gateways
) {}
public function processAll(float $amount): void
{
foreach ($this->gateways as $gateway) {
$gateway->process($amount);
}
}
}Symfony will automatically find all services tagged with app.payment_gateway and inject them as an iterable collection into $gateways.
To use the PaymentManager, you can autowire it into another class or controller to access all the payment gateways and public methods available. For example:
namespace App\Controller\Sample;
...
class SampleController extends AbstractController
{
#[Route('/')]
public function index(PaymentManager $paymentManager): Response
{
// Process payment through every gateway
$paymentManager->processAll(100);
// The above is just a sample, you should return a Response instead
}
}Result:
Processing 100 via Paypal...
Processing 100 via Stripe...
Processing 100 via Flutterwave...Learn more about AutowireIterator
Using #[AutowireLocator] — Inject Services Lazily by Key
The #[AutowireLocator] lets you create a service locator — a lightweight, mini-container that contains only the specific services you care about. This means Symfony won’t instantiate any of the services defined in the locator; it will only load them when you actually make a call.
Let’s assume your system doesn’t always use every injected service and you only want to use one payment gateway, based on user choice? Injecting all services would force Symfony to instantiate all gateways — even if you only need to use one. So instead, you can use a service locator, which gives you lazy access to only the services you ask for.
That’s where #[AutowireLocator] comes in. Here’s how you can inject a locator with multiple handlers:
use App\Payment\PayPalGateway;
use App\Payment\StripeGateway;
use App\Payment\BitcoinGateway;
use Psr\Container\ContainerInterface;
use Symfony\Component\DependencyInjection\Attribute\AutowireLocator;
class PaymentLocator
{
public function __construct(
#[AutowireLocator([
PayPalGateway::class,
StripeGateway::class,
])]
private ContainerInterface $gateways,
) {}
public function process(string $method, float $amount): void
{
if ($this->gateways->has($method)) {
$gateway = $this->gateways->get($method);
$gateway->process($amount);
}
throw new \InvalidArgumentException("Payment gateway '$method' not found.");
}
}
Now you can autowire the PaymentLocator and process a single payment gateway without instantiating others at runtime.
$paymentLocator->process(App\Payment\StripeGateway::class, 150.00);
Learn more about AutowireLocator
AutowireIterator vs AutowireLocator
| Feature | #[AutowireIterator] | #[AutowireLocator] |
|---|---|---|
| Injected type | iterable | ServiceLocator |
| When services load | Immediately | On demand (lazy) |
| How to access | Loop through all | Fetch by key or class |
| Best for | Batch processing (e.g., send notifications to all) | Dynamic lookup (e.g., pick one strategy) |
| Performance | All services initialized | Only used ones initialized |
In simple words:
- Use Iterator when you need all services associated to a particular tag
- Use Locator when you need to pick one service or load only specific services.
Final Thoughts
Symfony’s attribute-based DI system is one of the cleanest evolutions in the framework’s history.
#[AutowireIterator] and #[AutowireLocator] are two sides of the same coin.
One is for collecting all services; the other is for finding one when needed.
By tagging services with #[AutoConfigureTag] (or tagging interfaces directly), you make Symfony’s dependency system truly self-organizing — no config files, no boilerplate.
If you ever find yourself thinking “Symfony is too complex,” try writing your next service with these attributes. You’ll realize it’s not just powerful — it’s beautifully logical.
In short: Symfony isn’t hard. It’s just powerful. And once you learn these tools, you’ll start writing cleaner, more scalable code — with zero YAML in sight.