Yii2.0 初識 RESTful Serializer

AlanJager發表於2020-04-07

當RESTful API響應中包含一個資源時,該資源需要序列化成一個字串。 Yii將這個過程分成兩步,首先,資源會被yii\rest\Serializer轉換成陣列, 然後,該陣列會通過yii\web\ResponseFormatterInterface根據請求格式(如JSON, XML)被序列化成字串。當開發一個資源類時應重點關注第一步。


所以開啟Yii框架下的vendor/yiisoft/yii2/rest/Serializer.php

    public $fieldsParam = 'fields';
   
    public $expandParam = 'expand';
    
    public $totalCountHeader = 'X-Pagination-Total-Count';
   
    public $pageCountHeader = 'X-Pagination-Page-Count';
   
    public $currentPageHeader = 'X-Pagination-Current-Page';
    
    public $perPageHeader = 'X-Pagination-Per-Page';
    
    public $collectionEnvelope;
   
    public $linksEnvelope = '_links';
    
    public $metaEnvelope = '_meta';
    
    public $request;
   
    public $response;

變數$fieldsParam對應的是是RESTful風格中的yii\base\Model::fields()或者yii\db\ActiveRecord::fields()方法的返回值結果為一個鍵值對陣列

例如:

public function fields(){        return [                // 欄位名和屬性名相同        'id',        // 欄位名為"email", 對應的屬性名為"email_address"        'email' => 'email_address',               // 欄位名為"name", 值由一個PHP回撥函式定義       'name' => function ($model)            return $model->first_name . ' ' . $model->last_name;        },    ];}

那麼對應的當該資源被請求時返回的將是:

[        // 欄位名和屬性名相同        'id',        // 欄位名為"email", 對應的屬性名為"email_address"        'email' => 'email_address',        // 欄位名為"name", 值由一個PHP回撥函式定義        'name' => function ($model) {            return $model->first_name . ' ' . $model->last_name;        },    ]

如果對應的模型層中沒有實現yii\base\Model::fields()或者yii\db\ActiveRecord::fields()方法,則會預設返回所有定義的欄位。

變數$expandParam對應的是yii\db\Model::extraFields() 和 yii\db\ActiveRecord::extraFields() 其中前者返回空值,後者返回關聯模型中定義的欄位鍵值對陣列,實現方法與yii\base\Model::fields()或者yii\db\ActiveRecord::fields()相同。

變數$totalCountHeader當請求資源時如果設定了分頁,那麼$totalCountHeader對應資源請求中的總的記錄數量。

變數$pageCountHeader當請求資源時如果設定了分頁,那麼$pageCountHeader對應資源請求中的分頁總數。

變數$currentPageHeader當請求資源時如果設定了分頁,那麼$currentPageHeader對應資源請求中的當前的頁碼。

變數$perPageHeader當請求資源時如果設定了分頁,那麼$perPageHeader對應資源請求中每一頁的記錄數量。

變數$collectionEnvelope,當發出一個資源請求,其中包括了對分頁的要求或者對於關聯操作進行的設定,那麼$collectionEnvelope就會有對應的值。

首先是返回的關聯操作,統一的是以資源請求的url形式進行返回的,宣告的時候會有譬如最典型的返回一個self連結:

class User extends ActiveRecord implements Linkable{    public function getLinks()    {        return [            Link::REL_SELF => Url::to(['user/view', 'id' => $this->id], true),        ];    }}

那麼在我們得到的資源中將會有:

{    "id": 100,    "email": "user@example.com",    // ...    "_links" => {        "self": {            "href": "https://example.com/users/100"        }    }}

增加了一個_links標記 其中包括了我們在新增的yii\db\ActiveRecord::getLinks()中新增的url,而這個標籤內的內容恰好是變數$linksEnvelope對應的,如果設定了分頁那麼類似_links標記,還會增加一個_meta標記由$metaEnvelope對應:

'_meta' => {    // meta information as returned by Pagination::toArray()
    'totalCount' => 100,
    'pageCount' => 5,
    'currentPage' => 1,
    'perPage' => 20,
}

對於變數的解釋就到這裡,接下來是對方法部分的解釋。

init()方法:

 public function init()
 {
     if ($this->request === null) {
         $this->request = Yii::$app->getRequest();
     }
     if ($this->response === null) {
         $this->response = Yii::$app->getResponse();
     }
 }

每次初始化時會初始化$request和$response的值,這兩個變數的實現參考vendor/yiisoft/yii2/web/Request.php

和vendor/yiisoft/yii2/web/Respense.php 就是對資料的接收的返回的解析和封裝,這裡不做贅述。


serialize()方法:

    public function serialize($data)
    {
        if ($data instanceof Model && $data->hasErrors()) {
            return $this->serializeModelErrors($data);
        } elseif ($data instanceof Arrayable) {
            return $this->serializeModel($data);
        } elseif ($data instanceof DataProviderInterface) {
            return $this->serializeDataProvider($data);
        } else {
            return $data;
        }
    }

這一步是對資料進行序列化,將資料格式統一處理為標準的RESTful的資料格式,便於開發者操作處理。

首先看是第一個條件,判斷了$data是否為符合要求的資料,此處呼叫的yii\base\Model::hasErrors()方法的程式碼如下:

    public function hasErrors($attribute = null)
    {
        return $attribute === null ? !empty($this->_errors) : isset($this->_errors[$attribute]);
    }

當傳入資料沒有屬性,或者資料有屬性是否存在不符(通過資料層中實現的rules()方法檢驗)。

如果資料符合要求則會進入下面的序列化階段,如果傳入的資料為鍵值對那麼使用serializeModel()方法:

protected function serializeModel($model)
    {
        if ($this->request->getIsHead()) {
            return null;
        } else {
            list ($fields, $expand) = $this->getRequestedFields();
            return $model->toArray($fields, $expand);
        }
    }

首先用yii2/web/Request::getIsHead()方法來判斷當前的請求是否為HEAD請求是的話不返回資訊,如果不是則通過getRequestedFields()方法對資料進行序列化,程式碼如下:

    protected function getRequestedFields()
    {
        $fields = $this->request->get($this->fieldsParam);
        $expand = $this->request->get($this->expandParam);


        return [
            preg_split('/\s*,\s*/', $fields, -1, PREG_SPLIT_NO_EMPTY),
            preg_split('/\s*,\s*/', $expand, -1, PREG_SPLIT_NO_EMPTY),
        ];
    }

首先呼叫yii2/web/Request::get()方法

    public function get($name = null, $defaultValue = null)
    {
        if ($name === null) {
            return $this->getQueryParams();
        } else {
            return $this->getQueryParam($name, $defaultValue);
        }
    }
    public function getQueryParam($name, $defaultValue = null)     {         $params = $this->getQueryParams();         return isset($params[$name]) ? $params[$name] : $defaultValue;     }

去掉請求中的空格並且返回非空部分作為標籤名。然後由serialize()返回該值。


如果傳入的資料型別是根據DataProviderInterface介面實現的,那麼則需要根據傳入的分頁資訊對資料進行處理。

關於Yii的Arrayable介面,詳情請見官方文件