---
# System prepended metadata

title: Anser-Gateway開發守則
tags: [Anser]

---

# Anser-Gateway開發守則

###### tags: `Anser`
[TOC]

## Code Style

* 命名原則以 **`駝峰`** 命名為主
* 變數命名方式以描述清楚為目標，例如:
    ```php=
    class ServiceDiscovery
    {
        /**
         * ServiceDiscovery 設定檔實體
         *
         * @var \Config\ServiceDiscovery
         */
        protected $serviceDiscoveryConfig;

        /**
         * GatewayRegister 設定檔實體
         *
         * @var GatewayRegister
         */
        protected $gatewayRegister;

        /**
         * 須被訪問的服務
         *
         * @var array
         */
        protected array $defaultServiceGroup = [];

        /**
         * 重新搜尋的等待Timer
         *
         * @var integer
         */
        public int $reloadTime;

    ```
    每一變數命名方式皆為變數的職責為何。
* 變數命名後，**`註解須清楚標記該變數型別為何`**，如int則為integer(Line 24~29)，若為類別(Line 3~8)，則標記類別名稱。
* 方法撰寫原則與上述相似，方法名稱以描述行為為主；若方法具備傳入子，則須明確標記傳入子型別(Line 7)，註解部分盡可能撰寫清楚該方法職責為何，並標記`param`和`return` 型別，若無回傳值`return`則回傳void。可特別注意Line 7 結尾的`:void`，此處為標記回傳型別，記得加上，讓PHP編譯檢查。
    ```php=
    /**
     * 如服務回傳多組service的話則使用該方法
     *
     * @param array $services
     * @return void
     */
    public function setServices(array $services): void
    {
        foreach ($services as $service) {
            $serviceEntity        = $service["Service"];
            $serviceTags          = $serviceEntity["Tags"];
            $serviceName          = $serviceEntity["Service"];
            $serviceAddress       = $serviceEntity["Address"];
            $servicePort          = $serviceEntity["Port"];
            $serviceSchemeIsHttp  = false;

            foreach ($serviceTags as $tag) {
                if (strpos($tag, 'http_scheme=') === 0) {
                    $serviceScheme = substr($tag, strlen('http_scheme='));
                    $serviceSchemeIsHttp = strtolower($serviceScheme) == 'http' ? false : true;
                    break;
                }
            }

            $this->localServices[$serviceName][] = [
                "name"    => $serviceName,
                "address" => $serviceAddress,
                "port"    => $servicePort,
                "scheme"  => $serviceSchemeIsHttp
            ];
        }
    }

    ```
    
