Skip to content
Nicolas Demay
#[AutowireIterator]: the Strategy pattern in one line with Symfony

#[AutowireIterator]: the Strategy pattern in one line with Symfony

Published on 5 min read

I have an e-signature service running in production. DocuSign, works fine. But we want to bring Yousign into the mix, first for 10% of traffic, then gradually ramp up. And if Yousign goes down, we want to fall back to DocuSign automatically.

The kind of requirement that leads to endless if/else chains if you don’t structure things properly.

Let’s walk through an example.

The common interface

First step: define what “signing a document” means, regardless of the provider.

src/Signature/SignatureProviderInterface.php
namespace App\Signature;
interface SignatureProviderInterface
{
public function getName(): string;
public function sign(Document $document, Signer $signer): SignatureResult;
public function isAvailable(): bool;
}

This is the Strategy pattern in its simplest form: one interface, multiple interchangeable implementations. The calling code doesn’t know (and doesn’t need to know) which provider it’s using.

Two implementations

src/Signature/Provider/DocuSignProvider.php
namespace App\Signature\Provider;
class DocuSignProvider implements SignatureProviderInterface
{
public function getName(): string
{
return 'docusign';
}
public function sign(Document $document, Signer $signer): SignatureResult
{
// API calls to DocuSign
}
public function isAvailable(): bool
{
// Health check (ping API, check credentials, etc.)
return true;
}
}
src/Signature/Provider/YousignProvider.php
namespace App\Signature\Provider;
class YousignProvider implements SignatureProviderInterface
{
public function getName(): string
{
return 'yousign';
}
public function sign(Document $document, Signer $signer): SignatureResult
{
// API calls to Yousign
}
public function isAvailable(): bool
{
return true;
}
}

Nothing surprising. Two classes, same interface.

The orchestrator with AutowireIterator

src/Signature/SignatureOrchestrator.php
namespace App\Signature;
use Symfony\Component\DependencyInjection\Attribute\AutowireIterator;
class SignatureOrchestrator
{
public function __construct(
/** @var iterable<SignatureProviderInterface> */
#[AutowireIterator(SignatureProviderInterface::class, defaultPriorityMethod: 'getPriority')]
private iterable $providers,
) {
}
}

This is the line that does the heavy lifting. #[AutowireIterator] tells Symfony: “inject all services implementing SignatureProviderInterface”. No YAML configuration, no CompilerPass, no manual wiring. Symfony collects all implementations automatically.

The defaultPriorityMethod tells Symfony to call getPriority() on each provider to determine iteration order. Priority is carried by the interface contract, just like the name. Not by container config.

To have the tag applied automatically to every class implementing the interface, you declare it once:

src/Signature/SignatureProviderInterface.php
namespace App\Signature;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
#[AutoconfigureTag]
interface SignatureProviderInterface
{
public function getName(): string;
public function getPriority(): int;
public function sign(Document $document, Signer $signer): SignatureResult;
public function isAvailable(): bool;
}

Every class implementing SignatureProviderInterface gets the tag automatically, using the interface’s FQCN as the tag name (App\Signature\SignatureProviderInterface). And we take this opportunity to add getPriority() to the contract: each provider declares its priority order, just like its name. Zero configuration per provider.

On the implementation side, each provider simply returns its priority. The higher the value, the earlier it gets tried:

// DocuSignProvider
public function getPriority(): int
{
return 10; // Primary provider
}
// YousignProvider
public function getPriority(): int
{
return 5; // Secondary provider
}

Circuit breaker

The fallback logic. If the chosen provider is unavailable or fails, we move on to the next one:

src/Signature/SignatureOrchestrator.php
public function sign(Document $document, Signer $signer): SignatureResult
{
$lastException = null;
foreach ($this->providers as $provider) {
if (!$provider->isAvailable()) {
continue;
}
try {
return $provider->sign($document, $signer);
} catch (SignatureProviderException $e) {
$lastException = $e;
// Log and try the next provider
continue;
}
}
throw new NoAvailableProviderException(
'All signature providers failed.',
previous: $lastException
);
}

We iterate over the providers. The first one that responds, we take it. If it fails, we try the next. If none work, we throw an explicit exception.

No need for an external circuit breaker library for this simple case. The iterator does the job.

Progressive rollout

For the rollout, we want to direct a percentage of traffic to a specific provider. A simple config is enough:

config/services.yaml
parameters:
signature.rollout:
yousign: 10 # 10% of traffic goes to Yousign
docusign: 90 # 90% goes to DocuSign

And we adapt the orchestrator:

src/Signature/SignatureOrchestrator.php
class SignatureOrchestrator
{
public function __construct(
/** @var iterable<SignatureProviderInterface> */
#[AutowireIterator(SignatureProviderInterface::class)]
private iterable $providers,
#[Autowire('%signature.rollout%')]
private array $rolloutConfig,
) {
}
public function sign(Document $document, Signer $signer): SignatureResult
{
$provider = $this->selectProvider();
try {
return $provider->sign($document, $signer);
} catch (SignatureProviderException) {
// Fallback: try the others
return $this->fallback($document, $signer, exclude: $provider->getName());
}
}
private function selectProvider(): SignatureProviderInterface
{
$rand = random_int(1, 100);
$cumulative = 0;
foreach ($this->providers as $provider) {
$cumulative += $this->rolloutConfig[$provider->getName()] ?? 0;
if ($rand <= $cumulative) {
return $provider;
}
}
throw new \LogicException('Rollout configuration does not cover all cases.');
}
private function fallback(
Document $document,
Signer $signer,
string $exclude
): SignatureResult {
foreach ($this->providers as $provider) {
if ($provider->getName() === $exclude) {
continue;
}
if (!$provider->isAvailable()) {
continue;
}
try {
return $provider->sign($document, $signer);
} catch (SignatureProviderException) {
continue;
}
}
throw new NoAvailableProviderException('All signature providers failed.');
}
}

Want to move Yousign to 30%? Change one number in the config. Want to switch everything over? yousign: 100. Not a single line of code to touch.

In production, you’d probably want to go further: a user routed to Yousign for their first signature should stick with it for subsequent ones, to avoid an inconsistent experience. That’s handled by persisting the provider choice per user, but that’s a topic of its own.

Adding a third provider

This is where you see the power of the pattern. Want to add Universign? One class:

src/Signature/Provider/UniversignProvider.php
namespace App\Signature\Provider;
class UniversignProvider implements SignatureProviderInterface
{
public function getName(): string
{
return 'universign';
}
public function getPriority(): int
{
return 1;
}
public function sign(Document $document, Signer $signer): SignatureResult
{
// API calls to Universign
}
public function isAvailable(): bool
{
return true;
}
}

And update the rollout config:

config/services.yaml
parameters:
signature.rollout:
yousign: 30
universign: 20
docusign: 50

Zero changes in the orchestrator. Symfony automatically detects the new service, the tag is applied via the interface, and AutowireIterator injects it. Only the rollout config changes, the code stays untouched.

Key takeaways

AutowireIterator isn’t limited to the Strategy pattern, but it’s a particularly good fit. No manual factory, no CompilerPass, no switch/case that grows with every new provider.

The mechanism is simple:

  1. An interface with #[AutoconfigureTag]
  2. Classes that implement this interface
  3. A service that receives #[AutowireIterator] and iterates over them

The example shown here is intentionally simple. It doesn’t cover all the edge cases of a real production system. But that was the point: showing with a concrete case the power of one of my favorite Symfony features. Its dependency injection and how easily you can orchestrate a whole set of services is what makes the framework such a pleasure to use daily.