PHP Design Patterns — Adapter
Users are loving your application and you want to give them more joy. What could bring them more happiness than email notifications?
Let’s welcome our users!
We want our users to feel good after signing up in our application, so why not send an welcome email? You already know a reliable email service provider that has a SDK you can install using Composer, so everything seems lovely so far.
Unfortunately, when integrating the SDK you notice the classes doesn’t fit very well. SignUpService
only has the recipient email and the content, and ThirdPartyEmailClient
expects configuration parameters.
Our hypotethical SDK has the following structure:
class ThirdPartyEmailClient
{
public function __construct(
private string $apiKey,
private string $region,
) {
}
public function sendEmail(
string $recipient,
string $content,
): void {
echo sprintf("Using apiKey %s and region %s", $this->apiKey, $this->region);
echo sprintf("Sending email to %s and content %s", $recipient, $content);
}
}
Make it work
If ThirdPartyEmailClient
is asking for apiKey
and region
, let's fullfil its wishes. We could simply do it by injecting both parameters on SignUpService
, or even by hardcoding them. It wouldn't be a problem until you need to send an email after user updated his profile that is implemeted on ProfileService
class.
Well, you’re just figuring out how to make it work because you’re a good professional and want to get things done, then you just copy and paste the hardcoded parameters from SignUpService
. Good job!
Something feels wrong
That night you couldn’t sleep because of that code duplication. You’re also exposing the apiKey
everywhere. How to solve this problem?
Make it right
Then you remember of this pattern called Adapter
. It is supposed to make classes that have incompatible interfaces work together. First step is creating the interface expected by client:
interface EmailSenderAdapter
{
public function sendEmail(string $recipient, string $content): void;
}
Now you just need to wrap ThirdPartyEmailClient
in a new class that implements the previous interface:
final class ThirdPartyEmailSenderAdapter implements EmailSenderAdapter
{
public function __construct(private ThirdPartyEmailClient $emailClient)
{
}
public function sendEmail(string $recipient, string $content): void
{
$this->emailClient->sendEmail($recipient, $content);
}
}
SignUpService
is happy now because it has a friend that understands its language.
And now to make it even better, you decide to add a unit test!
Testing
public function testThirdPartyEmailSenderAdapterAdapterUsesCorrectClient(): void
{
$emailClient = $this->createMock(ThirdPartyEmailClient::class);
$emailAdapter = new ThirdPartyEmailSenderAdapter($emailClient);
$emailClient->expects($this->once())
->method('sendEmail')
->with('email@email.com', 'I love design patterns');
$emailAdapter->sendEmail(
'email@email.com',
'I love design patterns');
}
You did it!
Your classes are seamless integrated, tested and if you need to change the email service provider you just need to wrap the new client to implement the EmailSenderAdapter
and you can switch an old interface for the new one. Congratulations.