在PHP中寫複雜的Swagger定義時如何偷懶(基於zircote/swagger-php)

weixin_34337265發表於2017-03-28

Swagger大大降低了介面提供者和接入者之間的溝通和維護成本,如果你還不瞭解Swagger的話,可以看我的另一篇文章《Laravel(PHP)使用Swagger生成API文件不完全指南 - 基本概念和環境搭建》

在PHP中使用Swagger,大多都會用到zircote/swagger-php這個Composer庫。以Laravel專案為例,我們通常會為每個Controller的返回寫一個單獨的Swagger Definition以方便管理,然後在Controller的Annotation中寫下這樣的規則:

<?php 

// ...

    /**
     * 假設是專案中的一個API
     *
     * @SWG\Get(path="/swagger/my-data",
     *   tags={"project"},
     *   summary="拿一些神祕的資料",
     *   description="請求該介面需要先登入。",
     *   operationId="getMyData",
     *   produces={"application/json"},
     *   @SWG\Parameter(
     *     in="formData",
     *     name="reason",
     *     type="string",
     *     description="拿資料的理由",
     *     required=true,
     *   ),
     *   @Swagger\Annotations\Schema(ref="#/definitions/MyDataResponse"),
     * )
     */
    public function getMyData()
    {
        //todo 待實現
    }

// ...

而上面引用的MyDataResponse的定義看起來可能是這樣:

<?php

/**
 * @SWG\Definition
 */
class MyDataResponse
{
    /**
     * @var string
     * @SWG\Property(example="Alan Jones")
     */
    public $data;
}

注意,$data欄位定義中設定了example屬性,這實際上是給$data欄位舉了一個返回值的例子,這樣不光可以把Swagger定義匯入工具中做介面Mock(example即是Mock介面的返回值)、在Swagger UI返回格式同樣也一目瞭然:

594774-492bb647d0026e4a.png
Property定義了example之後在Swagger UI中的顯示效果

但有時這些簡單的整型或者字串example就無法滿足專案需求了,例如你可能會需要返回這樣一個資料結構:

{
    "data": {
        "current_level": 1,
        "machine_detail": {
            "sn": "77777777",
            "mode": "extreme"
        }
        "records": [
            {
                "time": "2017-03-28 00:00:00",
                "message": "machine started"
            }
        ]
    }
}

正常來講,我們應該針對例子中的datamachine_detailrecordrecords中的每一個元素)分別建立Definition,然後在定義中去寫引用(ref=)。但有時我們就是突然感覺很懶啊!又或者這些資料結構只有這一個介面使用,實在不值當單獨定義幾個Definition去實現啊(還是懶)!

那麼怎麼辦呢?我簡單總結了三個方法。

1. 直接把複雜結構寫在example

如下:

<?php
//...

/**
 * @SWG\Property(
 *     example={"current_level": 1, [省略] "records": { { "time" [繼續省略] } } },
 * )
 * @var object
 */
public $data;

//...

這種做法最大的壞處就是在註釋中排版實在很痛苦……另外要注意得把JSON的陣列括號(方括號)寫成花括號(這是zircote/swagger-php的限制)。

2. 使用JSON檔案定義

我們可以單獨把這一個$dataDefinition寫在一個JSON檔案中,如data.json,然後在註釋(Annotation)中寫一個引用:

<?php
//...

/**
 * @SWG\Property(
 *     ref="data.json",
 * )
 */
public $data;

//...

data.json內容為:

{
    "current_level": 1,
    "machine_detail": {
        "sn": "77777777",
        "mode": "extreme"
    }
    "records": [
        {
            "time": "2017-03-28 00:00:00",
            "message": "machine started"
        }
    ]
}

這樣Swagger UI在載入完之後還會去請求data.json來獲取定義內容,最終效果是一樣的。但壞處是一部分Definition被拆到了另一個檔案,一個JSON搞不定,而且請求多了也會慢。

3. 靈活使用zircote/swagger-php

讓我們先回頭看看是怎麼使用zircote/swagger-php返回JSON格式Swagger 定義的:

<?php

// ...

    /**
     * @SWG\Swagger(
     *   @SWG\Info(
     *     title="我的`Swagger`API文件",
     *     version="1.0.0"
     *   )
     * )
     */
    public function getJSON()
    {
        $swagger = \Swagger\scan(app_path('Http/Controllers/'));

        return response()->json($swagger, 200); //注意這一句我們直接把$swagger傳給了json()方法
    }

// ...

注意示例中的$swagger物件。

在呼叫\Swagger\scan()方法時,實際上是掃描你指定的所有目錄和檔案,將其中符合規則的Swagger Annotation解析出來,並轉換為各種Class(在Swagger\Annotations名字空間下可以找到),最終這些Annotation物件都會被載入到$swagger物件裡(Swagger\Annotations\Swagger)。$swagger是一個JsonSerializable,所以可以直接作為json_encode()函式的引數,在轉換過程中,內部的各種定義物件就會被處理成一個可以JSON化的stdClass

那麼我們其實可以把最終生成的資料拿到,然後把複雜的定義直接寫成PHP陣列,在最後和$swagger轉換結果中的definitions進行合併就可以了。之前也試過直接手動新建Swagger\Annotations\Definition物件,然後合併到$swagger->definitions陣列中,但發現這寫起來遠沒有直接寫陣列的效率高。

在專案中我將這些手寫的Definition分檔案存放,然後寫了一個方法載入,最後合併到返回中。

上面例子的檔案內容可以寫成這樣:

<?php

return [
    "current_level" => 1,
    "machine_detail" => [
        "sn" => "77777777",
        "mode" => "extreme"
    ]
    "records" => [
        [
            "time" => "2017-03-28 00 =>00 =>00",
            "message" => "machine started"
        ]
    ]
];

以及改過之後的getJSON()方法:

<?php

// ...

    /**
     * @SWG\Swagger(
     *   @SWG\Info(
     *     title="我的`Swagger`API文件",
     *     version="1.0.0"
     *   )
     * )
     */
    public function getJSON()
    {
        $swagger = \Swagger\scan(app_path('Http/Controllers/'));

        return response()->json(
        mergeWithRawDefinitions($swagger, loadRawDefinition(app_path('Swagger/Raw/'))), 
        200
    );
    }

// ...

本文僅僅是拋磚引玉,如果你有更“懶”的方法,歡迎在評論中與大家分享!

相關文章