The Factory Pattern in PHP - with Example

The Factory pattern is among the most commonly used design patterns in many programming languages. It allows us to encapsulate the instantiation of complex objects, so we don't have to do it in our business logic. Basically we just move this kind of code to a more appropriate place.

While cars and engines etc. are quite popular to use as examples, we're going to stick with something more e-commerce related.

Creating a Stock Importer

Let's imagine we have to import product stocks into a webshop or ERP system - we'll refer to it as app. To achieve this, users want to either upload a csv file containing the SKU's and the current stock quantities into our app. Or they want to configure an ftp account where the csv file is fetched and imported from. Furthermore, they want to know if any errors occurred during the import.

For the sake of simplicity, we keep this somewhat basic. We create some sort of console command to execute our stock importer and then the actual stock importer itself. We keep the instantiation of the used classes very simple. Most of the time this will be more complicated/complex.

class Console
{
    public function run(): void
    {
        $stockImporter = new StockImporter();
        $stockImporter->import();
    }
}
class StockImporter
{
    public function import(): void
    {
        $container = \App::getContainer();
        $reader = new StockReader($container->get('some.ftp.datasource'));
        $writer = new StockWriter($container->get('some.stock.repository'));
        $mapper = new StockMapper(new CsvStockMapping());
        $logger = new Logger($container->get('some.logging.config'));

        foreach ($reader->fetchRows() as $row) {
            try {
                $writer->writeOne($mapper->map($row));
            } catch (\Exception $e) {
                $logger->logError($e->getMessage());
            }
        }
    }
}

As you can see, there's quite a lot of stuff within our import method that could actually be done elsewhere. Should we move this to the constructor, maybe?

Properties Instead of Local Variables

Moving everything that is not actual logic to the constructor should make this somewhat cleaner. Let's give it a try and see what we end up with.

class StockImporter
{
    private StockReader $reader;
    private StockWriter $writer;
    private StockMapper $mapper;
    private Logger $logger;

    public function __construct()
    {
        $container = \App::getContainer();

        $this->reader = new StockReader($container->get('some.ftp.datasource'));
        $this->writer = new StockWriter($container->get('some.stock.repository'));
        $this->mapper = new StockMapper(new CsvStockMapping());
        $this->logger = new Logger($container->get('some.logging.config'));
    }

    public function import(): void
    {
        foreach ($this->reader->fetchRows() as $row) {
            try {
                $this->writer->writeOne($this->mapper->map($row));
            } catch (\Exception $e) {
                $this->logger->logError($e->getMessage());
            }
        }
    }
}

Alright - this is a little better already. We moved everything into the constructor, and our import method is simply accessing the properties. But still - the StockImporter has to know so much about where to find its dependencies and their respective dependencies and configs. That has nothing to with our business logic at all - it's basically just bootstrapping. So let's clean up that constructor and just pass the instantiated dependencies to it.

Properly Injecting our Dependencies

In the example below, we remove everything related to the dependencies from our stock importer. Instead of creating them in the importer, we simply expect them as dependencies that are passed to the constructor.

class StockImporter
{
    private StockReader $reader;
    private StockWriter $writer;
    private StockMapper $mapper;
    private Logger $logger;

    public function __construct(
        StockReader $reader,
        StockWriter $writer,
        StockMapper $mapper,
        Logger $logger 
    ) {
        $this->reader = $reader;
        $this->writer = $writer;
        $this->mapper = $mapper;
        $this->logger = $logger;
    }

    public function import(): void
    {
        foreach ($this->reader->fetchRows() as $row) {
            try {
                $this->writer->writeOne($this->mapper->map($row));
            } catch (\Exception $e) {
                $this->logger->logError($e->getMessage());
            }
        }
    }
}

While it's a matter of taste and preference, I think we made this a lot cleaner already. The only logic remaining in the class is the actual stock import. What's left to do is the instantiation of the dependencies and of course a place to pass them to the constructor. This is where our factory enters the ring.

Implementing our Factory

Since our StockImporter is no longer creating its dependencies itself, we still need some place to create them. Creating things? Well, if that doesn't sound like a job for a factory.

