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

ФасадFacade

Один простий вхід до складної підсистеми. Клієнт викликає один метод, а фасад сам знає, яких сервісів і в якому порядку торкнутися.

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

Контролер, який знає всю доставку

Щоб відправити замовлення, магазин резервує товар на складі, створює накладну Нової пошти й пише покупцю лист з номером ТТН. Усі три кроки живуть у контролері, а Telegram-бот для менеджерів і адмінка повторюють їх своїми словами. Порядок кроків і формат накладної тепер треба памʼятати в трьох місцях.

app/Http/Controllers/OrderShipmentController.php
1final class OrderShipmentController
2{
3 public function store(Order $order): RedirectResponse
4 {
5 $this->warehouse->reserve($order->items, $order->warehouseId);
1Склад резервують першим, бо без товару накладна не потрібна. У Telegram-боті цей рядок поставили після накладної.
6
7 $waybill = $this->novaPoshta->createWaybill([
2Контролер знає поля API Нової пошти, хоча його справа прийняти HTTP-запит і відповісти.
8 'CityRecipient' => $order->cityRef,
9 'RecipientAddress' => $order->branchRef,
10 'Weight' => $order->weight,
11 'Cost' => $order->total / 100,
12 'PaymentMethod' => 'Cash',
13 ]);
14
15 $order->update(['ttn' => $waybill->number, 'status' => 'shipped']);
16 $this->mailer->send(new ShippedMail($order, $waybill->number));
3Лист з номером ТТН забули додати в адмінку, і покупці питають, де посилка.
17
18 return back();
19 }
20}
02 · ІДЕЯ РІШЕННЯ

Один метод замість трьох підсистем

ДІАГРАМА · УСЯ СХЕМА
Кроки: 4
ПІДСИСТЕМА ДОСТАВКИship()ship()Controllerstore(Order)фасадShippingship(Order)TelegramBotWarehousereserve()NovaPoshtaClientcreateWaybill()Mailersend()
ПІДСИСТЕМА ДОСТАВКИControllerstore(Order)фасадShippingship(Order)TelegramBotWarehousereserve()NovaPoshtaClientcreateWaybill()Mailersend()
Уся схема. Натисніть → або виберіть крок нижче, щоб пройти по ній частинами.
Абстракція Ключовий учасник Клієнт реалізує · - - ▷
  • Підсистема доставки містить: Warehouse, NovaPoshtaClient, Mailer.
  • Controller використовує Shipping (ship()).
  • TelegramBot використовує Shipping (ship()).
  • Shipping використовує Warehouse.
  • Shipping використовує NovaPoshtaClient.
  • Shipping використовує Mailer.
03 · КОД

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

PHP 8.5
1final class OrderShipmentController
2{
3 public function store(Order $order): RedirectResponse
4 {
5 $this->warehouse->reserve($order->items, $order->warehouseId);
6
7 $waybill = $this->novaPoshta->createWaybill([
8 'CityRecipient' => $order->cityRef,
9 'RecipientAddress' => $order->branchRef,
10 'Weight' => $order->weight,
11 'Cost' => $order->total / 100,
12 'PaymentMethod' => 'Cash',
13 ]);
14
15 $order->update(['ttn' => $waybill->number, 'status' => 'shipped']);
16 $this->mailer->send(new ShippedMail($order, $waybill->number));
17
18 return back();
19 }
20}
1<?php
2
3final readonly class Order
4{
5 public function __construct(public string $number, public string $email, public string $branch, public int $total) {}
6}
7
81// Підсистема: три незалежні сервіси, кожен зі своїм API.
9final class Warehouse
10{
11 public function reserve(string $order): void
12 {
13 echo " склад: товар для {$order} відкладено\n";
14 }
15}
16
17final class NovaPoshtaClient
18{
19 /** @param array<string, string|int|float> $params */
20 public function createWaybill(array $params): string
21 {
22 echo " Нова пошта: накладна на {$params['RecipientAddress']}, оголошена вартість {$params['Cost']} грн\n";
23
24 return '2045' . str_pad((string) crc32((string) $params['RecipientAddress']), 10, '0', STR_PAD_LEFT);
25 }
26}
27
28final class Mailer
29{
30 public function send(string $to, string $text): void
31 {
32 echo " лист на {$to}: {$text}\n";
33 }
34}
35
362final readonly class Shipping
37{
38 public function __construct(
39 private Warehouse $warehouse,
40 private NovaPoshtaClient $novaPoshta,
41 private Mailer $mailer,
42 ) {}
43
443 public function ship(Order $order): string
45 {
46 $this->warehouse->reserve($order->number);
47
48 $ttn = $this->novaPoshta->createWaybill([
49 'RecipientAddress' => $order->branch,
50 'Cost' => $order->total / 100,
51 ]);
52
53 $this->mailer->send($order->email, "замовлення {$order->number} їде, ТТН {$ttn}");
54
55 return $ttn;
56 }
57}
58
594$shipping = new Shipping(new Warehouse(), new NovaPoshtaClient(), new Mailer());
60
61echo "Контролер:\n";
62$shipping->ship(new Order('UA-1042', 'olena@example.com', 'Київ, відділення 25', 185000));
63
64echo "Telegram-бот:\n";
65$shipping->ship(new Order('UA-1043', 'taras@example.com', 'Львів, відділення 3', 42000));
Перший запуск завантажить рушій PHP 8.5, приблизно 3,8 МБ Відкрити в пісочниці ↗
ВИВІД
Завантажуємо рушій PHP 8.5
Лише перший раз. Далі код запускається миттєво, рушій лишається в кеші браузера.
$ php main.php ▍
  1. 1 рядки 8–34

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

  2. 2 рядки 36–42

    Фасад отримує всі частини підсистеми в конструкторі. Клієнтам вони більше не потрібні.

  3. 3 рядки 44–56

    Порядок кроків і переклад даних замовлення на поля Нової пошти записані в одному методі.

  4. 4 рядки 59–65

    Контролер і бот роблять один виклик ship() і отримують однаковий результат.

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

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

