iT邦幫忙

2026 iThome 鐵人賽

DAY 17
0
Software Development

一套真實運作中的 Laravel 系統,拆解它的原生機制系列 第 17

Day 17:Service Container 實戰——SOAP Client 怎麼用 Factory 綁定

  • 分享至 

  • xImage
  •  

前言:bindsingleton,這個系統怎麼選?

「Service Container 的 bindsingleton 差在哪,官方文件講得很清楚,但實際綁一個外部系統的 Client 時,該選哪一個?」

今天開始這個系列的第三部:跟外部系統整合。這個系統要跟一個不受自己控制的外部系統(走 SOAP 協議)交換資料,第一步不是「怎麼呼叫」,是「這個 Client 該怎麼綁進容器」。今天先看容器綁定這一層,明後兩天再深入實際怎麼跟外部系統對接、怎麼補上逾時防護。

今日目標

  • 看一個真實案例:外部系統的 SOAP Client 怎麼透過 Factory 綁進 Service Container
  • 理解 bindsingleton 在這個案例裡各自被用在哪裡、為什麼
  • 認識同一條呼叫鏈路上疊了兩層逾時設定的設計
  • 建立「容器綁定不是隨便選 bind 或 singleton,背後要有理由」的判斷習慣

本文主體

真實綁定程式碼長什麼樣

AppServiceProvider::register() 裡,這個系統這樣綁定跟外部系統溝通用的物件:

$this->app->bind(Psr18Transport::class, function () {
    return Psr18Transport::createForClient(new Client([
        RequestOptions::CONNECT_TIMEOUT => 5,
        RequestOptions::TIMEOUT => 15,
    ]));
});

$this->app->singleton(DataGatewayClient::class, function () {
    return DataGatewayClientFactory::factory(
        app(Psr18Transport::class),
        config('services.外部系統.soap.data-gateway')
    );
});

這裡有兩個綁定,用了不同的生命週期:Psr18Transportbind(每次要新的實例就重新建立),SOAP Client 本身用 singleton(整個 request 生命週期只建立一次)。這不是隨便選的,兩者各自有清楚的理由。

為什麼 Transport 用 bind,Client 用 singleton

Psr18Transport 是負責實際發送 HTTP 請求的傳輸層物件,bind 代表每次從容器要它,都拿到一個全新的實例——如果這個系統之後需要對不同的外部端點分別建立連線(不同的逾時設定、不同的憑證),bind 讓每次取得的實例互不干擾,不會有共用狀態的疑慮。

SOAP Client(DataGatewayClient)用 singleton,理由是:這個物件建立過程本身有成本(要組出 SOAP engine、WSDL 解析等),而且同一個 request 裡通常只需要跟這個外部系統對話一次,用 singleton 確保整個 request 生命週期只建立一次,重複呼叫時直接複用已經建好的實例,不用每次都重新走一次組裝流程。

判斷依據可以歸納成一句話:物件本身有沒有需要「每次都是全新、互不干擾」的理由——有,用 bind;沒有,且建立成本不低,用 singleton

兩層逾時設定:同一條呼叫鏈路,設兩道防線

留意 Psr18Transport 綁定裡的 Guzzle Client 設定:

new Client([
    RequestOptions::CONNECT_TIMEOUT => 5,
    RequestOptions::TIMEOUT => 15,
]);

這裡設了兩個逾時:CONNECT_TIMEOUT(建立連線的逾時,5 秒)跟 TIMEOUT(整個請求完成的逾時,15 秒)。這是 HTTP 傳輸層的逾時保護——負責發送請求的 Guzzle Client 這一層先擋一次。

明天會看到,SOAP Client 自己的 Factory 裡,還會再設一次逾時(connection_timeout => 5)。同一條呼叫鏈路,從「發送 HTTP 請求」到「解析 SOAP 回應」,一共經過兩層逾時設定。這不是重複設計,是兩個不同層次各自負責自己的逾時邊界:Guzzle 這一層管的是底層 HTTP 傳輸,SOAP Client 這一層管的是它自己封裝的邏輯要不要提前放棄。哪一層先觸發逾時,取決於實際狀況,但兩層都設,代表這個系統對「跟外部系統對話可能卡住」這件事,做了雙重保護,而不是賭其中一層一定夠用。

用介面組裝,不寫死實作

DataGatewayClientFactory::factory() 接受的第一個參數是 Psr18Transport——這是一個介面/抽象層,不是寫死某個特定 HTTP client 的實作。這代表:如果哪天要幫這個 Client 換一個底層的 HTTP client(例如從 Guzzle 換成別的 PSR-18 相容實作),只要改容器裡 Psr18Transport 的綁定邏輯,DataGatewayClientFactory 本身完全不用動。這也是為什麼測試環境能夠輕鬆換成假的傳輸層——因為程式碼從一開始就是對著介面寫,不是對著某個具體類別寫。

對照:寫死實作 vs 透過容器綁定介面

在需要用到的地方直接 new 出具體實作

class SomeService
{
    public function fetch()
    {
        $client = new GuzzleHttp\Client(['timeout' => 15]);
        // 這裡的呼叫端永遠綁死 Guzzle,測試時無法替換
    }
}

透過容器綁定介面,呼叫端只依賴抽象

$this->app->bind(Psr18Transport::class, fn () => Psr18Transport::createForClient(
    new Client([RequestOptions::TIMEOUT => 15])
));

class SomeService
{
    public function __construct(private Psr18Transport $transport) {}
    // 呼叫端只知道有一個 Transport 可以用,不知道也不需要知道底層是 Guzzle
}

今日思考題

你的專案裡有沒有需要跟外部系統對話的物件?回想一下它是怎麼被建立出來的——是在需要用到的地方直接 new,還是透過容器綁定介面?如果要在測試裡替換掉真實的外部呼叫,現在的寫法容不容易做到?

今日重點回顧

  • Service Container 的 bindsingleton 選擇要有理由:需要「每次全新」用 bind,建立成本高且可以複用用 singleton
  • 這個系統的 SOAP Client 綁定:Transport 層用 bind,Client 本身用 singleton
  • 同一條呼叫鏈路疊了兩層逾時設定(Guzzle 傳輸層 + SOAP Client 層),是雙重保護,不是重複設計
  • 對著介面(Psr18Transport)組裝,而不是寫死具體實作,讓測試時可以輕鬆替換成假的傳輸層

明日預告

明天要具體看這個 SOAP Client 怎麼真的跟外部系統對接——SOAP 這種比較老派的協議,在 2026 年怎麼用 PSR-18 的方式包裝起來用。


上一篇
Day 16:Event/Listener——登入事件怎麼靠自動探索變成一筆審計紀錄
下一篇
Day 18:跟外部系統對接——SOAP 協議在 2026 年怎麼用 PSR-18 包起來用
系列文
一套真實運作中的 Laravel 系統,拆解它的原生機制20
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言