The Strategy Pattern Turned 500 Lines Into Clean Code

The Strategy Pattern Turned 500 Lines Into Clean Code

4 min read Design Patterns
Share:

Design patterns have a reputation problem.

They're taught in computer science courses with abstract examples — shapes, animals, vehicle factories. They come up in job interviews as gotcha questions. They sound impressive in architecture discussions but feel disconnected from the Laravel code you write every day.

Then you hit a real problem, and suddenly a pattern clicks. Not because you're trying to be clever, but because it's genuinely the cleanest solution.

This post is about those moments.

The Strategy Pattern: Dynamic Form Transformation

Here's a project I worked on: a client had an existing system that defined forms in XML. Dozens of forms, all different structures, all in XML format that made sense for their legacy system.

My job was to transform these XML forms into JSON that worked with Formio.js, a JavaScript form rendering library. Each XML element type needed different handling — text fields, dropdowns, date pickers, nested groups, conditional fields.

The naive approach would be a giant switch statement:

foreach ($xmlElements as $element) {
    switch ($element->type) {
        case 'text':
            // 20 lines of text field transformation
            break;
        case 'select':
            // 25 lines of select transformation
            break;
        case 'date':
            // 15 lines of date transformation
            break;
        case 'group':
            // 30 lines of nested group handling
            break;
        // ... 15 more cases
    }
}

This works until it doesn't. The switch grows. Each case gets more complex. Testing is painful. Adding a new field type means touching a file that's already 500 lines long.

Strategy Pattern to the Rescue

The Strategy pattern says: instead of one object deciding how to handle every case, create separate objects that each know how to handle one case.

First, I defined what every transformer needs to do:

interface XmlFieldTransformer
{
    public function supports(SimpleXMLElement $element): bool;
    
    public function transform(SimpleXMLElement $element): FormioField;
}

Then I created a transformer for each field type:

class TextFieldTransformer implements XmlFieldTransformer
{
    public function supports(SimpleXMLElement $element): bool
    {
        return (string) $element['type'] === 'text';
    }
    
    public function transform(SimpleXMLElement $element): FormioField
    {
        return new FormioField(
            type: 'textfield',
            key: (string) $element['name'],
            label: (string) $element['label'],
            placeholder: (string) $element['placeholder'] ?? '',
            validate: $this->buildValidation($element),
        );
    }
    
    private function buildValidation(SimpleXMLElement $element): array
    {
        $rules = [];
        
        if ((string) $element['required'] === 'true') {
            $rules['required'] = true;
        }
        
        if ($maxLength = (string) $element['maxlength']) {
            $rules['maxLength'] = (int) $maxLength;
        }
        
        return $rules;
    }
}
class SelectFieldTransformer implements XmlFieldTransformer
{
    public function supports(SimpleXMLElement $element): bool
    {
        return (string) $element['type'] === 'select';
    }
    
    public function transform(SimpleXMLElement $element): FormioField
    {
        return new FormioField(
            type: 'select',
            key: (string) $element['name'],
            label: (string) $element['label'],
            data: [
                'values' => $this->extractOptions($element),
            ],
            validate: [
                'required' => (string) $element['required'] === 'true',
            ],
        );
    }
    
    private function extractOptions(SimpleXMLElement $element): array
    {
        $options = [];
        
        foreach ($element->option as $option) {
            $options[] = [
                'label' => (string) $option,
                'value' => (string) $option['value'],
            ];
        }
        
        return $options;
    }
}
class DateFieldTransformer implements XmlFieldTransformer
{
    public function supports(SimpleXMLElement $element): bool
    {
        return (string) $element['type'] === 'date';
    }
    
    public function transform(SimpleXMLElement $element): FormioField
    {
        return new FormioField(
            type: 'datetime',
            key: (string) $element['name'],
            label: (string) $element['label'],
            format: $this->mapDateFormat((string) $element['format']),
            enableTime: false,
            validate: [
                'required' => (string) $element['required'] === 'true',
            ],
        );
    }
    
    private function mapDateFormat(string $xmlFormat): string
    {
        return match ($xmlFormat) {
            'DD/MM/YYYY' => 'dd/MM/yyyy',
            'MM/DD/YYYY' => 'MM/dd/yyyy',
            'YYYY-MM-DD' => 'yyyy-MM-dd',
            default => 'yyyy-MM-dd',
        };
    }
}

The Transformer Manager

Now I needed something to coordinate these transformers:

class XmlFormTransformer
{
    /** @var array<XmlFieldTransformer> */
    private array $transformers;
    
    public function __construct(array $transformers)
    {
        $this->transformers = $transformers;
    }
    
    public function transform(SimpleXMLElement $form): FormioSchema
    {
        $fields = [];
        
        foreach ($form->field as $element) {
            $fields[] = $this->transformField($element);
        }
        
        return new FormioSchema(
            title: (string) $form['title'],
            components: $fields,
        );
    }
    
    private function transformField(SimpleXMLElement $element): FormioField
    {
        foreach ($this->transformers as $transformer) {
            if ($transformer->supports($element)) {
                return $transformer->transform($element);
            }
        }
        
        throw new UnsupportedFieldTypeException(
            "No transformer found for field type: " . (string) $element['type']
        );
    }
}

Wiring It Up in Laravel

In a service provider:

public function register(): void
{
    $this->app->singleton(XmlFormTransformer::class, function () {
        return new XmlFormTransformer([
            new TextFieldTransformer(),
            new SelectFieldTransformer(),
            new DateFieldTransformer(),
            new GroupFieldTransformer(),
            new CheckboxFieldTransformer(),
            // ... add more as needed
        ]);
    });
}

The Console Command

With the pattern in place, the console command became simple:

class TransformXmlFormsCommand extends Command
{
    protected $signature = 'forms:transform {input} {output}';
    
    protected $description = 'Transform XML forms to Formio JSON';
    
    public function handle(XmlFormTransformer $transformer): int
    {
        $inputPath = $this->argument('input');
        $outputPath = $this->argument('output');
        
        $xmlFiles = glob($inputPath . '/*.xml');
        
        foreach ($xmlFiles as $xmlFile) {
            $xml = simplexml_load_file($xmlFile);
            $schema = $transformer->transform($xml);
            
            $jsonFile = $outputPath . '/' . basename($xmlFile, '.xml') . '.json';
            file_put_contents($jsonFile, $schema->toJson(JSON_PRETTY_PRINT));
            
            $this->info("Transformed: {$xmlFile} → {$jsonFile}");
        }
        
        return Command::SUCCESS;
    }
}

Why This Was Worth It

The Strategy pattern gave me:

Isolation. Each transformer is self-contained. When the date picker needed special handling for time zones, I only touched DateFieldTransformer.

Testability. I could unit test each transformer independently with focused test cases.

Extensibility. When the client said "oh, we also have file upload fields" — I created FileUploadTransformer, registered it, done. No touching existing code.

Readability. Anyone looking at the code can understand the structure immediately. Each file has one job.

Share: