iT邦幫忙

2023 iThome 鐵人賽

DAY 7
0
Modern Web

30 天上手! PHP 微服務入門與開發系列 第 7

第七章、Anser-Service:服務溝通的正確與錯誤處理 - PHP 微服務入門與開發

  • 分享至 

  • xImage
  •  

在這個章節,我們會使用到 HttpbinOrg 、 User Service 與 Main App,請參考第四章節所提到的內容建立你的本地開發環境。

延續第四章與第五章,你可以將專案內的 init.php 調整成下述程式碼,將 User Service 也宣告進來:

<?php

require_once './vendor/autoload.php';

use SDPMlab\Anser\Service\ServiceList;

ServiceList::addLocalService(
    name: "HttpbinOrg",
    address: "httpbin.org",
    port: 443,
    isHttps: true
);

ServiceList::addLocalService(
    name: "ProductionService",
    address: "production-service",
    port: 8080,
    isHttps: false
);

ServiceList::addLocalService(
    name: "UserService",
    address: "user-service",
    port: 8080,
    isHttps: false
);

使 Action 符合 API 端點需要

在現實世界的開發環境中,我們無法強制規定所有的微服務都遵循相同的介面與格式。因此,當使用 Anser Action 進行開發時,開發人員可能會遇到需要根據不同的微服務特性進行特定調整的情況。

首先,每個微服務可能有其特定的授權方式、標頭資料或其他特定的需要。例如,一些微服務可能需要某種驗證標頭(Authentication Header),而某些微服務則可能需要傳遞額外的參數。因為 Anser Action 是基於 Guzzle 7 開發的,所以開發人員可以直接利用 Guzzle 提供的功能來應對這些差異。

例如,如果一個微服務需要 Bearer Token 認證,開發者可以輕鬆地利用 Guzzle 的 headers 選項來添加:

$action = (new Action(
    serviceName: "SomeService",
    method: "GET",
    path: "/endpoint"
))->setOptions([
    'headers' => [
        'Authorization' => 'Bearer YOUR_TOKEN'
    ]
]);

當遇到需要自訂的請求時不妨先查看 Guzzle 7 的官方文件。這裡詳細介紹了如何設定各種請求選項,使你能夠建構你的 Request Options。

使用不同的 HTTP 動詞

<?php

require_once './init.php';

use SDPMlab\Anser\Service\Action;
use Psr\Http\Message\ResponseInterface;

$doneHandler = static function(
    ResponseInterface $response,
    Action $runtimeAction
){
    $body = $response->getBody()->getContents();
    $data = json_decode($body, true);
    $runtimeAction->setMeaningData($data);
};

$getAction = (new Action(
    serviceName: "HttpbinOrg",
    method: "GET",
    path: "/get"
))->doneHandler($doneHandler);

$postAction = (new Action(
    serviceName: "HttpbinOrg",
    method: "POST",
    path: "/post"
))->doneHandler($doneHandler);

$putAction = (new Action(
    serviceName: "HttpbinOrg",
    method: "PUT",
    path: "/put"
))->doneHandler($doneHandler);

$deleteAction = (new Action(
    serviceName: "HttpbinOrg",
    method: "delete",
    path: "/delete"
))->doneHandler($doneHandler);

$patchAction = (new Action(
    serviceName: "HttpbinOrg",
    method: "patch",
    path: "/patch"
))->doneHandler($doneHandler);

$response = [
    "get" => $getAction->do()->getMeaningData(),
    "post" => $postAction->do()->getMeaningData(),
    "put" => $putAction->do()->getMeaningData(),
    "delete" => $deleteAction->do()->getMeaningData(),
    "patch" => $patchAction->do()->getMeaningData(),
];

header("Content-Type: application/json");
echo json_encode($response) . PHP_EOL;

在上述範例中,我們創建了五個不同的 Action 實體,分別對應於五個不同的 HTTP 方法:GET、POST、PUT、DELETE 和 PATCH。每個 Action 實體都設定了相同的完成處理器。我們只需調整建構 Action 時的 method 參數,即可使用不同的 HTTP 動詞對目標端點發出請求。

調整 Request Option

你可以試著建立起下述 PHP 程式碼,並參考 Guzzle 7 的 Body 說明Headers 說明 :

<?php

require_once './init.php';

use SDPMlab\Anser\Service\Action;
use Psr\Http\Message\ResponseInterface;

$doneHandler = static function(
    ResponseInterface $response,
    Action $runtimeAction
){
    $body = $response->getBody()->getContents();
    $data = json_decode($body, true);
    $runtimeAction->setMeaningData($data);
};

$postBodyAction = (new Action(
    serviceName: "HttpbinOrg",
    method: "POST",
    path: "/post"
))->setOptions([
    "headers" => [
        "Content-Type" => "application/json"
    ],
    "body" => json_encode([
        "name" => "anser",
        "version" => "1.0.0"
    ])
])->doneHandler($doneHandler);