## AnserGateway 生命週期
### 角色描述
:::success
* Client : 通常指外部使用者或對Anser-Gateway發起請求的人
* GatewayWorker.php : Anser-Gateway運行關鍵，初始化Workerman的Worker，運行Anser-Gateway邏輯
* Anser-Gateway Class : Anser-Gateway處理HTTP Request/Response 的類別，內部將Request進行拆解後導向Routes、Filters、Controllers等元件解析請求。
* Router Class : 路由器，根據傳入的Request URI找尋User從config/Routes.php定義的路由，且呼叫所定義的Controller與Filter
* Filter Class : 過濾器，分為before、After Filter，Before作用於Router Class後，Controller Class前，主要作某些不宜實作於Controller層的邏輯，例如登入驗證等；After作用於Controller Class後，適合做一些回傳Response前的驗證。
* Controller Class : 完整闡述業務邏輯之處，如與微服務溝通、呼叫Orchestration等行為。
* Response : 由Controller Class建構時產生(此時為空)，Controller Class作用結束後會產生第一階段Response，若Controller Class完成後有綁定After Filter ，則會將Response帶入Filter中進行After Filter實作，最終回傳Response；若未綁定After Filter，則將直接回傳Response。
:::
![Anser Gateway class diagram](https://hackmd.io/_uploads/rJcaPUR0a.jpg)
圖1. Anser-Gateway的Request/Response處理邏輯

![Anser Gateway class diagram (1)](https://hackmd.io/_uploads/r1apsIRAT.jpg)
圖2. Anser-Gateway元件圖



## 關於新類別的開發
### 設定檔
:::danger
若實作類別不存在開發人員自訂義類別參數，則可略過此段。
:::

在軟體開發中，某些系統參數是私密或個人化的，故不宜直接被Hard Code於類別或功能模組中，故採環境變數(Environment-Variables, ENV)進行帶入。[關於ENV](https://www.w3schools.in/php/environment-variables)

#### Env在Anser-Gateway中的實踐
:::success
主要檔案位置
system/Config/BaseConfig.php
system/Config/DotEnv.php
:::

##### DotEnv.php
該檔案主要職責為將我們在.env檔案中定義的環境設定變數存入到memory中，讓PHP呼叫；會在anser啟動時被初始化。
##### BaseConfig.php
所有涉及環境變數的類別設定皆須繼承該類別，該類別主要職責為讀取類別變數，並將.env檔案中的參數賦值至類別中。

以服務探索為例 `config/ServiceDiscovery.php`，類別於Line 7繼承了BaseConfig，並存在一些變數未被賦值，此時可觀察下面的.env檔案，內部定義皆與`ServiceDiscovery.php`相同，該處須注意的為，在.env中註冊變數時記得以`類別名稱.變數名稱`的方式進行定義，如: `servicediscovery.defaultServiceGroup`
```php=
<?php
// config/ServiceDiscovery.php
namespace Config;

use AnserGateway\Config\BaseConfig;

class ServiceDiscovery extends BaseConfig
{
    /**
     * 需要被探索的服務名稱
     *
     * @var array<string>
     */
    public array $defaultServiceGroup = [];

    /**
     * Consul Server IP Address and port
     *
     * @var string
     */
    public string $address = 'http://localhost:8500';

    /**
     * HTTP Scheme [http or https]
     *
     * @var string
     */
    public string $scheme = 'http';

    /**
     * Consul Server DataCenter
     *
     * @var string
     */
    public string $dataCenter = '';

    /**
     * 請求間隔
     *
     * @var integer
     */
    public int $reloadTime = 10;

    /**
     * 服務負載均衡演算法
     *
     * @var string
     */
    public string $LBStrategy = 'random';

    public function __construct()
    {
        parent::__construct();

        // 因.env傳入為字串，故使用explode作切割
        if(getenv('servicediscovery.defaultServiceGroup') !== ''){
            $this->defaultServiceGroup = explode(',', getenv('servicediscovery.defaultServiceGroup'));
        }
    }
}
```
```env=
#.env
#--------------------------------------------------------------------
# Service Discovery
#--------------------------------------------------------------------
# servicediscovery.defaultServiceGroup = ServiceName1,ServiceName2  # [required] Same name as the service registered on Consul, If you have more than one service, please separate them with a comma(,).
# servicediscovery.address = 'localhost:8600'                       # [required]
# servicediscovery.scheme  = 'http'                                 # [optional] defaults to "http"
# servicediscovery.dataCenter = ''                                  # [optional]
# servicediscovery.reloadTime = 300                                 # [optional] php json encode opt value to use when serializing requests
# servicediscovery.LBStrategy = random
```

### 類別中呼叫外部API
此時建議使用Anser-Action進行操作，若直接使用GuzzleHTTP的話會出現小複雜的效能負擔(這部份往Coroutine和HTTP連線管理去了)。
#### 實際撰寫方式
以`system/ServiceDiscovery/ServiceDiscovery.php`為例，下述程式碼為Anser-Gateway啟動時將自己註冊至Consul中的步驟，此處直接使用Anser-Action進行實現，可有效使用HTTP連線管理。

```php=
    /**
     * 註冊AnserGateway 至 Consul Server
     *
     * @param string $httpScheme
     * @param integer $port
     * @return bool
     */
    public function registerSelf(string $httpScheme, int $port): bool
    {

        $gatewayAddress =  sprintf(
            '%s://%s:%s',
            $httpScheme,
            $this->gatewayRegister->address,
            $port
        );

        $checkRoute = sprintf(
            '%s/%s',
            $gatewayAddress,
            $this->gatewayRegister->healthRoute
        );

        array_push($this->gatewayRegister->tags, "http_scheme={$httpScheme}");

        $action = (new Action(
            $this->consulAddress,
            "PUT",
            "v1/agent/service/register"
        ))->addOption("json", [
            "id" => $this->gatewayRegister->id,
            "name" => $this->gatewayRegister->name,
            "tags" => $this->gatewayRegister->tags,
            "address" => $this->gatewayRegister->address,
            "port" => (int)$port,
            "check" => [
                "name" => $this->gatewayRegister->name,
                "service_id" => $this->gatewayRegister->id,
                "http" => $checkRoute,
                "interval" => $this->gatewayRegister->interval,
                "timeout" => $this->gatewayRegister->timeout
            ]
        ])->doneHandler(function (
            ResponseInterface $response,
            Action $runtimeAction
        ) {
            $body = $response->getBody()->getContents();
            $data = json_decode($body, true);
            $runtimeAction->setMeaningData($data);
        })->failHandler(function (
            ActionException $e
        ) {
            if($e->isClientError()) {
                $e->getAction()->setMeaningData([
                    "code" => $e->getStatusCode(),
                    "msg" => "client error"
                ]);
            } elseif ($e->isServerError()) {
                $e->getAction()->setMeaningData([
                    "code" => $e->getStatusCode(),
                    "msg" => "server error"
                ]);
            } elseif($e->isConnectError()) {
                $e->getAction()->setMeaningData([
                    "msg" => $e->getMessage()
                ]);
            }
        });

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

        if (isset($data['msg'])) {
            throw \AnserGateway\ServiceDiscovery\Exception\ServiceDiscoveryException::forAnserGatewayRegisterError($data);
        }

        /**
         * Consul 註冊成功回傳為null
         */
        if(is_null($data)) {
            return true;
        }
        return false;
    }

```

### 類別初始化位置
若是元件規模較大或為單例，建議於`system/Worker/GatewayWorker.php`進行初始化。
簡單介紹GatewayWorker的週期，主要有兩個時期
:::success
一、$webWorker->onWorkerStart
這邊簡單來說就是當Worker被建立時，需要做些甚麼，很像JS的callable函式實作方式。

二、$webWorker->onMessage
當Worker接收到外部Request時會動作的部分，此處也可以用JS的callable去理解，當某個按鈕被按到時就觸發。
此處放置較重要的功能模組，如AnserGateway Class
:::

* 以Line 30的`$serviceDiscovery`為例，變數屬性為static(靜態)並賦予初始值null(盡量給初始值)。
* 變數初始化周期在於`onWorkerStart`時，參考Line 72~75。
* 若功能需對每一請求進行處理則可放在`onMessage`執行，視功能規模必要性實作於此，若可實作於`runtimeTcpConnection`或`Filter`的話盡量實作在那邊。
```php=
<?php

namespace AnserGateway\Worker;

use Config\Gateway;
use Swow\Coroutine;
use Workerman\Timer;
use Workerman\Worker;
use AnserGateway\Autoloader;
use AnserGateway\Worker\Swow;
use AnserGateway\AnserGateway;
use AnserGateway\Router\Router;
use Workerman\Protocols\Http\Request;
use Workerman\Protocols\Http\Response;
use SDPMlab\Anser\Service\ServiceList;
use AnserGateway\Router\RouteCollector;
use Workerman\Connection\TcpConnection;
use AnserGateway\HTTPConnectionManager;
use AnserGateway\Worker\WorkerRegistrar;
use AnserGateway\ServiceDiscovery\ServiceDiscovery;

class GatewayWorker extends WorkerRegistrar
{
    protected Gateway $gatewayConfig;

    public static $routeList;

    public static $router;

    public static $serviceDiscovery = null;

    public function __construct()
    {
        $this->gatewayConfig = new Gateway();
        self::staticSetting();
    }

    public function initWorker(): Worker
    {
        $config    = $this->gatewayConfig;

        $webWorker = new Worker(
            sprintf(
                '%s://%s:%s',
                $config->ssl ? 'https' : 'http',
                '0.0.0.0',
                $config->listeningPort
            ),
            $this->gatewayConfig->ssl ? [
                'ssl' => [
                    'local_cert'        => $config->sslCertFilePath,
                    'local_pk'          => $config->sslKeyFilePath,
                    'verify_peer'       => $config->sslVerifyPeer,
                    'allow_self_signed' => $config->sslAllowSelfSigned,
                ],
            ] : []
        );
        $webWorker->name = 'AnserGateway';
        $webWorker->reloadable = true;
        $this->instanceSetting($webWorker);

        // On start
        $webWorker->onWorkerStart = static function (Worker $worker) use ($config) {
            Autoloader::$instance->appRegister();
            Autoloader::$instance->composerRegister();
            require_once PROJECT_CONFIG . 'Service.php';
            //此處開始框架其他部件初始化

            \AnserGateway\Worker\GatewayWorker::$routeList        = RouteCollector::loadRoutes();
            \AnserGateway\Worker\GatewayWorker::$router           = new Router(\AnserGateway\Worker\GatewayWorker::$routeList);

            if ($config->enableServiceDiscovery) {
                \AnserGateway\Worker\GatewayWorker::$serviceDiscovery = new ServiceDiscovery();
                \AnserGateway\Worker\GatewayWorker::$serviceDiscovery->registerSelf($config->ssl ? 'https' : 'http', $config->listeningPort);
            }

            ServiceList::setGlobalHandlerStack(HTTPConnectionManager::connectionMiddleware());
            HTTPConnectionManager::$hostMaxConnectionNum = 150;
            HTTPConnectionManager::$waitConnectionTimeout = 200;

            // Timer包co ，實作服務發現邏輯...
            if (!is_null(\AnserGateway\Worker\GatewayWorker::$serviceDiscovery)) {
                // first discovery
                ServiceList::setServiceDataHandler(\AnserGateway\Worker\GatewayWorker::$serviceDiscovery->serviceDataHandler());
                // 預先探索服務
                \AnserGateway\Worker\GatewayWorker::$serviceDiscovery->doServiceDiscovery();
                Timer::add(
                    \AnserGateway\Worker\GatewayWorker::$serviceDiscovery->reloadTime,
                    static function () {
                        Coroutine::run(static function (): void {
                            \AnserGateway\Worker\GatewayWorker::$serviceDiscovery->doServiceDiscovery();
                        });
                    }
                );
            }
        };

        // Worker
        $webWorker->onMessage = static function (TcpConnection $connection, Request $request) use ($config) {
            Coroutine::run(static function () use ($connection, $request, $config): void {
                $config->runtimeTcpConnection($connection, $request);

                # Injection Router class to AnserGateway
                $gateway = new AnserGateway(\AnserGateway\Worker\GatewayWorker::$router);

                try {
                    $workermanResponse = $gateway->handleRequest($request);
                } catch (\Exception $e) {
                    $workermanResponse = new Response(
                        500,
                        [
                            'Content-Type' => 'application/json charset=utf-8',
                        ],
                        json_encode([
                            'code' => 500,
                            'msg' => $e->getMessage(),
                            'data' => null
                        ])
                    );
                }

                $connection->send($workermanResponse);
                unset($gateway);
            });
        };

        return $webWorker;
    }

    protected function instanceSetting(Worker &$worker)
    {
        $worker->count      = $this->gatewayConfig->workerCount;
        $worker->user       = $this->gatewayConfig->workerUser;
        $this->gatewayConfig->initWorker($worker);
    }

    public function staticSetting()
    {
        Worker::$eventLoopClass                  = Swow::class;
        Worker::$stdoutFile                      = $this->gatewayConfig->stdoutFile;
        Worker::$logFile                         = $this->gatewayConfig->logFile;
        TcpConnection::$defaultMaxPackageSize    = $this->gatewayConfig->defaultMaxPackageSize;
        TcpConnection::$defaultMaxSendBufferSize = $this->gatewayConfig->defaultMaxSendBufferSize;
    }
}
```
