在這個章節,我們會使用到 User Service 與 Main App,請參考第四章節所提到的內容建立你的本地開發環境。
在先前的章節中,我們學習到了如何撰寫一個個單獨的 Action 定義與服務溝通的最小單元。但在一個大型的專案或微服務架構中,僅有 Action 是遠遠不夠的。當我們需要在多個 Action 之間共享一些組態設定、邏輯或行為時,將會需要一個更高層次的抽象結構,並且有組織地統合具有內聚性的 Action。
而整個 Anser-Service 子元件就是專為此目的而設計的,我們提供了 SimpleService
抽象類別給予開發人員進行實作,期望將具有特定相似功能的 Actions 抽象化為「服務」,這不僅使得邏輯更加清晰,還有助於模組化開發。
馬上來看看實作方式吧!
首先,建立起一個 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
:呼叫服務前後要執行的過濾器。範例中,我們在呼叫服務之前設定了 JsonDoneHandlerFilter
和 FailHandlerFilter
。$retry
:與微服務溝通時若發生失敗時的重試次數。$retryDelay
:當連線失敗必須重試時得等待多久才能發出連線請求。$timeout
:微服務的處理時長大於多久後自動中斷連線,也代表當次的請求為失敗請求。在示範的 UserService
類別中,我們以公開方法映射了微服務端點所提供的資源,而透過 SimpleService
的幫助,在進行 Http 請求時的前濾器、後濾器,與請求失敗時的重試次數、間隔,以及超時 皆能整合在一個類別之中統一設定。
筆者在設計 SimpleService
類別時,便希望限定開發者僅能使用成員方法 getAction 來實作微服務 API 連線細節。在執行 getAction 的過程中,會套用開發人員透過成員變數所定義的基礎設定再進行微服務連線,這意味著在相同類別下的所有連線能擁有一致的組態設定。
透過繼承這個基本的類別,開發者僅須撰寫公開方法,並在方法中對微服務的端點資源做詳細設定,這些設定包含端點位置、 請求方法、Request Options,與回應解析方式等等。
接著是在 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
}
}
這個過濾器旨在處理各種失敗的情況,範例中處理了三種主要的失敗情況:
在實際的系統開發中,你應該能在某個地方定義好通用的 failHandler()
,在大部分的情況下,伺服器錯誤與連接錯誤應該是能夠通用的區塊。
最後是 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
類別實體,再接著逐步呼叫 userLoginAction
、userInfoAction
以及 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
子元件的單元。