Skip to content
intermediate Phase 10 · More Design Patterns

Adapter Pattern

Adapter pattern for converting interfaces, with Magento payment gateway and shipping carrier adapter examples

45m
0 problems
Topic Progress 0%

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:

  1. PHP doesn't support multiple inheritance
  2. Composition is more flexible
  3. 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?

Question 1 options

2. Object Adapter uses which mechanism?

Question 2 options

3. In Magento, payment gateway adapters:

Question 3 options

Flashcards

Question

What does the Adapter pattern do?

Answer

Converts one interface into another that clients expect

Question

Object Adapter vs Class Adapter?

Answer

Object: composition (wraps adaptee). Class: inheritance (extends adaptee).

Question

Why does Magento use adapters?

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)