Generating XML files by using the strategy pattern

A while back we had a problem in a project of ours in which we had to send a couple of different XML files to a remote server. The way we had to transfer was the same for all types of XML files. We started out with various the classes that looked like this.

final class XmlTransferCustomer
{
    private CustomerXmlGenerator $generator;

    public function transfer(Customer $entity)
    {
        /** @var Xml $xml */
        $xml = $this->generator->generate($entity);
        $this->storage->put($xml->getFilePath(), $xml->getContent());
    }
}

final class CustomerXmlGenerator
{
    public function generate(Customer $entity)
    {
        return new Xml('filepath.xml', '<?xml version="1.0" encoding="UTF-8"?>');
    }
}

As you can imagine after a while we got a lot of the same classes. So we searched for a solution. We came up with something that uses the strategy pattern.

Strategy Pattern

The strategy pattern is a design pattern in which you define a generic task with different ways to get the same result, in our case an XML file. All the different ways to generate an XML file will be split in different classes, called 'strategies'. The 'task' class will then select which strategy is the right one to run the logic for a given input. Let me show you the gist of our solution.

Implementation

First we started with defining an interface for the entities that had to be transferred, TransferableInterface. This was just an empty interface, just for the purpose of recognizing the entities that can be passed to the XmlTransfer class. The XmlTransfer class looks like this now.

final class XmlTransfer
{
    private XmlGenerator $generator;

    public function transfer(TransferableInterface $entity)
    {
        /** @var Xml $xml */
        $xml = $this->generator->generate($entity);
        $this->storage->put($xml->getFilePath(), $xml->getContent());
    }
}

As you can see the class we added is basically the same as XmlTransferCustomer, but instead of accepting a Customer-object we accept the TransferableInterface. Now we had to create a new XmlGenerator-class which accepts a TransferableInterface-object.

final class XmlGenerator
{
    private XmlGeneratorStrategyInterface[] $generators;

    public function generate(TransferableInterface $entity): Xml
    {
        foreach ($this->generators as $generator) {
            if (!$generator->supports($entity)) {
                continue;
            }
            return $this->generator->generate($entity);
        }
    }
}

In the code you see that we loop over an array called generators, this is an array which only contains classes of the type XmlGeneratorStrategyInterface, the interface looks like this.

interface XmlGeneratorStrategyInterface {
    public function supports(TransferableInterface $entity): bool;
    public function generator(TransferableInterface $entity): Xml;
}

We keep looping over the $this->generators list until we find a generator that supports the given entity. In the example implementation we won't do anything if none of the generators support the given entity, but you could throw an exception or a null object.

An example implementation looks like this.

final class CustomerXmlGeneratorStrategy implements XmlGeneratorStrategyInterface
{
    public function supports(TransferableInterface $entity): bool
    {
        return $entity instanceof Customer::class;
    }

    public function generator(TransferableInterface $entity): Xml
    {
        // You can inject other dependencies here to build a more advanced xml file
        return new Xml('filepath.xml', '<?xml version="1.0" encoding="UTF-8"?>');
    }

}

Using this with Symfony

Last we had to inject classes that implement the XmlGeneratorStrategyInterface into the XmlGenerator class. Since we used Symfony in this project, we can easily do this in the service container by adding this to our services.yml

services:
    _instanceof:
        XmlGeneratorStrategyInterface:
            tags: ['xml.generators']

    XmlGenerator:
        class: XmlGenerator
        arguments:
            $generators: !tagged_iterator xml.generators

What this does is it will tag all classes which implement XmlGeneratorStrategyInterface with the tag xml.generators. When we inject XmlGenerator in a class the service container will inject all the implementation of XmlGeneratorStrategyInterface into the $generators property.

The result of this implementation that it's much easier to transfer other entities, because you only have to implement the TransferableInterface on the entity and create a new class which implements XmlGeneratorStrategyInterface.