Share

Stop the If-Else Hell: Clean Up Your Symfony Code with the Strategy Pattern

We’ve all been there. You start with a simple if statement for a payment method. A month later, a new requirement comes in, then another, and suddenly your “simple” service is a 200-line monster of nested logic. It’s a point of professional observation that many engineers still haven’t reached for the Strategy Pattern—a tool designed specifically to kill this kind of “if-else hell”.

If you’re using Symfony, implementing this pattern is no longer about complex YAML files or manual compiler passes. With PHP 8 attributes, you can build modular, testable systems that practically wire themselves.


The High Price of Messy Code

Messy decision trees aren’t just ugly; they are expensive. Every time you add a new branch to a giant if-else chain, you risk breaking everything else in that file. This “architectural decay” makes it harder for your team to move fast.

The Strategy Pattern fixes this by taking “what varies” and moving it into its own class. Instead of one giant method that knows how to do everything, you have several small classes that each do one thing well.

FeatureMessy Logic (If-Else)Strategy Pattern
Adding FeaturesYou have to edit existing, risky codeYou just add a new class
TestingHard: You need complex mocks for all pathsEasy: Test each strategy on its own
ReadabilityHigh cognitive load (the “arrow code” trap)Clean: One class, one responsibility
ScalingCode gets messier as you growComplexity stays the same

How the Strategy Pattern Works

The pattern uses three simple parts:

  1. The Interface (The Contract): This defines the rules. Every strategy must follow it.
  2. Concrete Strategies: These are the actual workers. One class for PayPal, one for Stripe, etc.
  3. The Context (The Boss): This class coordinates the workers. It doesn’t care how they work, just that they follow the rules.

Automate Everything with Symfony Attributes

In the past, you had to manually tell Symfony about every single strategy. Now, we use “Zero Configuration” with attributes.

The Secret Weapon: #[AutoconfigureTag]

By putting #[AutoconfigureTag] on an interface, you tell Symfony: “Every class that implements this is part of the same team”. No more editing services.yaml every time you add a new feature.

Lazy Loading with #[AutowireIterator]

When the “Boss” class receives these strategies, it uses #[AutowireIterator]. This uses a PHP Generator, meaning strategies are “lazy-loaded” – they only wake up if your code actually needs them. This can cut memory usage by up to 50% in large apps.


Real-Life Example: A Document Processor

Imagine an app that needs to handle PDFs, CSVs, and JSON files.

1. Set the Rules

First, define your interface. Use # so Symfony knows to track its implementations.

PHP
namespace App\Contract;

use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;

#[AutoconfigureTag('app.file_processor')]
interface DocumentProcessorInterface {
    public function supports(string $mimeType): bool;
    public function process(string $filePath): void;
    public static function getProcessorName(): string;
}

2. Create the Workers

Each worker only cares about its own format. If the PDF library needs an update, you only touch the PdfProcessor.

PHP
namespace App\Processor;

class PdfProcessor implements DocumentProcessorInterface {
    public function supports(string $mimeType): bool {
        return $mimeType === 'application/pdf';
    }

    public function process(string $filePath): void {
        // PDF logic here
    }

    public static function getProcessorName(): string {
        return 'pdf_handler';
    }
}

3. The Orchestrator (The Boss)

The DocumentManager gets the whole list of workers. It simply loops through them until it finds one that says “I can handle this”.

PHP
namespace App\Service;

use Symfony\Component\DependencyInjection\Attribute\AutowireIterator;

final readonly class DocumentManager {
    public function __construct(
        #[AutowireIterator('app.file_processor')]
        private iterable $processors
    ) {}

    public function handle(string $filePath, string $mimeType): void {
        foreach ($this->processors as $processor) {
            if ($processor->supports($mimeType)) {
                $processor->process($filePath);

                return;
            }
        }

        throw new \InvalidArgumentException(sprintf('No processor for %s', $mimeType));
    }
}

Performance: Iterator vs. Locator

For most apps, #[AutowireIterator] is perfect. But if you have hundreds of strategies and need extreme speed, use #[AutowireLocator].

While an iterator loops through everything (O(n) complexity), a locator acts like a map, giving you the exact worker you need instantly (O(1) complexity).

MetricTaggedIteratorTaggedLocator
Search SpeedO(n) (Looping)O(1) (Direct access)
Memory UsageVery LowVery Low
Best Use CaseWhen requirements are unknownWhen you know the key upfront

How to Test Your Strategies

The best part of this pattern is how easy it is to test.

  • Isolated Unit Tests: You can test the PdfProcessor logic in milliseconds without booting the whole framework.
  • Boss Logic Tests: Test the DocumentManager by giving it a fake list of workers to make sure it loops correctly.
  • Container Smoke Tests: Run a quick test to make sure all your tags are registered correctly. Use the console: php bin/console debug:container --tag=app.document_processor.

4 Mistakes to Avoid

  1. Over-Engineering: If you only have two options and they never change, a simple match expression is better than a full Strategy Pattern.
  2. Circular Dependencies: If Strategy A needs Strategy B, and B needs A, Symfony will crash. Extract the shared logic instead.
  3. Duplicate Names: If you key your strategies by name (like ‘paypal’), make sure those names are unique, or your container won’t compile.

Bottom Line

The Strategy Pattern isn’t academic fluff – it’s a survival tool for growing codebases. By leveraging Symfony’s modern attributes, you can stop fighting your framework and start building systems that are a pleasure to maintain.