iT邦幫忙

2023 iThome 鐵人賽

DAY 15
0
Modern Web

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

第十五章、Anser-Orchestration:深入指揮執行週期的協作器 - PHP 微服務入門與開發

  • 分享至 

  • xImage
  •  

上一章,我們了解了如何在一個協作器中同時編排順序與並行並存的協作器,本章我們將聚焦在協作器的物件實體本身。在這個章節中,我們利用 PHP 的語法特性與你介紹幾種在執行週期中處理協作器物件資訊溝通的方法,以及動態地透過其他步驟的執行結果建立下一個步驟中的 Action。

與上一章相同,本章讓我們來闡述一下開發需求:

  1. 透過傳入 User Token 與 Order ID 取得訂單的詳細資訊
  2. 依據訂單詳細資訊中的訂單商品列表再取得所有商品的詳細資訊

準備工作

建立訂單

在正常的情況下,你的 Order Service 應該會是空的,先讓我們來建立起一個測試用的訂單吧?打開你的 Postman 並來到 Order Service 的「建立訂單」API。

在預設的情況下,於 Headers 中的 x-user-key 會是 1,這意味著我們會將這個新的訂單建立給 1 的使用者。你也可以修改為你在 User Service 中存在的任何 ID ,唯獨要記得你建立給哪個 ID 以完成我們後續的開發。

你可以參考以下 JSON 結構建立你的 Raw Body :

{
    "o_key" : "test_order_1",
    "product_detail" : [
        {
            "p_key"  : 1,
            "price"  : 150,
            "amount" : 10
        },
        {
            "p_key"  : 2,
            "price"  : 75,
            "amount" : 5
        },
        {
            "p_key"  : 3,
            "price"  : 75,
            "amount" : 5
        }
    ]
}

product_detail 中的物件數量可以隨你的喜好新增與修改,唯獨需要注意的是,Production Service 中的 p_key 在自動填充商品時的序號是 1~100 不要超過這個號碼,除非你想額外處理一些討人厭的例外。

取得你的 User Token

將 Postman 切換到 User Service 的使用者登入 API ,鍵入正確的帳號密碼後即可取得 User Token,這裡要先幫我記住這個 Token ,待會兒我們會用到。

實作

進入你的 Anser-Tutorial-Service 資料夾中,若你的本地開發環境還未有這些內容,請參考第 12 章與第 4 章的內容建立起你的本地開發環境。若你是新來的讀者,筆者也非常推薦你停下腳步,回頭看看前面的章節。

在本章中我們會與 User、Production 以及 Order Service 進行溝通,因此將不再贅述我們可能會使用到的 Service 以及 Service-Methods 。

{Project_Root}/Orchestrators/OrderInfoOrchestrator 並貼入以下內容:

<?php
namespace Orchestrators;

use SDPMlab\Anser\Orchestration\Orchestrator;
use SDPMlab\Anser\Orchestration\StepInterface;
use SDPMlab\Anser\Service\ActionInterface;
use Services\OrderService;
use Services\ProductionService;
use Services\UserService;

class OrderInfoOrchestrator extends Orchestrator
{
    protected $userService;
    protected $orderService;
    public $productionService;

    /**
     * 訂單資訊 Action
     *
     * @var ActionInterface
     */
    protected $orderInfoAction;

    /**
     * 產品資訊 Action List
     * 
     * @var ActionInterface[]
     */
    public $productActionList = [];

    protected string $authToken;

    protected string $orderId;

    public function __construct(string $authToken, string $orderId)
    {
        $this->authToken = $authToken;
        $this->orderId = $orderId;
        $this->userService = new UserService();
        $this->orderService = new OrderService();
        $this->productionService = new ProductionService();
    }

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

        //查詢訂單資訊
        $orderId = $this->orderId;
        $orderService = $this->orderService;
        $this->setStep()->addAction(
            alias: 'orderInfo',
            action: static function (OrderInfoOrchestrator $runtimeOrch) use ($orderId, $orderService) {
                $data = $runtimeOrch->getStepAction('userInfo')->getMeaningData();
                $userKey = $data['data']['u_key'];
                return $orderService->orderInfoAction($userKey, $orderId);
            }
        );
        
