<? phpukraine PHP-ПЛАТФОРМА
⌕Пошук по платформі
СТРУКТУРНИЙ ПАТЕРН

АдаптерAdapter

Обгортка перекладає чужий інтерфейс на той, якого чекає ваш код. Ваш код не знає, з яким SDK насправді говорить.

Laravel Symfony складність 1 з 3 6 хв читання
ГЛІФ
01 · ПРОБЛЕМА

SDK платіжки розповзається по проєкту

Магазин приймає оплату через LiqPay, і його SDK викликають прямо там, де треба взяти гроші. Формат параметрів, гривні замість копійок і розбір відповіді повторюються в оплаті, поверненнях і підписках. Тепер бізнес хоче ще й monobank, у якого все інакше.

src/Payments/OrderPaymentService.php
1final class OrderPaymentService
2{
3 public function pay(Order $order): string
4 {
5 $liqpay = new LiqPay(config('services.liqpay.public'), config('services.liqpay.private'));
1Сервіс замовлень сам створює SDK і знає, де лежать його ключі.
6
7 $response = $liqpay->api('request', [
8 'action' => 'pay',
9 'version' => '3',
10 'amount' => $order->total / 100,
2Копійки в гривні перераховують тут, у поверненнях і в підписках. monobank чекає копійки, і в одному з місць це забудуть.
11 'currency' => 'UAH',
12 'order_id' => $order->number,
13 ]);
14
15 // RefundJob і SubscriptionRenewal роблять те саме своїми словами
16 return $this->parseLiqPayResponse($response);
3Формат відповіді LiqPay розбирають у коді магазину, а скоро поруч зʼявиться розбір відповіді monobank.
17 }
18}
02 · ІДЕЯ РІШЕННЯ

Свій інтерфейс і по адаптеру на кожен SDK

ДІАГРАМА · УСЯ СХЕМА
Кроки: 4
charge()api()createInvoice()Checkoutpay(Order)«interface»PaymentGatewaycharge(id, kopecks)адаптерLiqPayGatewayадаптерMonobankGateway{ }LiqPay SDK{ }monobank API
Checkoutpay(Order)«interface»PaymentGatewaycharge(id, kopecks)адаптерLiqPayGatewayадаптерMonobankGateway{ }LiqPay SDK{ }monobank API
Уся схема. Натисніть → або виберіть крок нижче, щоб пройти по ній частинами.
Абстракція Ключовий учасник Клієнт реалізує · - - ▷
  • Checkout використовує PaymentGateway (charge()).
  • LiqPayGateway реалізує PaymentGateway.
  • MonobankGateway реалізує PaymentGateway.
  • LiqPayGateway використовує LiqPay SDK (api()).
  • MonobankGateway використовує monobank API (createInvoice()).
03 · КОД

Запустіть і подивіться

PHP 8.5
1final class OrderPaymentService
2{
3 public function pay(Order $order): string
4 {
5 $liqpay = new LiqPay(config('services.liqpay.public'), config('services.liqpay.private'));
6
7 $response = $liqpay->api('request', [
8 'action' => 'pay',
9 'version' => '3',
10 'amount' => $order->total / 100,
11 'currency' => 'UAH',
12 'order_id' => $order->number,
13 ]);
14
15 // RefundJob і SubscriptionRenewal роблять те саме своїми словами
16 return $this->parseLiqPayResponse($response);
17 }
18}
1<?php
2
31interface PaymentGateway
4{
5 public function charge(string $orderId, int $kopecks): string;
6}
7
8// Спрощені SDK платіжних сервісів: їх код не наш, і змінювати його ми не можемо.
9final class LiqPayClient
10{
11 /** @param array<string, string|float> $params */
12 public function api(string $path, array $params): array
13 {
14 return ['status' => 'success', 'payment_id' => 'lp-' . $params['order_id']];
15 }
16}
17
18final class MonobankAcquiring
19{
20 public function createInvoice(int $amount, int $ccy, string $reference): object
21 {
22 return (object) ['invoiceId' => 'mono-' . $reference, 'amount' => $amount, 'ccy' => $ccy];
23 }
24}
25
26final readonly class LiqPayGateway implements PaymentGateway
27{
28 public function __construct(private LiqPayClient $client) {}
29
302 public function charge(string $orderId, int $kopecks): string
31 {
32 $response = $this->client->api('request', [
33 'action' => 'pay',
34 'amount' => $kopecks / 100,
35 'currency' => 'UAH',
36 'order_id' => $orderId,
37 ]);
38
39 return $response['payment_id'];
40 }
41}
42
43final readonly class MonobankGateway implements PaymentGateway
44{
453 private const int UAH = 980;
46
47 public function __construct(private MonobankAcquiring $acquiring) {}
48
49 public function charge(string $orderId, int $kopecks): string
50 {
51 return $this->acquiring->createInvoice($kopecks, self::UAH, $orderId)->invoiceId;
52 }
53}
54
554final readonly class Checkout
56{
57 public function __construct(private PaymentGateway $payments) {}
58
59 public function pay(string $orderId, int $kopecks): string
60 {
61 $paymentId = $this->payments->charge($orderId, $kopecks);
62
63 return sprintf('%s на %.2f грн, платіж %s', $orderId, $kopecks / 100, $paymentId);
64 }
65}
66
67$gateways = [new LiqPayGateway(new LiqPayClient()), new MonobankGateway(new MonobankAcquiring())];
68
69foreach ($gateways as $gateway) {
70 echo str_pad($gateway::class, 17), (new Checkout($gateway))->pay('UA-1042', 185000), "\n";
71}
Перший запуск завантажить рушій PHP 8.5, приблизно 3,8 МБ Відкрити в пісочниці ↗
ВИВІД
Завантажуємо рушій PHP 8.5
Лише перший раз. Далі код запускається миттєво, рушій лишається в кеші браузера.
$ php main.php ▍
  1. 1 рядки 3–6

    Інтерфейс написаний мовою магазину: номер замовлення й сума в копійках. Про LiqPay і monobank він не знає.

  2. 2 рядки 30–40

    Адаптер LiqPay перекладає копійки в гривні й збирає параметри так, як хоче SDK.

  3. 3 рядки 45–52

    Адаптер monobank перекладає ті самі дані інакше: копійки лишаються, валюта числовим кодом 980.

  4. 4 рядки 55–65

    Checkout працює з будь-яким адаптером і не знає, чий SDK під ним.

  5. Наведіть на пояснення, щоб підсвітити рядки.Торкніться пояснення, щоб підсвітити рядки.
