Mastering the ‘Abstract Factory’ Design Pattern in Symfony

Filip Horvat
7 min readFeb 16, 2024

The Abstract Factory Design Pattern is one of the design patterns introduced in the book ‘Design Patterns,’ written by the Gang of Four. The authors of this book are widely regarded as authorities in the field of software engineering.

The Abstract Factory is a creational design pattern, and its purpose is to provide an interface for creating families of related or dependent objects without specifying their concrete classes. This might seem very abstract to you, but in the example below, I will explain in more detail, with a real implementation, the purpose of that design pattern.

Abstract Factory consist of 5 participants:

  • AbstractFactory
  • ConcreteFactory
  • AbstractProduct
  • ConcreteProduct
  • Client

In PHP implementation, AbstractFactory is an interface implemented by ConcreteFactory, and AbstractProduct is an interface implemented by ConcreteProduct.

In this story, I will try to explain, using my own example, how to implement and incorporate Abstract Factory Design Pattern into Symfony.

The Abstract Factory pattern also finds usage in Symfony; for example, it is utilized in the SecurityBundle in Symfony:

Example

The example used to explain an Abstract Factory may not be ideal because I aimed for simplicity, but it will still serve its purpose.

In our app, we will have a File logger with a basic version where only text can be logged, and an advanced version where the text, userId, and time can be logged. Additionally, we will also have an Es Logger (ElasticSearch logger), also with basic and advanced version, with the same interfaces as the File logger. We want to change loggers from a File to ES and vice versa with one change in configuration at any time.

Concrete Product participants

We have 4 concrete products participants, basic and advanced logger for file, and basic and advanced logger for ES.

<?php

namespace App\Service\AbstractFactoryExample\ConcreteProduct;

use App\Service\AbstractFactoryExample\AbstractProduct\BasicLogger;

class FileBasicLogger implements BasicLogger
{
public function log(string $text): void
{
echo 'Storing to FILE_basic: '.$text."\n";
}
}
<?php

namespace App\Service\AbstractFactoryExample\ConcreteProduct;

use App\Service\AbstractFactoryExample\AbstractProduct\AdvancedLogger;
use DateTime;

class FileAdvancedLogger implements AdvancedLogger
{
public function log(string $text, int $userId, DateTime $dateTime): void
{
echo 'Storing to FILE_advanced: '.$text.', user='.$userId.', time='.$dateTime->format('Y-m-d H:i:s')."\n";
}
}
<?php

namespace App\Service\AbstractFactoryExample\ConcreteProduct;

use App\Service\AbstractFactoryExample\AbstractProduct\BasicLogger;

class EsBasicLogger implements BasicLogger
{
public function log(string $text): void
{
echo 'Storing to ES_basic: '.$text."\n";
}
}
<?php

namespace App\Service\AbstractFactoryExample\ConcreteProduct;

use App\Service\AbstractFactoryExample\AbstractProduct\AdvancedLogger;
use DateTime;

class EsAdvancedLogger implements AdvancedLogger
{
public function log(string $text, int $userId, DateTime $dateTime): void
{
echo 'Storing to ES_advanced: '.$text.', user='.$userId.', time='.$dateTime->format('Y-m-d H:i:s')."\n";
}
}

Abstract Product participants

We have two abstract products participants, one for basic logger and one for ES logger:

<?php

namespace App\Service\AbstractFactoryExample\AbstractProduct;

interface BasicLogger
{
public function log(string $text): void;
}
<?php

namespace App\Service\AbstractFactoryExample\AbstractProduct;

use DateTime;

interface AdvancedLogger
{
public function log(string $text, int $userId, DateTime $dateTime): void;
}

Abstract Factory

Only one Abstract factor.

<?php

namespace App\Service\AbstractFactoryExample\AbstractFactory;

use App\Service\AbstractFactoryExample\AbstractProduct\AdvancedLogger;
use App\Service\AbstractFactoryExample\AbstractProduct\BasicLogger;

interface AbstractFactory
{
public function createBasicLogger(): BasicLogger;

public function createAdvancedLogger(): AdvancedLogger;
}

Concrete Factory

We also have two concrete factories, one for File family of products and one for ES family of products.

<?php

namespace App\Service\AbstractFactoryExample\ConcreteFactory;

use App\Service\AbstractFactoryExample\AbstractFactory\AbstractFactory;
use App\Service\AbstractFactoryExample\AbstractProduct\AdvancedLogger;
use App\Service\AbstractFactoryExample\AbstractProduct\BasicLogger;
use App\Service\AbstractFactoryExample\ConcreteProduct\FileAdvancedLogger;
use App\Service\AbstractFactoryExample\ConcreteProduct\FileBasicLogger;

class FileLoggerFactory implements AbstractFactory
{
public function createBasicLogger(): BasicLogger
{
return new FileBasicLogger();
}

public function createAdvancedLogger(): AdvancedLogger
{
return new FileAdvancedLogger();
}
}
<?php

namespace App\Service\AbstractFactoryExample\ConcreteFactory;

