Adapter Design Pattern in PHP
Sooner or later you'll need to plug a class into your code whose interface simply doesn't match what your code expects — a third-party library, a legacy class you're not allowed to touch, or an API client generated from someone else's spec. Rewriting either side isn't an option. The Adapter pattern solves this: it wraps the incompatible class in a small translator object that speaks the interface your code expects, and internally forwards the call to the method the wrapped class actually has.
What Is the Adapter Pattern?
Adapter is a structural pattern that lets two otherwise incompatible interfaces work together. Instead of changing the class you can't modify, you write a new class — the adapter — that implements the interface your code calls, and inside each method it translates the call into whatever the wrapped ("adaptee") class actually understands.
The Problem It Solves
Imagine your application defines a clean interface for processing payments, and every part of your
checkout flow is written against that interface. Now you need to integrate a legacy payment gateway
class that was written years ago (or supplied by a vendor) with a completely different method name
and parameter shape. You can't edit the vendor class, and you don't want to scatter
if ($gateway instanceof LegacyThing) special cases throughout your checkout code. An
adapter isolates that translation in exactly one place.
Worked Example: Adapting a Legacy Payment Gateway
First, the interface your checkout code already relies on everywhere:
<?php
interface PaymentProcessorInterface
{
// $amount is expressed in dollars, e.g. 49.99
public function pay(float $amount): bool;
}
?>
Next, the legacy class you were handed — you can't change its signature, it works in cents and a separate currency code, and it returns a status string instead of a boolean:
<?php
// Third-party / legacy class — do NOT modify this file
class LegacyPaymentGateway
{
public function makeTransaction(int $cents, string $currency): string
{
// Pretend this actually talks to a payment processor
$success = $cents > 0;
return $success ? 'TXN_OK' : 'TXN_FAILED';
}
}
?>
The adapter implements PaymentProcessorInterface, holds an instance of the legacy class,
and translates the call — converting dollars to cents and the string result to a boolean:
<?php
class PaymentAdapter implements PaymentProcessorInterface
{
private LegacyPaymentGateway $legacyGateway;
private string $currency;
public function __construct(LegacyPaymentGateway $legacyGateway, string $currency = 'USD')
{
$this->legacyGateway = $legacyGateway;
$this->currency = $currency;
}
public function pay(float $amount): bool
{
$cents = (int) round($amount * 100);
$status = $this->legacyGateway->makeTransaction($cents, $this->currency);
return $status === 'TXN_OK';
}
}
?>
And here's the payoff — checkout code only ever depends on PaymentProcessorInterface. It
has no idea a legacy class is involved at all:
<?php
function checkout(PaymentProcessorInterface $processor, float $total): void
{
if ($processor->pay($total)) {
echo "Payment succeeded!";
} else {
echo "Payment failed.";
}
}
$processor = new PaymentAdapter(new LegacyPaymentGateway());
checkout($processor, 49.99);
?>
PaymentProcessorInterface — a new gateway, a mock for testing — and checkout() doesn't change at all. That's the whole point: the adaptee is hidden entirely behind the adapter.Object Adapter vs. Class Adapter
The example above is an object adapter — it holds an instance of the adaptee and
delegates to it. PHP has no multiple inheritance, so a class adapter (extending the
adaptee directly) is rarely practical here; the object adapter style shown above is the idiomatic
approach in PHP, and it has the added benefit of working even when the adaptee class is
final.
When To Reach For This Pattern
| Situation | Why Adapter helps |
|---|---|
| Integrating a third-party library | Wrap the vendor's classes behind your own interface so a future library swap only touches the adapter |
| Legacy code you can't modify | Bridge old method signatures to the interface new code expects, without touching the legacy class |
| Combining two APIs with different shapes | Let both sides of your app agree on one common interface instead of branching on which API you're talking to |
| Unit testing against external services | Adapt a real SDK behind an interface, then substitute a test double implementing the same interface |