04 · У ВАШОМУ ФРЕЙМВОРКУ

Ви вже цим користуєтесь

LARAVEL

Storage і Flysystem

Storage говорить з диском через Flysystem. Для локальної теки, S3 чи FTP є свій адаптер, який перекладає put() і get() на мову конкретного сховища.

// config/filesystems.php: 'driver' => 'local' або 's3'
Storage::disk('local')->put("invoices/{$number}.pdf", $pdf);
 
Storage::disk('s3')->put("invoices/{$number}.pdf", $pdf);
// той самий виклик, інший адаптер Flysystem

Ви вже користуєтесь цим, коли переносите файли з локального диска в S3 зміною конфігу, а не коду.

SYMFONY

Адаптери Cache

Компонент Cache має адаптери для Redis, файлів, APCu і бази. Код працює з CacheInterface, а яке сховище під ним, вирішує конфіг.

# config/packages/cache.yaml
framework:
    cache:
        app: cache.adapter.redis
        default_redis_provider: '%env(REDIS_URL)%'

Ви вже користуєтесь цим, коли міняєте cache.adapter.filesystem на cache.adapter.redis, і жоден сервіс цього не помічає.

✓ ЯК ЦЕ ТЕСТУВАТИ

Адаптер перевіряйте на межі: чи правильно він перекладає ваші дані в чужий формат. Решта коду в тестах отримує простий фейк PaymentGateway і не ходить у мережу.

it('sends kopecks and the hryvnia code to monobank', function () {
    $acquiring = new FakeMonobankAcquiring();
 
    (new MonobankGateway($acquiring))->charge('UA-1042', 185000);
 
    expect($acquiring->lastInvoice)
        ->toMatchArray(['amount' => 185000, 'ccy' => 980]);
});
05 · КОЛИ НЕ ТРЕБА

Один виклик в одному місці

Якщо SDK зручний, потрібен в одному місці й міняти постачальника ніхто не збирається, обгортка лише додає клас. Адаптер окупається, коли чужий API розходиться з мовою вашого коду або постачальників кілька. Свій власний код не адаптують, його просто змінюють.

// єдине SMS у проєкті: обгортка тут нічого не дає
$this->turboSms->send($order->phone, 'Замовлення відправлено');
06 · СУСІДИ

Схожі на вигляд, різні за змістом

07 · ПЕРЕВІР СЕБЕ

Питання зі співбесід

Усі питання з архітектури →

Формат чужого API живе в одному класі. Оновлення SDK чи новий постачальник змінюють адаптер, а не всі місця виклику, і в тестах замість мережі можна підставити фейк.

Адаптер підганяє чужий обʼєкт під інтерфейс, якого вже чекає клієнт. Фасад вводить новий, простіший інтерфейс до цілої підсистеми, і клієнт пишуть під нього.

PaymentGateway це порт: інтерфейс, який описує застосунок своєю мовою. LiqPayGateway і MonobankGateway це адаптери в інфраструктурі. Домен і сценарії залежать лише від порту, тому постачальника можна змінити, не чіпаючи їх.