        //動態查詢產品資訊
        $this->setStep()->addDynamicActions(
            callable: static function(OrderInfoOrchestrator $runtimeOrch, StepInterface $step){
                $data = $runtimeOrch->getStepAction('orderInfo')->getMeaningData();
                $productList = $data['data']['products'];
                foreach($productList as $product){
                    $action = $runtimeOrch->productionService->productInfoAction($product['p_key']);
                    $runtimeOrch->productActionList[$product['p_key']] = $action;
                    $step->addAction(
                        alias: $product['p_key'],
                        action: $action
                    );
                }
            }
        );
    }

    protected function defineResult(): array
    {
        $orderInfo = $this->getStepAction('orderInfo')->getMeaningData()['data'];
        foreach($orderInfo['products'] as &$product){
            $product['info'] = $this->productActionList[$product['p_key']]->getMeaningData()['data'];
        }

        $data = [
            "success" => true,
            "message" => "協作器執行成功",
            "data" => $orderInfo
        ];
        return $data;
    }

}

整個生命週期下的請求順序

在這個範例中我們首先將 $authToken 傳給 UserServiceuserInfoAction 方法,並將該 Action 設定到 userInfo 步驟中。接著,我們利用上一個步驟中的結果取得使用者的 u_key,然後再與 $orderId 一起從 OrderService 查詢訂單資訊。

最後,可能是最有趣的部分。我們要動態地根據訂單中的商品列表查詢每個商品的詳細資訊。這是透過 addDynamicActions 方法來實現的,它允許我們在執行時根據前一步驟的結果動態地建立起步驟中的 Action。

初始化協作器

與前一章不同,在這個章節中的協作器我們將在建構子(constructor)中處理所需的外來資訊。我們可以看到已經接收了兩個參數 $authToken$orderId ,並將它們隨著建構子的執行儲存在成員變數中。

這兩個參數在後續的方法中將被用到,用於取得使用者資訊和訂單資訊。

public function __construct(string $authToken, string $orderId);
protected function definition(string $authToken, string $orderId);

雖然上述的兩個做法在執行上是完全一樣的,但在開發上卻有些許不同。

你可能會發現 definition() 是一個非公開的方法,他是透過 Orchestrator::build(...$args) 執行時,再將外部的參數轉傳過來給 definition() ,也就是說在大部分的情況下 IDE 會無法了解應該傳遞什麼參數給 build() 才是正確的。

但若是你將這些所需的參數都在建構子中宣告,那麼我們將能很清楚的了解到應該傳入何種參數:

)

當然這兩種做法沒有好壞,選擇你最方便管理的開發形態即可。

使用匿名函數的 use 傳遞所需變數

匿名函數在 PHP 中能夠建立沒有指定名稱的函數體,而 use 關鍵字則是一個特殊的功能,讓你可以將外部變數「匯入」到該函數的作用域中。

例如在我們的協作器中:

$orderId = $this->orderId;
$orderService = $this->orderService;
$this->setStep()->addAction(
    alias: 'orderInfo',
    action: static function (OrderInfoOrchestrator $runtimeOrch) use ($orderId, $orderService) {
        //...
    }
);

在這裡,我們的匿名函數需要存取外部的 $orderId 和 $orderService 變數,而這些變數都宣告在協作器之下。使用 use 關鍵字,就能夠將它們帶入函數中。

公開成員變數並透過 $runtimeOrch 存取

與使用 use 將變數傳入匿名函數執行域不同,你可以換個思路把所需的變數變成公開的成員變數。


class OrderInfoOrchestrator extends Orchestrator
{
    protected $userService;
    protected $orderService;
    public $productionService;
    //...
}

在協作器執行的過程中,我們經常需要透過協作器的實體物件來獲取某些資訊或功能。這就是 $runtimeOrch 存在的原因。

$action = $runtimeOrch->productionService->productInfoAction($product['p_key']);

因此透過公開成員變數的做法,你也可以直接在匿名函數中透過 $runtimeOrch 直接存取到你所需要的實體或變數。使用這種方法可以省去將參照透過 use() 傳遞給匿名函數的過程。

一切依照你的喜好使用即可。

將 Action 的參照通通留存

