iT邦幫忙

2023 iThome 鐵人賽

DAY 16
0
Modern Web

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

第十六章、Anser-Orchestration:建立訂單,與三個微服務溝通的協作器 - PHP 微服務入門與開發

  • 分享至 

  • xImage
  •  

如果你是跟著文章一天接著一天實作的讀者,那麼你需要確保你的 Anser-Tutorial-Service 使用的是最新的 v1.0.3 以上的版本;或者是在你本地的 Anser-Tutorial-Service 下執行 composer updateAnser 版本更新至 v0.2.1 以上版本。

來到了 Anser-Orchestration 單元的最後一個章節了,在這個章節中我們將綜合前面章節的所有知識,進行一次完整的整合。本章與前面的章節會比較不一樣,我會詳細地闡述需求,你可以試著自己開發你的協作器。當然,本章一樣會給出一個筆者自己設計的協作器給予你參考。

建立訂單需求

在 Anser 的整個示範教學中,我們橫跨了三個微服務的溝通,分別是 User Service 、 Product Service 與 Order Service 。若我們得進行建立訂單的工作,那麼是必要與多個微服務進行溝通。建立一個新的訂單意味著訂單的庫存必須減少、訂單資訊必須被記錄,以及使用者餘額必須得到扣款。

你可以參考下圖大致了解整個微服務溝通的目標效果:

在這個需求中,我們會與微服務產生一次以上溝通,並且下一個步驟的行動通常會依賴於上一個步驟的行動的結果。分別是:

  1. 使用 Token 與 User Service 取得使用者詳細資料,在這個步驟中若是驗證成功將取得使用者的 User Key
  2. 使用請求實體中包含的 Product List 與 Product Service 取得產品最新的資訊,這包資訊裡會包含著在執行週期下產品最新的價格與庫存
  3. 執行扣庫存
  4. 建立訂單,並將產品最新售價、數量與 User Key 寫入,在這個步驟中將會取得由 Order Service 所計算好的訂單總價
  5. 最後使用 User Key 對 User Service 執行扣款

對於最外部接受使用者請求的 App 而言,會是一個這樣子的 create_order.php PHP 應用程式 :

<?php

require_once './init.php';

use Services\Models\OrderProductDetail;
use Orchestrators\CreateOrderOrchestrator;

$productList = array_map(function($product){
    return new OrderProductDetail(
        p_key: $product['p_key'],
        price: $product['price'],
        amount: $product['amount']
    );
}, json_decode(file_get_contents('php://input'), true));

$apiKey = $_SERVER['HTTP_AUTHORIZATION'] ?? "";
$result = (new CreateOrderOrchestrator($apiKey, $productList))->build();

header("Content-Type: application/json");
echo json_encode($result);

在上述的程式碼中,我們會將 Input 的 Json Body 轉換成內部的 OrderProductDetail 資料類別,你可以到 {project_root}/Orchestrators/CreateOrderOrchestrator.php 中看看這個類別的實作。總之,這個類別將是商品資料傳遞時的一個主要介面。

綜上所述,你應該可以透過 Postman 呼叫這個端點使協作器開始工作:

{{main_service}}/create_order.php
json Raw Body:
[ { "p_key": 1, "price": 450, "amount": 5 }, { "p_key": 2, "price": 70, "amount": 4 }, { "p_key": 55, "price": 70, "amount": 400000 } ]

建立訂單的需求已經闡述完畢,你可以自己先撰寫看看,別忘了我們的協作器類別名稱應該要是 CreateOrderOrchestrator 並建立在: {project_root}/Orchestrators/CreateOrderOrchestrator.php

示範程式與解說

<?php
namespace Orchestrators;

use SDPMlab\Anser\Orchestration\Orchestrator;
use Services\OrderService;
use Services\ProductionService;
use Services\UserService;
use Services\Models\OrderProductDetail;

class CreateOrderOrchestrator extends Orchestrator
{
    public UserService $userService;
    public OrderService $orderService;
    public ProductionService $productionService;

    protected string $authToken;

    /**  
     * @var OrderProductDetail[]
     */
    public array $orderProducts;

    public ?string $orderId = null;

    /**
     * CreateOrderOrchestrator
     *
     * @param string $authToken
     * @param OrderProductDetail[] $orderProducts
     */
    public function __construct(string $authToken, array $orderProducts)
    {
        $this->authToken = $authToken;
        $this->orderProducts = $orderProducts;
        $this->userService = new UserService();
        $this->orderService = new OrderService();
        $this->productionService = new ProductionService();
        $this->orderId = $this->generateOrderId();
    }

