iT邦幫忙

2023 iThome 鐵人賽

DAY 9
0
Modern Web

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

第九章、Anser-Service:服務抽象化 - PHP 微服務入門與開發

  • 分享至 

  • xImage
  •  

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

Simple Service

在先前的章節中,我們學習到了如何撰寫一個個單獨的 Action 定義與服務溝通的最小單元。但在一個大型的專案或微服務架構中,僅有 Action 是遠遠不夠的。當我們需要在多個 Action 之間共享一些組態設定、邏輯或行為時,將會需要一個更高層次的抽象結構,並且有組織地統合具有內聚性的 Action。

而整個 Anser-Service 子元件就是專為此目的而設計的,我們提供了 SimpleService 抽象類別給予開發人員進行實作,期望將具有特定相似功能的 Actions 抽象化為「服務」,這不僅使得邏輯更加清晰,還有助於模組化開發。

馬上來看看實作方式吧!

1. UserService

首先,建立起一個 UserService.php ,並鍵入以下內容:

<?php
namespace Services;

require_once './FailHandlerFilter.php';
require_once './JsonDoneHandlerFilter.php';

use SDPMlab\Anser\Service\SimpleService;
use SDPMlab\Anser\Service\ActionInterface;
use Filters\FailHandlerFilter;
use Filters\JsonDoneHandlerFilter;

class UserService extends SimpleService
{
    protected $serviceName = "UserService";
    protected $filters = [
        "before" => [
            JsonDoneHandlerFilter::class,
            FailHandlerFilter::class
        ],
        "after" => [],
    ];
    protected $retry = 1;
    protected $retryDelay = 0.2;
    protected $timeout = 3.0;
    protected $options = [];

    /**
     * 使用者登入
     *
     * @param string $email 使用者信箱
     * @param string $password 使用者密碼
     * @return ActionInterface
     */
    public function userLoginAction(string $email, string $password): ActionInterface
    {
        return $this->getAction(
            method: "POST",
            path: "/api/v1/user/login"
        )->setOptions([
            "headers" => [
                "Content-Type" => "application/json"
            ],
            "body" => json_encode([
                "email" => $email,
                "password" => $password
            ])
        ]);
    }

    /**
     * 取得使用者資訊
     *
     * @param string $apiKey
     * @return ActionInterface
     */
    public function userInfoAction(string $apiKey): ActionInterface
    {
        return $this->getAction(
            method: "GET",
            path: "/api/v1/user"
        )->setOptions([
            "headers" => [
                "Authorization" => $apiKey
            ]
        ]);
    }

    /**
     * 取得使用者錢包資訊
     *
     * @param integer $userId
     * @return ActionInterface
     */
    public function walletAction(int $userId): ActionInterface
    {
        return $this->getAction(
            method: "GET",
            path: "/api/v1/wallet"
        )->setOptions([
            "headers" => [
                "X-User-Key" => $userId
            ]
        ]);
    }

}

UserService 繼承自 SimpleService,而 SimpleService 將會提供管理 Actions 的相關功能。

  • $serviceName:這個成員變數將成為 UserService 中所有 Action 的基礎,所有的 Action 都會以這個變數的內容作為微服務端點的連線資訊。
  • $filters:呼叫服務前後要執行的過濾器。範例中,我們在呼叫服務之前設定了 JsonDoneHandlerFilterFailHandlerFilter
  • $retry:與微服務溝通時若發生失敗時的重試次數。
  • $retryDelay:當連線失敗必須重試時得等待多久才能發出連線請求。
  • $timeout:微服務的處理時長大於多久後自動中斷連線,也代表當次的請求為失敗請求。
  • 其餘的公開方法為提供了圍繞著 User Service 提供的可用介面,並透過設定請求方法、路徑以及其他選項來與後端服務互動。

在示範的 UserService 類別中,我們以公開方法映射了微服務端點所提供的資源,而透過 SimpleService 的幫助,在進行 Http 請求時的前濾器、後濾器,與請求失敗時的重試次數、間隔,以及超時 皆能整合在一個類別之中統一設定。

筆者在設計 SimpleService 類別時,便希望限定開發者僅能使用成員方法 getAction 來實作微服務 API 連線細節。在執行 getAction 的過程中,會套用開發人員透過成員變數所定義的基礎設定再進行微服務連線,這意味著在相同類別下的所有連線能擁有一致的組態設定。

透過繼承這個基本的類別,開發者僅須撰寫公開方法,並在方法中對微服務的端點資源做詳細設定,這些設定包含端點位置、 請求方法、Request Options,與回應解析方式等等。

2. 過濾器與處理器

接著是在 UserServuce 中提到的 JsonDoneHandlerFilter.php

<?php
namespace Filters;

use SDPMlab\Anser\Service\FilterInterface;
use SDPMlab\Anser\Service\ActionInterface;
use \Psr\Http\Message\ResponseInterface;

class JsonDoneHandlerFilter implements FilterInterface
{
    public function beforeCallService(ActionInterface $action)
    {
        $action->doneHandler(static function (
            ResponseInterface $response,
            ActionInterface $action
        ) {
            $body = $response->getBody()->getContents();
            $data = json_decode($body, true);
            $data['code'] = $response->getStatusCode();
            $action->setMeaningData($data);
        });
    }

