Mastering the ‘Adapter’ Design Pattern in Symfony

Filip Horvat
5 min readFeb 13, 2024

--

The Adapter 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 Adapter pattern is a structural design pattern, and its purpose is to convert the interface of a class into another interface that clients expect. Adapter lets classes work together that couldn’t otherwise because of incompatible interfaces.

Adapter consist of 4 participants:

  • Client
  • Target
  • Adapter
  • Adaptee

In PHP implementation Target is an interface whose implementations is used by Client. Adaptee is an class which we want to use by client but it does not implements Target interface, Adapter is a class which will make this possible.

In this story, I will try to explain, using my own example, how to implement and incorporate decorator design pattern into Symfony.

Adapter pattern is widely used in Symfony, for example when you are using cache component, you have many adapters to use:

Example

For the adapter example, let’s consider some hypothetical logger system. While this example may seem a bit simplistic, it serves to illustrate the concept of the adapter pattern and demonstrates how and why it is utilized in Symfony.

In our application, we have multiple services where logging is required, and for this purpose, we’ll utilize imaginary bundles. At the initial stage of the application life cycle, we log information into log files using the FileLogger class from the fictional 'FileLoggerBundle'.

Target participant

The FileLogger within the FileLoggerBundle bundle employs an interface for logging, which is also an imaginary interface defined in a PSR package. This interface serves as the target participant in our Adapter implementation:

<?php

//Target participant in "adapter implementation"

namespace App\Service\AdapterExample;

interface LoggerInterface
{
public function log($level, string $message, array $context = []): void;
}

FileLogger implementation for logging in FileLoggerBundle:

<?php

namespace App\Service\AdapterExample;

class FileLogger implements LoggerInterface
{
public function log($level, string $message, array $context = []): void
{
// here is a code for logging into the files. Config where to log is defined in parameters
}
}

Client participant

Now, across various services, we have code for logging, and this code essentially acts as the client participant in our Adapter implementation:

<?php

//Client participant in "adapter implementation"

namespace App\Service\AdapterExample;

class OrderService
{
public function __construct(private readonly LoggerInterface $logger)
{
}

public function placeOrder()
{
// something...
$this->logger->log('Level 1', 'User #1 made an Order #1');
// something...
}
}

The FileLogger is injected where LoggerInterface is typehinted, because we defined that in our services.yaml configuration:

services:
#something...
App\Service\AdapterExample\LoggerInterface: '@App\Service\AdapterExample\FileLogger'

We inject LoggerInterface rather than FileLogger to facilitate effortless changes to the logger implementation in the future.

The code executed in OrderService currently calls methods from the FileLogger.

Later, we discover that previewing log files stored as files is challenging, prompting us to change the logger from FileLogger to SentryLogger.

The SentryLogger is another service from the hypothetical bundle 'SentryLoggerBundle.' Upon inspection, we discover that this implementation also uses the same interface defined in PSR, just like the 'FileLoggerBundle’ :

<?php

namespace App\Service\AdapterExample;

class SentryLogger implements LoggerInterface
{
public function log($level, string $message, array $context = []): void
{
// here is a code for logging into the sentry. Config where to log is defined in parameters
}
}

Consequently, the only change required is to update the configuration from:

services:
#something...
App\Service\AdapterExample\LoggerInterface: '@App\Service\AdapterExample\FileLogger'

to:

services:
#something...
App\Service\AdapterExample\LoggerInterface: '@App\Service\AdapterExample\SentryLogger'

Now, the application will begin sending logs to Sentry without any changes in the code. Of course, configuration adjustments are needed, such as providing the Sentry secret, but these are handled in the configuration — leaving the code untouched.

Later, still unsatisfied with the log filtering in Sentry, we decide to change the logging to Elasticsearch. We discover a suitable imaginary bundle, ‘ElasticsearchLoggerBundle,’ for this purpose.

The challenge arises because the ElasticsearchLogger from the imaginary bundle does not implement the LoggerInterface from PSR, as the FileLogger and SentryLogger classes do. Instead, it adheres to a similar but different interface, named LoggerNewInterface from PSR:

<?php

namespace App\Service\AdapterExample;

interface LoggerNewInterface
{
public function log($level, string $message, string $description, array $context = []): void;
}

Please note that LoggerNewInterface includes $description in its interface definition. This interface is not compatible with the LoggerInterface already used in our code, as it lacks the $description attribute.

So, what can we do?

We could clone that bundle, add the LoggerInterface interface inside ElasticsearchLogger, and implement that method. However, that approach is obviously not advisable, and we will refrain from doing so.

What else can we do?

We could change all our services where LoggerInterface is typehinted and switch to using LoggerNewInterface. However, this approach is also impractical; we do not want to modify our existing code, and we wish to retain the flexibility to easily switch back to using a File or Sentry logger in the future.

What can we do?

Of course we will use Adapter to overcome that problem.

Previously, you saw the client and target participants in the adapter implementation, and now you will see the other two.

Adaptee participant

Our adaptee participant will be the ElasticsearchLogger from that imaginary bundle:

<?php

//Adaptee participant in "adapter implementation"

namespace App\Service\AdapterExample;

class ElasticsearchLogger implements LoggerNewInterface
{
public function log($level, string $message, string $description, array $context = []): void
{
// here is a code for logging into the elasticseach. Config where to log is defined in parameters
}
}

Adapter participant

Since the Adaptee (ElasticsearchLogger) cannot be directly injected into our services where LoggerInterface is typehinted, we require an adapter to facilitate this integration:

<?php

//Adapter participant in "adapter implementation"

namespace App\Service\AdapterExample;


class ElasticsearchLoggerAdapter implements LoggerInterface
{
public function __construct(private readonly ElasticsearchLogger $logger)
{
}

public function log($level, string $message, array $context = []): void
{
$this->logger->log($level, $message, '', $context);
}
}

In the constructor, the adapter receives the Adaptee (ElasticsearchLogger). Simultaneously, it implements the Target (LoggerInterface), allowing it to be injected where LoggerInterface is typehinted. Additionally, it can call code from the Adaptee (ElasticsearchLogger) within the log() function as it does.

Now in the services we will change this configuration:

services:
#something...
App\Service\AdapterExample\LoggerInterface: '@App\Service\AdapterExample\SentryLogger'

to:

services:
#something...
App\Service\AdapterExample\LoggerInterface: '@App\Service\AdapterExample\ElasticsearchLoggerAdapter'

In the services, we updated the configuration to utilize the Elasticsearch logger. However, instead of using the service or interface directly, we use the adapter.

ElasticsearchLoggerAdapter implements LoggerInterface, so we can inject that into our service where LoggerInterface is typehinted.

“When we call the log() method of ElasticsearchLoggerAdapter, it, in turn, invokes the log() method from ElasticsearchLogger.

What we did was adapt ElasticsearchLogger from the bundle to make it possible to invoke it with a different interface than it originally implements.

We cannot add a custom description to Elasticsearch log entries from the code, and for all Elasticsearch log entries, an empty string is added. However, this is acceptable to us as a reasonable compromise. After all, we did not send a description to the logs in previous logger implementations.

That’s all I hope you enjoyed!

--

--

Filip Horvat

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