LARAVEL

Фасади Cache::, DB::, Mail::

У Laravel фасадом називають статичний проксі до сервісу з контейнера. Facade::__callStatic() бере обʼєкт за ключем з getFacadeAccessor() і передає йому виклик. Так скорочують доступ до одного сервісу, підсистеми за ним немає. Фасад GoF у Laravel це звичайний клас на кшталт Shipping, який ви інжектуєте.

class Cache extends Facade
{
    protected static function getFacadeAccessor()
    {
        return 'cache';
    }
}
 
Cache::get('rates'); // те саме, що app('cache')->get('rates')

Ви вже користуєтесь цим, коли пишете Cache::get() чи Mail::to(), а в тесті підмінюєте сервіс через Mail::fake() або Cache::shouldReceive().

SYMFONY

Фасад як звичайний сервіс

Статичних фасадів у Symfony немає. Фасад GoF тут це сервіс, який отримує частини підсистеми через автовайринг, а контролер приймає його аргументом дії.

#[Route('/orders/{id}/ship', methods: ['POST'])]
public function ship(Order $order, Shipping $shipping): Response
{
    $shipping->ship($order);
 
    return $this->redirectToRoute('order_show', ['id' => $order->getId()]);
}

Ви вже користуєтесь цим, коли додаєте клас у src/, і контейнер сам збирає його залежності.

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

Фасад тестуйте з простими фейками складу, Нової пошти й пошти: так видно порядок кроків. Код, що викликає фасад, отримує один фейк Shipping і не знає про підсистему взагалі.

it('does not create a waybill when stock is empty', function () {
    $novaPoshta = new FakeNovaPoshta();
    $shipping = new Shipping(new EmptyWarehouse(), $novaPoshta, new FakeMailer());
 
    expect(fn () => $shipping->ship(order()))->toThrow(OutOfStock::class)
        ->and($novaPoshta->waybills)->toBe([]);
});
05 · КОЛИ НЕ ТРЕБА

Фасад над одним класом

Якщо за фасадом стоїть один сервіс і фасад просто переказує його методи, він нічого не ховає. Фасад окупається, коли клієнту треба кілька підсистем у певному порядку.

// обгортка, яка лише повторює NovaPoshtaClient
public function track(string $ttn): Status
{
    return $this->novaPoshta->track($ttn);
}
06 · СУСІДИ

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

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

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

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

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

Ні. Це статичний проксі: Facade::__callStatic() дістає з контейнера сервіс cache і передає йому виклик. Він скорочує доступ до одного обʼєкта й нічого не обʼєднує. Фасад GoF ви пишете самі звичайним класом.

Туди дописують усе, що стосується доставки: трекінг, повернення, тарифи, і клас росте без меж. Фасад має обслуговувати один сценарій або вузьку групу. Коли сценаріїв багато, розбивайте на кілька фасадів чи сервісів застосунку.