    public function afterCallService(ActionInterface $action)
    {
        //do nothing
    }
}

這個過濾器專門用於處理成功的 JSON 響應,當 Action 成功時,會將 HTTP 響應內容解析成 JSON 格式並存在 Action 之中。也因為 User Service 所提供的響應皆採用 Json 格式,因此我們將能夠以一個 Handler 完成所有 Action 的 doneHandler() 設定。

我們再來看看 FailHandlerFilter.php

<?php
namespace Filters;

use SDPMlab\Anser\Service\FilterInterface;
use SDPMlab\Anser\Service\ActionInterface;
use SDPMlab\Anser\Exception\ActionException;

class FailHandlerFilter implements FilterInterface
{
    public function beforeCallService(ActionInterface $action)
    {
        $action->failHandler(static function (
            ActionException $e
        ) {
            if($e->isClientError()){
                $msg = $e->getAction()->getResponse()->getBody()->getContents();
                $error = json_decode($msg, true)['error'] ?? "unknow error";
                $e->getAction()->setMeaningData([
                    "code" => $e->getAction()->getResponse()->getStatusCode(),
                    "msg" => $error
                ]);
            }else if ($e->isServerError()){
                $serverBody = $e->getAction()->getResponse()->getBody()->getContents();
                file_put_contents("./log.txt", "[" . date("Y-m-d H:i:s") . "] " . $serverBody . PHP_EOL, FILE_APPEND);
                $e->getAction()->setMeaningData([
                    "code" => 500,
                    "msg" => "server error"
                ]);
            }else if($e->isConnectError()){
                file_put_contents("./log.txt", "[" . date("Y-m-d H:i:s") . "] " . $e->getMessage() . PHP_EOL, FILE_APPEND);
                $e->getAction()->setMeaningData([
                    "code" => 500,
                    "msg" => $e->getMessage()
                ]);
            }
        });
    }

    public function afterCallService(ActionInterface $action)
    {
        //do nothing
    }
}

這個過濾器旨在處理各種失敗的情況,範例中處理了三種主要的失敗情況:

  • 客戶端錯誤:例如輸入不正確、資料遺失等情況。
  • 伺服器錯誤:例如 500 的內部伺服器錯誤。在此,除了設定錯誤訊息外,還會將錯誤詳細內容寫入日誌。
  • 連接錯誤:例如無法連接到伺服器的情況,這也會被記錄到日誌。

在實際的系統開發中,你應該能在某個地方定義好通用的 failHandler() ,在大部分的情況下,伺服器錯誤與連接錯誤應該是能夠通用的區塊。

3. 主程式

最後是 userlogin.php

<?php

require_once './init.php';
require_once './UserService.php';

use Services\UserService;

$userService = new UserService();
$data = $userService->userLoginAction($_POST['email'], $_POST['password'])->do()->getMeaningData();
$info = $userService->userInfoAction($data['token'])->do()->getMeaningData();
$wallet = $userService->walletAction($info['data']['u_key'])->do()->getMeaningData();

header("Content-Type: application/json");
if ($data['code'] !== 200) {
    http_response_code($data['code']);
}
echo json_encode([
    "token"        => $data['token'],
    "userData"     => $info['data'],
    "walletInfo"   => $wallet['data']
]);

上述程式透過建立一個我們設計好的 UserService 類別實體,再接著逐步呼叫 userLoginActionuserInfoAction 以及 walletAction 等方法取得 Action 並展開連線,最後回傳組合的 JSON 結果。在資料的解析與使用流程中,我們先登入取得 Token ,再使用 Token 取得使用者資訊,最後透過使用者 ID 取得錢包餘額資訊。

綜合上述程式碼的結構和流程,每一個 Action 方法代表著一個單獨的功能或業務邏輯。這意味著每一個功能都被獨立地封裝,且 UserService 類別集中了與使用者相關的所有操作,也增加了維護性與可讀性。

最後,我們打開 Postman 進行呼叫,一切應該都在我們的掌握之中:

結語

Anser-Service 希望你透過 Simple Service 來抽象微服務的連接和溝通。Simple Service 在 Anser-Service 中扮演著關鍵的角色,它是所有服務溝通的基礎,提供了簡化服務溝通的可能性,也為整個微服務實作提供了高可讀性與可模組化的結構。

藉由 SimpleService,我們可以容易地集中和模組化我們的連接邏輯、重試策略、超時和過濾器。此外,透過定義專屬的 Action,我們可以對單一的API請求進行細緻的設定與處理。

至此,我們已經介紹完 Anser 程式庫中的第一個子元件 Anser-Service ,很高興你也一起練習到了這裡。從下一個章節開始,我們將進入 Anser-Orchestration 子元件的單元。


上一篇
第八章、Anser-Service:服務重試與過濾器 - PHP 微服務入門與開發
下一篇
第十章、Anser-Orchestration:服務協作設計理念 - PHP 微服務入門與開發
系列文
30 天上手! PHP 微服務入門與開發30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言