🙄

l5-swaggerでOpenAPI入門

2022/06/24に公開

APIのエンドポイントが数百を超えてくるともう一つ一つ思い出すのは無理、IDEの力を借りて辿っていけないことはないが一覧性が低いのでSwaggerに入門。
びっくりするぐらいドキュメントがないので(本当に世界一使われているドキュメンテーションツールなのか?)キックスタートできるように備忘録としてチュートリアルを残す。
ちなみに英語が大丈夫な人は下記のチュートリアルがかなりわかりやすいのでそちら見ていただいた方が良い。
https://blog.quickadminpanel.com/laravel-api-documentation-with-openapiswagger/

目的の整理

Swaggerでapi仕様をドキュメント化したい動機としては上記の通りでかつ、コンフルとかNotionとかで書くのはだるいというかコードに近い方が開発している段階で書きやすいのでそちらに寄せたい。
ただし、最初からハイエンドなことをするつもりはないのでバックエンドとフロントエンドの型一致までやるとかモックサーバーにするとかはスコープ外にする。
重要だと思うのは下記の通り

  • ルートごとにどのようなリクエストを受け付けているのか
    • パラメーター
    • クエリー
    • リクエストボディ
    • 認証有無
  • ルートごとにどのようなレスポンスが帰ってくるのか
    • レスポンスボディ
    • ステータスコード
      ようはどこに何をリクエストすると何が帰ってくるのか、エラーハンドリングが必要かということが最低限わかればまずは良い。

環境

  • Laravel9
  • PHP8.1
  • darkaonline/l5-swagger 8.3

l5-swaggerを入れる

https://github.com/DarkaOnLine/L5-Swagger
一応公式サポートはLaravel8系までみたいなのだが、9でも特に問題なく動いている。

composer require "darkaonline/l5-swagger"

l5-swaggerはPHPdocみたいな形で書けるのでJsonとかYmlを書いてメンテしなくていいので今回採用。

コントローラで使えるようにする。

Controller.php
<?php

namespace App\Http\Controllers;

use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;
use OpenApi\Annotations as OA;

/**
 * @OA\Info(
 *     version="1.0.0",
 *     title="Your System Name",
 *     description="Sample system"
 *
 * )
 */
class Controller extends BaseController
{
    use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
}

早速やってみる

/auth/meにリクエストが来たときにハンドリングしているコントローラで書いてみる。

app/Http/Controllers/MeController.php
<?php

namespace App\Http\Controllers\Auth;

use App\Constants\AccountRole;
use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use OpenApi\Annotations as OA;

class MeController extends Controller
{
    /**
     * @OA\Get(
     *     path="/auth/me",
     *     operationId="authMe",
     *     tags={"auth"},
     *     summary="check is authenticated",
     *     @OA\Response(
     *     response="200",
     *     description="successful operation",
     *     @OA\JsonContent(
     *     ref="#/components/schemas/User"
     *     )
     * )
     * )
     *
     * Handle the incoming request.
     *
     * @param \Illuminate\Http\Request $request
     */
    public function __invoke(Request $request)
    {
        return \auth()->user();
    }
}
app/OpenApi/Models/User.php
<?php

namespace App\OpenAPI\Models;

use OpenApi\Annotations as OA;

/**
 * @OA\Schema(
 *     title="User"
 * )
 */
class User
{
    /**
     * @OA\Property(
     *     title="ID",
     *     description="ID",
     *     format="int64",
     *     example="1"
     * )
     * @var int
     */
    private int $id;
}

@OAを使うとアノテーションでこのコントローラが扱うリクエストについて色々書ける。

  • まずは@OA\GET()などでリクエストの種類を指定
  • 上記の括弧の中にリクエストの具体的内容を書いていく
    • path=リクエストルート
    • parameter=リクエストパラメーター
    • operationId=この処理のIDになるもの(クラス名的な感じでつけると良いみたい)
    • tags=タグ(分類のためのものなので自由に)
    • summary=サマリー(自然言語で処理内容を記述できる)
    • @OA\Response()=レスポンスの内容
      • response=ステータスコード
      • description=このレスポンスの概要
      • @OA\JsonContent()=JsonResponseを返すときにそのJsonの具体的な内容

一旦実行してみる

php artisan l5-swagger:generate

特にエラーなどでなければOK、起動しているサーバーのapi/documentationにアクセスすると下記のような画面が表示される。

authのタブを開くと上記の内容がそれぞれ表示されているはず。

自動生成できるようにする

.env
L5_SWAGGER_GENERATE_ALWAYS=true

環境変数に蒸気を加えてあげるとファイル保存のタイミングで、l5-generateのコマンドが走るようになるので便利。

refとは

上記のコントローラのところでref="#/components/schemas/User"を使っている。
これがなんなのかというと、生成されたjsonからcomponents->schemas->Userに定義されている内容を引っ張ってくることができる。