    /**
     * definition of orchestrator
     *
     * @return void
     */
    protected function definition()
    {
        //Step0 取得使用者資訊(驗證使用者)
        $this->setStep()->addAction(
            alias: 'userInfo',
            action: $this->userService->userInfoAction($this->authToken)
        );

        //Step1 取得產品最新資訊(用於取得最新售價)
        $step1 = $this->setStep();
        foreach ($this->orderProducts as $index => $orderProduct) {
            $step1->addAction(
                alias: 'product_' . ($orderProduct->p_key),
                action: $this->productionService->productInfoAction($orderProduct->p_key)
            );
        }

        //Step2 扣商品庫存
        $step2= $this->setStep();
        foreach ($this->orderProducts as $index => $orderProduct) {
            $step2->addAction(
                alias: 'product_' . ($orderProduct->p_key) . '_reduceInventory',
                action: $this->productionService->reduceInventory($orderProduct->p_key,  $this->orderId, $orderProduct->amount)
            );
        }

        //Step3 建立訂單(將會取得訂單總價 total)
        $this->setStep()->addAction(
            alias: 'createOrder',
            action: static function (CreateOrderOrchestrator $runtimeOrch) {
                $userKey = $runtimeOrch->getStepAction('userInfo')->getMeaningData()['data']['u_key'];
                //將最新商品售價更新至訂單資訊
                foreach ($runtimeOrch->orderProducts as &$product ) {
                    $product->price = (int)$runtimeOrch->getStepAction('product_' . $product->p_key)->getMeaningData()['data']['price'];
                }                
                return $runtimeOrch->orderService->createOrderAction($userKey, $runtimeOrch->orderId, $runtimeOrch->orderProducts);
            }
        );
        
        //Step4 使用者錢包扣款
        $this->setStep()->addAction(
            alias: 'walletCharge',
            action: static function (CreateOrderOrchestrator $runtimeOrch) {
                $userKey = $runtimeOrch->getStepAction('userInfo')->getMeaningData()['data']['u_key'];
                $total = $runtimeOrch->getStepAction('createOrder')->getMeaningData()['total'];
                return $runtimeOrch->userService->walletChargeAction($userKey, $runtimeOrch->orderId, $total);
            }
        );
    }

    protected function defineResult(): array
    {
        $orderInfo = $this->getStepAction('createOrder')->getMeaningData();

        $data = [
            "success" => true,
            "message" => "訂單建立成功",
            "data" => [
                "order_id" => $this->orderId,
                "total" => $orderInfo['total'],
            ]
        ];

        return $data;
    }

    protected function defineFailResult(): array
    {
        $failActions = $this->getFailActions();
        $failMessages = [];
        foreach ($failActions as $failAction) {
            $failMessages[] = $failAction->getMeaningData();
        }
        $data = [
            "success" => false,
            "message" => "訂單建立失敗",
            "data" => [
                "order_id" => $this->orderId,
                "fail_messages" => $failMessages,
            ]
        ];
        return $data;
    }

    protected function generateOrderId(): string
    {
        return sprintf(
            '%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
            random_int(0, 0xffff),
            random_int(0, 0xffff),
            random_int(0, 0xffff),
            random_int(0, 0x0fff) | 0x4000,
            random_int(0, 0x3fff) | 0x8000,
            random_int(0, 0xffff),
            random_int(0, 0xffff),
            random_int(0, 0xffff)
        );
    }

}

在上述的範例中有一些設計上的特點:

  1. 使用建構函式記錄初始參數

    所有需要使用的數值,都將以建構函式的參數形式傳遞給 CreateOrderOrchestrator 類別。透過在建構函數中明確列出所有必需參數,能夠替使用此類別的開發人員提供更清晰的指南。同時,預先定義此類別所需的所有依賴關係,將使得程式碼更容易理解和使用。
  2. 使用公開變數

    將需要在整個協作器生命週期中反覆使用的變數宣告為公開變數,即可在步驟中的匿名函數的 $runtimeOrch 進行取用。
  3. 明確的回傳處理

    透過 defineResultdefineFailResult 方法,提供了一個統一和明確的方式來處理正常和異常情況,無論協作器成功與否,都能夠獲得具有意義的回傳。
  4. 使用 UUID 於協作器中產生 Order ID

    generateOrderId() 方法利用 PHP 的 random_int 函數和 sprintf 函數生成一個符合 UUID 格式的唯一訂單ID。UUID 的生成確保每次創建訂單時,都產生一個唯一的 ID。

結語

在 Anser-Orchestration 單元的最後一章中,我們整合了前面章節的所有知識,完整展示如何建立一個訂單並與三個微服務溝通的協作器。透過這個實作案例,我們理解了在微服務架構中,如何透過協作器指揮不同服務間的互動,以完成一個複雜的業務流程。

明天,我們將進入分散式交易的單元。


上一篇
第十五章、Anser-Orchestration:深入指揮執行週期的協作器 - PHP 微服務入門與開發
下一篇
第十七章、微服務交易與 Saga 設計模式 - PHP 微服務入門與開發
系列文
30 天上手! PHP 微服務入門與開發30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言