So what should our factory look like? Personally, I've come to like prefixing methods that have the new keyword in them with create. For our example, that would be createStockImporter().

In small projects, I think its alright to just have one factory with a dozen or so methods. Once things start getting bigger, you may want to create one for each domain. For medium-sized projects, I like to use a Trait that allows us to retrieve a domain specific factory via methods like getStockFactory() or getCustomerFactory(). I think you get the point.

If you don't know about traits yet, below is a link to the PHP manual. In short, they add additional functionality to a class.

php manual: Traits PHP Manual

class StockFactory
{
    private Container $container;

    public function __construct(Container $container)
    {
        $this->container = $container;
    }

    public function createStockImporter(): StockImporter
    {
        return new StockImporter(
            new StockReader($this->container->get('some.ftp.datasource')),
            new StockWriter($this->container->get('some.stock.repository')),
            new StockMapper(new CsvStockMapping()),
            new Logger($this->container->get('some.logging.config'))
        ); 
    }
}
trait FactoryAwareTrait
{
    private function getStockFactory(): StockFactory
    {
        return new StockFactory(\App::getContainer());
    }
}
class Console
{
    use FactoryAwareTrait;

    public function run(): void
    {
        $this->getStockFactory()->createStockImporter()->import();
    }
}

Let's see what we got here. First, we have the actual factory, which knows about the container and what to retrieve from it. That makes perfect sense, just like a factory worker who knows how and where to get his tools and materials for a job.

The StockFactory simply retrieves all the necessary dependencies for our StockImporter from the container and returns a new instance of the importer.

The FactoryAwareTrait really just returns a new instance of our factory, while also injecting the container through the constructor.

Lastly, we use the FactoryAwareTrait trait in our console and retrieve the StockFactory and we are basically done.

Covering other Scenarios in the Factory

Assuming we have to add another source for stock data, we can simply add different methods to our factory, that inject different configs, mappers, data sources or whatever is necessary.

class StockFactory
{
    private Container $container;

    public function __construct(Container $container)
    {
        $this->container = $container;
    }

    public function createCsvStockImporter(): StockImporter
    {
        return $this->createStockImporter(
            $this->container->get('some.ftp.datasource'),
            new CsvStockMapping($this->container->get('some.csv.mapping.config'))
        );
    }

    public function createXmlStockImporter(): StockImporter
    {
        return $this->createStockImporter(
            $this->container->get('some.ftp.datasource'),
            new XmlStockMapping($this->container->get('some.xml.mapping.config'))
        );
    }

    public function createJsonStockImporter(): StockImporter
    {
        return $this->createStockImporter(
            $this->container->get('some.api.datasource'),
            new JsonStockMapping($this->container->get('some.json.mapping.config'))
        );
    }

    private function createStockImporter(
        StockDataSourceInterface $stockDataSource,
        StockMappingInterface $stockMapping
    ): StockImporter {
        return new StockImporter(
            new StockReader($stockDataSource),
            new StockWriter($this->container->get('some.stock.repository')),
            new StockMapper($stockMapping),
            new Logger($this->container->get('some.logging.config'))
        ); 
    }
}

As you can see, we made the basic factory method private and inject different dependencies in each of the public factory methods.

We simply continue to add more methods to our factory to cover different scenarios. The example is once again over-simplified, but should give an idea about the ease of adding more and more imports to the system without having to write a completely new importer every time.

class Console
{
    use FactoryAwareTrait;

    public function run(): void
    {
        $this->getStockFactory()->createCsvStockImporter()->import();
        // or 
        $this->getStockFactory()->createXmlStockImporter()->import();
        // or 
        $this->getStockFactory()->createJsonStockImporter()->import();
    }
}

In reality, our stock importer would be much more complex. And don't forget - classes tend to grow over time. Right now our import method is quite small a nice to look at, but in reality, this import might trigger followup processes, add things to a queue, trigger a state machine and so on ...
Therefore it's good to start cleaning up early on when it's still easy to do.

Thinking a step further, we could use interfaces for some dependencies, which would allow us to use the stock importer in other scenarios e.g. if we have different suppliers, and they all provide us with stock data in different formats.