storage/api-docs/api-docs.json
{
    "openapi": "3.0.0",
    "info": {
        "title": "System",
        "description": "システムapi仕様",
        "version": "1.0.0"
    },
    ~
    中略
    ~
    "components": {
        "schemas": {
            "User": { //refでこいつが取れる
                "title": "User",
                "properties": {
                    "id": {
                        "title": "ID",
                        "description": "ID",
                        "type": "integer",
                        "format": "int64",
                        "example": "1"
                    }
                },
                "type": "object"
            },
        },
    }
}

l5-swaggerの場合にはアノテーションに書いてある内容がstorage/api-docs/api-docs.jsonにぶち込まれる、phpファイルを用意してあげて、定義すれば最終的にはこの一つのJsonの中に入ってくるのでref以降にこのファイルの中の構造に従って使いたいSchemaなどを指定してあげればそれを参照することができる。
先頭の#はルートのかわり。

なるべく楽したい

ネットに落ちている情報だけだと困ったのが分割単位と、再利用の方法。
分割についてはどんな形で分けて管理するのが良いのかな〜と考えているがcomponentsとして使えるものは下記の通り。(ref参照できるもの)
https://swagger.io/docs/specification/components/

components:
  # Reusable schemas (data models)
  schemas:
    ...
  # Reusable path, query, header and cookie parameters
  parameters:
    ...
  # Security scheme definitions (see Authentication)
  securitySchemes:
    ...
  # Reusable request bodies
  requestBodies:
    ...
  # Reusable responses, such as 401 Unauthorized or 400 Bad Request
  responses:
    ...
  # Reusable response headers
  headers:
    ...
  # Reusable examples
  examples:
    ...
  # Reusable links
  links:
    ...
  # Reusable callbacks
  callbacks:
    ...

結論積極的に再利用するのは下記のみでそのほかは様子見にする。

  • schemas
  • parameters
  • requestBodies
  • responses

具体的にどう再利用する?

これがなかなか見つからなくて困ったけど一旦やり方がわかったので記載する。
やりたいこととしては、UserモデルがRoleを持つ場合にUserWithRoleというスキーマを新しく作成するのではなくで、@OA\JsonContent のなかでUserモデルとRoleモデルがいい感じにマージされた形が作れると嬉しい。
最終的に作りたいJsonResponseは下記の通り。

{
  "id": 1,
  "role": {
    "id": 1,
    "name": "owner"
  }
}

まずRoleのスキーマを追加

Role.php
<?php

namespace App\OpenAPI\Models;

use OpenApi\Annotations as OA;

/**
 * @OA\Schema(
 *     title="Role"
 * )
 */
class Role
{
    /**
     * @OA\Property(
     *     title="ID",
     *     description="ID",
     *     format="int64",
     *     example="1"
     * )
     * @var int
     */
    private int $id;
    /**
     * @OA\Property(
     *     title="Name",
     *     description="Role name.",
     *     format="string",
     *     example="owner"
     * )
     * @var string
     */
    private string $name;
}

MeControllerを下記のように修正

MeController.php
<?php

namespace App\Http\Controllers\Auth;

use App\Constants\AccountRole;
use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use OpenApi\Annotations as OA;

class MeController extends Controller
{
    /**
     * @OA\Get(
     *     path="/auth/me",
     *     operationId="authMe",
     *     tags={"auth"},
     *     summary="get current login user with status and role",
     *     @OA\Response(
     *     response="200",
     *     description="successful operation",
     *     @OA\JsonContent(
     *     allOf={
     *     @OA\Schema(ref="#/components/schemas/User"),
     *     @OA\Schema(
     *         @OA\Property(
     *         property="role",
     *         ref="#/components/schemas/Role"
     *         )
     *     )
     *     }
     *   )
     * )
     * )
     *
     * Handle the incoming request.
     *
     * @param \Illuminate\Http\Request $request
     */
    public function __invoke(Request $request)
    {
        return \auth()->user();
    }
}

allOfを使うと{}に含まれるスキーマのプロパティーを結合できる。今回はUserモデルのプロパティはそのままに、RoleモデルについてはUserモデルのroleプロパティとして入るような形にしたかったので、その場で@OA\Schema()を使ってスキーマを作成し、そのスキーマのroleプロパティとしてRoleモデルのプロパティをrefで参照する形にした。
上記でちゃんと動けば下記のようになっているはず。

ここまでできてしまえばあとは適宜ベースになるモデルファイルを作っていって参照したり拡張したりすればいい感じに仕様書が作られていく(はず)

あまりにも出てこなくて記事書くまでもない情報なのか、、、と思ったけどもしかしたらこれから始めたい人もいるかもというところでお役に立てば幸いです。

Discussion