// 下述程式碼與上述程式碼相同
// $postBodyAction = (new Action(
//     serviceName: "HttpbinOrg",
//     method: "POST",
//     path: "/post"
// ))->doneHandler($doneHandler);
// $postBodyAction->addOption(
//     optionName: "headers",
//     value: [
//         "Content-Type" => "application/json"
//     ]
// );
// $postBodyAction->addOption(
//     optionName: "body",
//     value: json_encode([
//         "name" => "anser",
//         "version" => "1.0.0"
//     ])
// );

$response = [
    "postBody" => $postBodyAction->do()->getMeaningData(),
];

header("Content-Type: application/json");
echo json_encode($response) . PHP_EOL;

這部分的程式碼展示了兩種不同的方式來建立和設定 Action 的 Request Options。

第一種方式採用鏈式方法透過 setOptions 進行設定;而第二種方法則使用 addOption 進行設定。

  1. setOptions(array $options): ActionInterface

    設定 Action 在執行請求時使用的選項。此方法將會覆蓋所有現有的 Options 設定。

    • 參數:
      $options: 一個關聯陣列,其中的選項規則與 Guzzle7 的 Option 一致。
  2. addOption(string $optionName, $value): ActionInterface

    新增一個選項設定。如果已經存在相同名稱的設定,則會被新值覆蓋。

    • 參數:
      • $optionName: 選項名稱。
      • $value: 設定值,型別是多樣化的,請參考 Guzzle7 的說明,每種選項會有不同的 Value 型別。

關於 Request Options ,Action 還提供了下列 Method:

  1. removeOption(string $optionName): ActionInterface

    刪除現有的選項設定。

    • 參數:
      • $optionName: 需要被刪除的選項名稱。
  2. getOptions(): array
    回傳 Action 上所有的 Request Options。
  3. getOption(string $optionName): mixed

    回傳指定選項名稱的數值。如果選項不存在,則回傳 null。

    • 參數:
      • $optionName: 需要獲取值的選項名稱。
    • 回傳: 回傳 Option Value,在 Option Name 不存在時回傳 null。

上述方法主要提供了設定和獲取 HTTP 請求選項的功能,其選項的規則與 Guzzle7 的 Option 一致。開發者可以輕鬆地自訂所需的 HTTP 請求行為。

使用處理器(Handler)處理連線響應

在設計微服務或API串接時,我們經常需要對不同的響應結果進行特定的處理。因為我們無法規定所有的服務端點使用我們期待的方式響應結果,這裡的「處理器」就是為這個目的而設計的功能,它允許我們定義特定的匿名函式,來對響應或錯誤進行處理。

透過使用 Callback 模式,你可以將任務的執行和結果的處理分開,這使得程式碼更加模組化和可重用。不同的 Action 可以使用相同的處理器,或者根據需要輕鬆更換所需的處理器。

完成處理器(Done Handler)

當 HTTP 連線成功時,開發者能夠在回呼函式中取得 HTTP 回應實體以及執行期間 Action 實體。透過解析 HTTP 響應後,將解析成功的內容以 setMeaningData() 儲存至 Action 中,方便在之後的程式邏輯中取用。

我們以 User Service 的登入 API 作為範例:

<?php

require_once './init.php';

use SDPMlab\Anser\Service\Action;
use Psr\Http\Message\ResponseInterface;

$email = $_POST["email"] ?? "";
$password = $_POST["password"] ?? "";

$action = (new Action(
    serviceName: "UserService",
    method: "POST",
    path: "/api/v1/user/login"
))->setOptions([
    "headers" => [
        "Content-Type" => "application/json"
    ],
    "body" => json_encode([
        "email" => $email,
        "password" => $password
    ])
])->doneHandler(static function(
    ResponseInterface $response,
    Action $runtimeAction
){
    $body = $response->getBody()->getContents();
    $data = json_decode($body, true);
    $runtimeAction->setMeaningData($data['token']);
});

$token = $action->do()->getMeaningData();

header("Content-Type: application/json");
echo json_encode([
    "token" => $token
]) . PHP_EOL;
  1. 從 POST 請求中取得使用者輸入的 email 和 password。
  2. 建立一個新的 Action 用於對 UserService 進行 POST 請求。
  3. 完成處理器 doneHandler ,這個回呼函式將在 HTTP 請求成功時會被調用。你能夠依據你的 API 響應結構自訂解析方式,這裡則是將 token 儲存至 Action 實體之中。
    值得注意的是,doneHandler 在被執行時將會傳入基於 PSR-7 Response 介面的響應物件。你可以參考 PSR-7 的文件了解這個物件提供哪些方法幫助你解析 HTTP 回應。
  4. 使用 do() 方法執行 Action,然後使用 getMeaningData() 取得我們在 doneHandler 中儲存的 token。

你可以透過 Postman 執行這個範例:

錯誤處理器(Fail Handler)