use App\Service\AbstractFactoryExample\AbstractFactory\AbstractFactory;
use App\Service\AbstractFactoryExample\AbstractProduct\AdvancedLogger;
use App\Service\AbstractFactoryExample\AbstractProduct\BasicLogger;
use App\Service\AbstractFactoryExample\ConcreteProduct\EsAdvancedLogger;
use App\Service\AbstractFactoryExample\ConcreteProduct\EsBasicLogger;

class EsLoggerFactory implements AbstractFactory
{
public function createBasicLogger(): BasicLogger
{
return new EsBasicLogger();
}

public function createAdvancedLogger(): AdvancedLogger
{
return new EsAdvancedLogger();
}
}

Client

The client will be an Order service where both the basic logger and advanced logger are used.

<?php

namespace App\Service\AbstractFactoryExample\Client;

use App\Service\AbstractFactoryExample\AbstractProduct\AdvancedLogger;
use App\Service\AbstractFactoryExample\AbstractProduct\BasicLogger;
use Carbon\Carbon;

class PlaceOrderService
{
public function __construct(
private readonly BasicLogger $basicLogger,
private readonly AdvancedLogger $advancedLogger,
) {
}

public function placeOrder(): void
{
$this->basicLogger->log('Start PlaceOrder');

// working..

$this->advancedLogger->log('Order #17 - placed', 18, Carbon::now());

$this->basicLogger->log('Starting transaction in PlaceOrder');

// working..

$this->basicLogger->log('Finish transaction in PlaceOrder');

$this->advancedLogger->log('Order #17 - charged', 18, Carbon::now());

$this->basicLogger->log('Finish PlaceOrder');
}
}

Run the code without Symfony

This is the PHP code where previouly mentioned participants from Abstract factory are used:

public function test()
{
echo "Client: Testing client code with the first factory type";
$this->clientCode(new EsLoggerFactory());


echo "Client: Testing the same client code with the second factory type";
$this->clientCode(new FileLoggerFactory());
}

private function clientCode(AbstractFactory $factory): void
{
$basicLogger = $factory->createBasicLogger();
$advancedLogger = $factory->createAdvancedLogger();

$basicLogger->log('test');
$advancedLogger->log('test2', 1, new DateTime());
}

Output is:

Client: Testing client code with the first factory type:
Storing to ES_basic: test
Storing to ES_advanced: test2, user=1, time=2024-02-16 11:45:32

Client: Testing the same client code with the second factory type:
Storing to FILE_basic: test
Storing to FILE_advanced: test2, user=1, time=2024-02-16 11:45:32

You can see that the general idea of Abstract Factory is to run the same piece of code with different product families with only passing and changing concrete factory, not product itself.

We just changed one line of code:

$this->clientCode(new EsLoggerFactory());

to:

$this->clientCode(new FileLoggerFactory());

And the whole family of products is created differently:

FileBasicLogger
FileAdvancedLogger
...

to:

EsBasicLogger
EsAdvancedLogger
...

In this example, we have two families of products: ‘FileLogger’ and ‘ESLogger.’ Each family has two products: ‘BasicLogger’ and ‘AdvancedLogger.

When you have more products in a family, it makes more sense. For example, if you have ‘BasicLogger,’ ‘AdvancedLogger,’ ‘CustomLogger,’ ‘OrderLogger,’ etc., all of those items would need to be changed individually when you switch from the ‘FileLogger’ family to the ‘ESLogger’ family.

With the abstract factory pattern, if you want to change a whole family of products, you only need to modify the code in one place. Without the Abstract Factory, you would need to change each product in the family individually.

Run the code with Symfony

If there is any confusion, it may become clearer with a Symfony example. In the following illustration, we will incorporate the same code as mentioned above into Symfony.

You can see that in our service, which acts as a Client participant in the Abstract Factory Implementation, we injected Abstract Products: BasicLogger and AdvancedLogger:

<?php

namespace App\Service\AbstractFactoryExample\Client;

use App\Service\AbstractFactoryExample\AbstractProduct\AdvancedLogger;
use App\Service\AbstractFactoryExample\AbstractProduct\BasicLogger;
use Carbon\Carbon;

class PlaceOrderService
{
public function __construct(
private readonly BasicLogger $basicLogger,
private readonly AdvancedLogger $advancedLogger,
) {
}

public function placeOrder(): void
{
$this->basicLogger->log('Start PlaceOrder');

// working..

$this->advancedLogger->log('Order #17 - placed', 18, Carbon::now());

$this->basicLogger->log('Starting transaction in PlaceOrder');

// working..

$this->basicLogger->log('Finish transaction in PlaceOrder');

$this->advancedLogger->log('Order #17 - charged', 18, Carbon::now());

$this->basicLogger->log('Finish PlaceOrder');
}
}

The idea is to change only one place in the configuration when we want to switch BasicLogger and AdvancedLogger from file to ES and vice versa.

We will use Symfony utility “Using a Factory to Create Services”:

We will instruct Symfony that when BasicLogger is typehinted, it should execute the code from the Abstract Factory in the method createBasicLogger to fetch the real implementation. Similarly, we will do the same for AdvancedLogger:

services:
App\Service\AbstractFactoryExample\AbstractProduct\BasicLogger:
factory: [ '@App\Service\AbstractFactoryExample\AbstractFactory\AbstractFactory', 'createBasicLogger' ]

App\Service\AbstractFactoryExample\AbstractProduct\AdvancedLogger:
factory: [ '@App\Service\AbstractFactoryExample\AbstractFactory\AbstractFactory', 'createAdvancedLogger' ]

The final step is to specify in the configuration which implementation of the AbstractFactory should be used. Currently, it is defined as an abstract class (AbstractFactory), and we will address this with:

App\Service\AbstractFactoryExample\AbstractFactory\AbstractFactory: '@App\Service\AbstractFactoryExample\ConcreteFactory\EsLoggerFactory'

So the final services configuration is:

services:
App\Service\AbstractFactoryExample\AbstractFactory\AbstractFactory: '@App\Service\AbstractFactoryExample\ConcreteFactory\EsLoggerFactory'

App\Service\AbstractFactoryExample\AbstractProduct\BasicLogger:
factory: [ '@App\Service\AbstractFactoryExample\AbstractFactory\AbstractFactory', 'createBasicLogger' ]

App\Service\AbstractFactoryExample\AbstractProduct\AdvancedLogger:
factory: [ '@App\Service\AbstractFactoryExample\AbstractFactory\AbstractFactory', 'createAdvancedLogger' ]

Now Symfony knows that the factory to use for the createBasicLogger function is the FileLoggerFactory.

Now if we run code from the client:

<?php

namespace App\Service\AbstractFactoryExample\Client;

use App\Service\AbstractFactoryExample\AbstractProduct\AdvancedLogger;
use App\Service\AbstractFactoryExample\AbstractProduct\BasicLogger;
use Carbon\Carbon;

class PlaceOrderService
{
public function __construct(
private readonly BasicLogger $basicLogger,
private readonly AdvancedLogger $advancedLogger,
) {
}

public function placeOrder(): void
{
$this->basicLogger->log('Start PlaceOrder');

// working..

$this->advancedLogger->log('Order #17 - placed', 18, Carbon::now());

$this->basicLogger->log('Starting transaction in PlaceOrder');

// working..

$this->basicLogger->log('Finish transaction in PlaceOrder');

$this->advancedLogger->log('Order #17 - charged', 18, Carbon::now());

$this->basicLogger->log('Finish PlaceOrder');
}
}

This is the output:

Storing to FILE_basic: Start PlaceOrder
Storing to FILE_advanced: Order #17 - placed, user=18, time=2024-02-16 12:36:16
Storing to FILE_basic: Starting transaction in PlaceOrder
Storing to FILE_basic: Finish transaction in PlaceOrder
Storing to FILE_advanced: Order #17 - charged, user=18, time=2024-02-16 12:36:16
Storing to FILE_basic: Finish PlaceOrder

If we want to switch to ES logger we only need to change one item in configuration:

services:
App\Service\AbstractFactoryExample\AbstractFactory\AbstractFactory: '@App\Service\AbstractFactoryExample\ConcreteFactory\FileLoggerFactory'

to:

services:
App\Service\AbstractFactoryExample\AbstractFactory\AbstractFactory: '@App\Service\AbstractFactoryExample\ConcreteFactory\EsLoggerFactory'

And now the output is:

Storing to ES_basic: Start PlaceOrder
Storing to ES_advanced: Order #17 - placed, user=18, time=2024-02-16 12:37:32
Storing to ES_basic: Starting transaction in PlaceOrder
Storing to ES_basic: Finish transaction in PlaceOrder
Storing to ES_advanced: Order #17 - charged, user=18, time=2024-02-16 12:37:32
Storing to ES_basic: Finish PlaceOrder

We just changed a factory and the whole family of products is changed, from first family of products:

FileBasicLogger
FileAdvancedLogger
...

to second family of products:

EsBasicLogger
EsAdvancedLogger
...

We changed a whole family of products with one change, without Abstract Factory you would need to change each product individually, something like this:

App\Service\AbstractFactoryExample\AbstractProduct\BasicLogger: '@App\Service\AbstractFactoryExample\ConcreteProduct\FileBasicLogger'
App\Service\AbstractFactoryExample\AbstractProduct\AdvancedLogger: '@App\Service\AbstractFactoryExample\ConcreteProduct\FileAdvancedLogger'

to:

App\Service\AbstractFactoryExample\AbstractProduct\BasicLogger: '@App\Service\AbstractFactoryExample\ConcreteProduct\EsBasicLogger'
App\Service\AbstractFactoryExample\AbstractProduct\AdvancedLogger: '@App\Service\AbstractFactoryExample\ConcreteProduct\EsAdvancedLogger'

This might not seem like a big deal, but if you have a large number of products in the family, it makes more sense to use an Abstract Factory.

That’s all I hope you enjoyed!

--

--

Filip Horvat

Senior Software Engineer, Backend PHP Developer, Located at Croatia, Currently working at myzone.com