The Adapter Pattern Explained
Definition
The Adapter pattern converts the interface of a class into another interface clients expect. It lets classes work together that couldn't otherwise because of incompatible interfaces.
Problem: Incompatible External APIs
// Your internal payment interface
namespace Vendor\Payment\Api;
interface PaymentGatewayInterface
{
public function charge(float $amount, string $currency): PaymentResult;
public function refund(string $transactionId, float $amount): RefundResult;
}
// External library (you can't modify this)
class StripeSdk
{
public function createCharge(array $params): \Stripe\Charge
{
return \Stripe\Charge::create($params);
}
public function createRefund(string $chargeId, int $amountInCents): \Stripe\Refund
{
return \Stripe\Refund::create([
'charge' => $chargeId,
'amount' => $amountInCents,
]);
}
}
// StripeSdk API is incompatible with PaymentGatewayInterface
Solution: Adapter
namespace Vendor\Payment\Adapter;
class StripeAdapter implements \Vendor\Payment\Api\PaymentGatewayInterface
{
private \Stripe\StripeSdk $stripe;
public function __construct(string $apiKey)
{
$this->stripe = new \Stripe\StripeSdk($apiKey);
}
public function charge(float $amount, string $currency): \Vendor\Payment\Api\PaymentResult
{
// Adapt: convert our interface to Stripe's format
$stripeAmount = (int) ($amount * 100); // Stripe uses cents
$charge = $this->stripe->createCharge([
'amount' => $stripeAmount,
'currency' => strtolower($currency),
'source' => $this->getStripeToken(),
]);
// Adapt: convert Stripe's response to our format
return new PaymentResult(
success: $charge->status === 'succeeded',
transactionId: $charge->id,
message: $charge->status
);
}
public function refund(string $transactionId, float $amount): \Vendor\Payment\Api\RefundResult
{
$stripeAmount = (int) ($amount * 100);
$refund = $this->stripe->createRefund($transactionId, $stripeAmount);
return new RefundResult(
success: $refund->status === 'succeeded',
refundId: $refund->id
);
}
}
The adapter translates between your interface and the third-party API.
Object Adapter vs Class Adapter
Object Adapter (Composition)
Wraps the adaptee via composition (most common in PHP):
// Wraps PayPal SDK via composition
class PayPalAdapter implements PaymentGatewayInterface
{
private PayPalRestApiSdk $paypal; // Adaptee
public function __construct(PayPalRestApiSdk $paypal)
{
$this->paypal = $paypal;
}
public function charge(float $amount, string $currency): PaymentResult
{
$payment = $this->paypal->createPayment([
'intent' => 'sale',
'amount' => [
'total' => number_format($amount, 2, '.', ''),
'currency' => $currency,
],
]);
return new PaymentResult(
success: $payment->getState() === 'approved',
transactionId: $payment->getId()
);
}
}
When to Use Each
| Approach | When |
|---|---|
| Object Adapter | You can't extend the adaptee (external library, no inheritance) |
| Class Adapter | You can extend the adaptee and want to override specific methods |
In PHP, Object Adapter (composition) is almost always preferred because:
- PHP doesn't support multiple inheritance
- Composition is more flexible
- You can swap the adaptee at runtime
Magento Gateway Adapters
Magento Payment Gateway Adapters
Magento uses adapters extensively for payment gateways. Each gateway has an adapter that converts between Magento's payment interface and the gateway's API:
// Magento's payment gateway adapter pattern
// Each gateway adapter wraps an external SDK
namespace Magento\Payment\Gateway\Request\Builder\Composite;
// Builder (adapter) converts order data to gateway-specific format
interface BuilderInterface
{
public function build(array $buildSubject): array;
}
// Example: builds Stripe-specific request from Magento order data
class StripePaymentRequestBuilder implements BuilderInterface
{
public function build(array $buildSubject): array
{
$paymentDO = $buildSubject['payment'];
$order = $paymentDO->getOrder();
return [
'amount' => (int) ($order->getGrandTotalAmount() * 100),
'currency' => $order->getCurrencyCode(),
'source' => $buildSubject['token'],
'description' => sprintf(
'Order #%s',
$order->getOrderNumber()
),
];
}
}
// PayPal adapter builds different structure
class PayPalPaymentRequestBuilder implements BuilderInterface
{
public function build(array $buildSubject): array
{
$paymentDO = $buildSubject['payment'];
$order = $paymentDO->getOrder();
return [
'intent' => 'sale',
'payer' => [
'payment_method' => 'credit_card',
],
'transactions' => [[
'amount' => [
'total' => $order->getGrandTotalAmount(),
'currency' => $order->getCurrencyCode(),
],
]],
];
}
}
Shipping Carrier Adapters
// Shipping carrier adapters wrap carrier-specific APIs
namespace Vendor\Shipping\Adapter;
class DhlAdapter implements \Vendor\Shipping\Api\CarrierInterface
{
public function __construct(
private \Dhl\Api\Client $dhlClient // Third-party SDK
) {}
public function getRates(array $params): array
{
// Adapt: convert Magento params to DHL API format
$dhlRequest = $this->dhlClient->createRateRequest([
'originCountryCode' => $params['origin_country'],
'originPostalCode' => $params['origin_zip'],
'destinationCountryCode' => $params['dest_country'],
'destinationPostalCode' => $params['dest_zip'],
'weight' => $params['total_weight'],
]);
$dhlRates = $dhlRequest->getRates();
// Adapt: convert DHL response to our format
return array_map(fn($rate) => [
'carrier' => 'dhl',
'method' => $rate->getServiceCode(),
'price' => $rate->getTotalAmount() / 100,
'currency' => $rate->getCurrencyCode(),
'delivery_days' => $rate->getEstimatedDays(),
], $dhlRates);
}
}
The adapter pattern lets Magento support dozens of payment gateways and shipping carriers without modifying core code.
Quiz
1. What is the primary purpose of the Adapter pattern?
2. Object Adapter uses which mechanism?
3. In Magento, payment gateway adapters:
Flashcards
Question
What does the Adapter pattern do?
Click to reveal answer
Answer
Converts one interface into another that clients expect
Question
Object Adapter vs Class Adapter?
Click to reveal answer
Answer
Object: composition (wraps adaptee). Class: inheritance (extends adaptee).
Question
Why does Magento use adapters?
Click to reveal answer
Answer
To integrate external APIs (payment gateways, shipping carriers) with a consistent interface
Revision Notes
Key Takeaways
- 1. Adapter converts incompatible interfaces to work together
- 2. Object Adapter (composition) is preferred in PHP over Class Adapter (inheritance)
- 3. Magento uses adapters for payment gateways and shipping carriers
- 4. Adapters translate both request and response formats
- 5. Adding a new gateway = creating a new adapter class
Interview Tips
- • Give a concrete example: StripeAdapter converts PaymentGatewayInterface to StripeSdk
- • Explain the two-way translation: our format → their API, their response → our format
- • Distinguish Adapter from Decorator (Adapter converts, Decorator adds behavior)
Cheat Sheet
Adapter Pattern:
Problem: Incompatible interfaces (external SDK vs your API)
Solution: Wrapper class that translates between them
Object Adapter: class StripeAdapter implements OurInterface { wraps StripeSdk }
Class Adapter: class StripeAdapter extends StripeSdk implements OurInterface
Magento:
Payment: StripeAdapter, PayPalAdapter, BraintreeAdapter
Shipping: DhlAdapter, UpsAdapter, FedexAdapter
Adapter vs Decorator:
Adapter → converts interface
Decorator → adds behavior (same interface)