在協作器中,我們可能會碰到一個情況:隨著流程進行,會有多個 Action 進行交互和執行。有時候,這些 Action 可能是基於先前的結果動態生成的,而在整個流程中,我們可能會需要參考或重複使用某些 Action 的結果。

為了在整個協作器的生命週期中輕鬆存取和參照這些 Action,一個好的做法是將每個 Action 的參照儲存在協作器的成員變數中。這可以視為一個「索引」或「目錄」,讓我們可以在之後的任何時刻快速查找和存取任何 Action 的資料。

例如:

public $productActionList = [];

我們定義了一個 productActionList 的公開成員變數,其目的是用來儲存所有關於產品的 Action 參照。當每一個新的產品 Action 被建立起來時,它的參照就會被加入到這個列表中,以供之後使用。

而在 addDynamicActions 方法中:

foreach($productList as $product){
    $action = $runtimeOrch->productionService->productInfoAction($product['p_key']);
    $runtimeOrch->productActionList[$product['p_key']] = $action;
    //...
}

透過這個流程,我們就能在協作器完成時輕易地存取到所有商品的 Action 實體,就像這樣:

protected function defineResult(): array
{
    $orderInfo = $this->getStepAction('orderInfo')->getMeaningData()['data'];
    foreach($orderInfo['products'] as &$product){
        $product['info'] = $this->productActionList[$product['p_key']]->getMeaningData()['data'];
    }
    //...
}

動態地新增步驟中的 Action

接續上一個小節,在某些情境下,我們可能會基於先前步驟的結果來動態地建立新的 Action。這與前一章提到的建立複數的 $step->addAction() 不同。在上個場景中,我們的 Action 是依賴 build(array $modifyProducts=[]) 傳入的商品陣列所依序建立起來的。

但在本章的需求中,我們無法預先得知 Order Info 中的商品數量,只能以執行週期的協作器去及時地取出 Info 列表,因此我們需要透過 addDynamicActions() 進行處理:

$this->setStep()->addDynamicActions(
    callable: static function(OrderInfoOrchestrator $runtimeOrch, StepInterface $step) {
        //...
    }
);

也因為 addDynamicActions() 會將當前的 Step 實體給傳入,所以我們可能夠直接將 Action 動態地宣告到這個步驟中。

$data = $runtimeOrch->getStepAction('orderInfo')->getMeaningData();
$productList = $data['data']['products'];
foreach($productList as $product){
    $action = $runtimeOrch->productionService->productInfoAction($product['p_key']);
    $runtimeOrch->productActionList[$product['p_key']] = $action;
    $step->addAction(
        alias: $product['p_key'],
        action: $action
    );
}

綜上所述,協作器的設計和實作都需要根據實際的業務需求來靈活選擇,但理解其核心概念和使用方式,將有助於你更高效地開發和管理你的應用程式邏輯。

執行協作器

來到了文章的末端,我們得來執行看看這個協作器了!請先建立你的程式進入點準備好 {Project_Root}/order_info.php

<?php

require_once './init.php';

use Orchestrators\OrderInfoOrchestrator;

$apiKey = $_SERVER['HTTP_AUTHORIZATION'] ?? "";
$result = (new OrderInfoOrchestrator($apiKey, $_GET['order_id']))->build();

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

再透過在準備工作中產出的 order_id 以及 user_token 以 Postman 發出請求吧!

{{main_service}}/order_info.php?order_id=test_order_1

完美!

結語

本章節為我們展示了如何進行微服務間更複雜的協作,並且有效地串接和組織這些微服務。而我們也來到了第十五章,在這半個月的時間,你應該已經了解了 Anser-Service 對微服務連線的抽象方式,以及 Anser-Orchestration 如何指揮與表達多個服務連線下的邏輯。

而下一章就是 Anser-Orchestration 的最後一章,再加把勁吧!


上一篇
第十四章、Anser-Orchestration:「順序」與「並行」共存的複雜服務協作 - PHP 微服務入門與開發
下一篇
第十六章、Anser-Orchestration:建立訂單,與三個微服務溝通的協作器 - PHP 微服務入門與開發
系列文
30 天上手! PHP 微服務入門與開發30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言