Adapter and Facade are two structural patterns that manage complexity at the boundaries of a system. Adapter makes incompatible interfaces work together. Facade simplifies a complex subsystem behind a clean, high-level interface. Both appear constantly in real PHP projects – I show implementations and concrete examples from Magento 2.
Adapter – bridging incompatible interfaces
<?php
declare(strict_types=1);
// Your application expects this interface
interface PaymentGatewayInterface
{
public function charge(int $amountInCents, string $currency, string $token): string;
public function refund(string $transactionId, int $amountInCents): bool;
}
// Third-party SDK with a completely different interface
class StripeClient
{
public function createPaymentIntent(array $params): object { /* Stripe API */ return new \stdClass(); }
public function createRefund(array $params): object { /* Stripe API */ return new \stdClass(); }
}
class PaypalClient
{
public function executePayment(string $token, float $amount, string $currency): array { return []; }
public function issueRefund(string $saleId, float $amount): array { return []; }
}
// Adapter wraps Stripe and exposes your interface
class StripeAdapter implements PaymentGatewayInterface
{
public function __construct(private StripeClient $stripe) {}
public function charge(int $amountInCents, string $currency, string $token): string
{
$intent = $this->stripe->createPaymentIntent([
'amount' => $amountInCents,
'currency' => strtolower($currency),
'payment_method' => $token,
'confirm' => true,
]);
return $intent->id;
}
public function refund(string $transactionId, int $amountInCents): bool
{
$refund = $this->stripe->createRefund([
'payment_intent' => $transactionId,
'amount' => $amountInCents,
]);
return $refund->status === 'succeeded';
}
}
// Adapter wraps PayPal and exposes the same interface
class PayPalAdapter implements PaymentGatewayInterface
{
public function __construct(private PaypalClient $paypal) {}
public function charge(int $amountInCents, string $currency, string $token): string
{
$result = $this->paypal->executePayment(
$token,
$amountInCents / 100,
$currency
);
return $result['sale_id'] ?? '';
}
public function refund(string $transactionId, int $amountInCents): bool
{
$result = $this->paypal->issueRefund($transactionId, $amountInCents / 100);
return ($result['state'] ?? '') === 'completed';
}
}
// Service uses the interface - completely unaware of Stripe or PayPal specifics
class CheckoutService
{
public function __construct(private PaymentGatewayInterface $gateway) {}
public function pay(int $amount, string $currency, string $token): string
{
return $this->gateway->charge($amount, $currency, $token);
}
}
// Swap payment provider by changing the adapter
$service = new CheckoutService(new StripeAdapter(new StripeClient()));
Adapter in Magento 2 – logging
<?php
// Magento wraps Monolog through an Adapter
// Your code uses Psr\Log\LoggerInterface
// Internally Magento binds it to Magento\Framework\Logger\Monolog
// which adapts Monolog to the PSR-3 interface
class MyService
{
public function __construct(
private \Psr\Log\LoggerInterface $logger // interface, not Monolog
) {}
public function doWork(): void
{
$this->logger->info('Work done');
// Works whether the logger is Monolog, a test mock, or any PSR-3 logger
}
}
Facade – simplified interface to a subsystem
<?php
declare(strict_types=1);
// Complex subsystem - many classes with interrelated responsibilities
class ProductLoader { public function load(int $id): array { return []; } }
class PriceCalculator { public function calculate(array $product, int $customerId): float { return 0.0; } }
class StockChecker { public function isAvailable(string $sku): bool { return true; } }
class ImageResizer { public function resize(string $path, int $w, int $h): string { return ''; } }
class BreadcrumbBuilder { public function build(array $categories): array { return []; } }
// Facade provides a simple, task-oriented interface
class ProductPageFacade
{
public function __construct(
private ProductLoader $loader,
private PriceCalculator $priceCalc,
private StockChecker $stock,
private ImageResizer $images,
private BreadcrumbBuilder $breadcrumbs
) {}
// One method does everything the product page needs
public function getProductPageData(int $productId, int $customerId): array
{
$product = $this->loader->load($productId);
return [
'product' => $product,
'price' => $this->priceCalc->calculate($product, $customerId),
'in_stock' => $this->stock->isAvailable($product['sku']),
'image_url' => $this->images->resize($product['image'], 800, 800),
'breadcrumbs' => $this->breadcrumbs->build($product['category_ids']),
];
}
}
// Controller becomes trivially simple
class ProductController
{
public function show(int $id, int $customerId): array
{
return $this->facade->getProductPageData($id, $customerId);
}
}
Adapter vs Facade – key difference
| Aspect | Adapter | Facade |
|---|---|---|
| Problem solved | Incompatible interfaces | Overly complex subsystem |
| Interface count | Two existing interfaces, one adapter | Many classes, one facade |
| Changes existing code? | No – wraps without modifying | No – adds a layer above |
| Hides complexity? | No – translates it | Yes – simplifies it |
| Magento 2 example | Monolog -> PSR-3 Logger | Magento\Catalog\Helper\Data |
Summary
Adapter is the go-to pattern when integrating third-party libraries – wrap them in an interface you own so you can swap implementations without changing application code. Facade is the go-to pattern when a subsystem has grown complex – one well-named method that does “what the caller needs” is worth more than exposing five internal services. Both patterns are about managing coupling at the right level.