當執行 HTTP 請求時可能會遇到各種問題,例如:伺服器無法連線、服務故障、認證失敗,或是響應的結果包含了一個錯誤的狀態碼(HTTP Status Code)。為了處理這些可能的問題,開發者應該依照 API 端點的需要,設定符合需求的錯誤處理器,以便在這些情況下執行特定的動作或記錄錯誤資訊。

使用上一個範例做個實驗,隨意鍵入密碼故意出錯:

程式出錯了!在預設的情況下,Action 會在碰到除了 HTTP 200 以外的響應拋出異常。在這個範例中我們所連結的原始 API 在驗證帳號密碼不通過時將會響應 401 狀態碼,此時就會觸發異常的拋出。

這時就可以透過 Fail Handler 處理這個錯誤了,讓我們來改寫一下上一個範例吧?

<?php

require_once './init.php';

use SDPMlab\Anser\Service\Action;
use Psr\Http\Message\ResponseInterface;
use SDPMlab\Anser\Exception\ActionException;

$email = $_POST["email"] ?? "";
$password = $_POST["password"] ?? "";

$action = (new Action(
    serviceName: "UserService",
    method: "POST",
    path: "/api/v1/user/login"
))->setOptions([
    "headers" => [
        "Content-Type" => "application/json"
    ],
    "body" => json_encode([
        "email" => $email,
        "password" => $password
    ])
])->doneHandler(static function(
    ResponseInterface $response,
    Action $runtimeAction
){
    $body = $response->getBody()->getContents();
    $data = json_decode($body, true);
    $runtimeAction->setMeaningData($data['token']);
})->failHandler(function (
    ActionException $e
) {
    if($e->isClientError()){
        $msg = $e->getAction()->getResponse()->getBody()->getContents();
        $error = json_decode($msg, true)['error'];
        $e->getAction()->setMeaningData([
            "code" => $e->getAction()->getResponse()->getStatusCode(),
            "msg" => $error
        ]);
    }else if ($e->isServerError()){
        $e->getAction()->setMeaningData([
            "code" => 500,
            "msg" => "server error"
        ]);
    }else if($e->isConnectError()){
        $e->getAction()->setMeaningData([
            "code" => 500,
            "msg" => $e->getMessage()
        ]);
    }
});

header("Content-Type: application/json");
if($action->do()->isSuccess()){
    echo json_encode([
        "token" => $action->getMeaningData()
    ]) . PHP_EOL;
}

$msg = $action->getMeaningData();
http_response_code($msg['code']);
echo json_encode($msg) . PHP_EOL;

依據上述修改後的程式碼,你應該也能觀察到一些特別的地方:

  1. 每一個錯誤分類都有其特定的意義和處理方式,開發者能夠對不同的錯誤採取適當的相應的處理:
    • isServerError:代表 HTTP 500 Status 系列錯誤,通常是因為伺服器過載、伺服器停止服務或是伺服器發生未知錯誤觸發。
    • isClientError:代表 HTTP 400 Status 系列錯誤,通常為找不到資源、驗證失敗或是不符合業務邏輯時觸發相應錯誤。
    • isConnectError:代表伺服器目前處於離線狀態,或是連線的過程中網路不穩定而引發的錯誤。

Fail Handler 提供了一個彈性的方式讓你自訂錯誤時的行為。無論是提供使用者一個友善的錯誤提示,還是將詳細的錯誤訊息記錄到日誌中,都可以輕鬆實現。透過 doneHandler 和 failHandler 的分離,讓成功和失敗的邏輯獨立開來,使得程式碼更為結構化且易於維護。

再重新以 Postman 執行上述程式碼,並鍵入錯誤的帳號密碼,你應該能看見我們所預期的錯誤響應:

你也可以進入 init.php 故意將 User Service 的設定搞亂,比如輸入一個不存在連接埠,接著再執行一次請求:

此時,所有的異常都在我們的掌控之中了。你能夠依照你的需求處理最終響應至客戶端的結果,也能在出現特別錯誤的場合中將重要的錯誤資訊給紀錄下來。

結語

在 API 請求方面,雖然我們無法預見所有微服務的具體需求,但通過結合 Anser Action 和 Guzzle 7,開發者可以靈活應對各種情況,確保 API 請求總是符合目標微服務的期望。

透過處理器(Handler)的概念,我們能夠將微服務或 API 的連線響應以更結構化、模組化的方式進行處理。上述方式不僅使我們能夠簡化程式碼與提高可讀性,還能確保在各種不同的情境下,精確地對連線響應或錯誤給予適當的處理。

在微服務架構或API串接中,無法避免會遇到各式各樣的錯誤。然而,如果我們能提前預期這些錯誤,並用策略性的方式來處理它們,那麼我們就能確保系統的穩定性和提供使用者更好的體驗。


上一篇
第六章、Anser-Service:並行處理連線請求 - PHP 微服務入門與開發
下一篇
第八章、Anser-Service:服務重試與過濾器 - PHP 微服務入門與開發
系列文
30 天上手! PHP 微服務入門與開發30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言