📑

Laravelで最速でOpenAPI(Swagger)ドキュメントを自動生成する

2024/03/18に公開

TL;DR

  • dedoc/Scrambleを使えば超ラクにOpenAPIを作成できるよ
  • あまり細かな記述はできないけど、カスタムやアノテーション記述など自由度はある
  • 全くドキュメントが更新されていない既存プロジェクトに突っ込むのもアリ

はじめに

ドキュメントってほんとに難しいですよね
なにが難しいかって高確率でソースと乖離するところ
最初のうちはドキュメント更新も頑張るんだけど、そのうちソースコードと乖離が大きくなってきてソースコードが仕様書みたいになっているプロジェクトに遭遇したことはありませんか?

「あっそれ仕様書と違ってここは◯◯が返って、ここには✕✕が追加されてて、それで、、、」

こういうやつ
私は何度もあります、、、
これを回避するためにドキュメントの自動生成に踏み切ろうかと思いましたが、なかなか自動化のための記述量が多かったり、学習コストが高いものが多く、なかなか導入に踏み切れなかった中、Scrambleという超イケてるライブラリを見つけたので共有の記事になります。

各種ライブラリ比較

OpenAPI自動生成となると色んなライブラリがあると思いますが、まずはじめにてっとり早く比較をします。
同じものを作った場合のサンプルを3つ
LoginというAPIのドキュメントを作成してみます。

  • L5-Swagger
    有名なライブラリですね、これで試しに書いてみます。
/**
 * @OA\Tag(
 *     name="auth",
 *     description="ログイン"
 * )
 */
class Login extends Controller
{
    /**
     *
     * @OA\Post(
     *     path="/login",
     *     operationId="authLogin",
     *     tags={"auth"},
     *     summary="ログイン",
     *     description="ユーザーのログイン処理を行い、ユーザー情報とAPIトークンを返却します。",
     *     @OA\RequestBody(
     *         required=true,
     *         @OA\JsonContent(
     *             required={"email", "password"},
     *             @OA\Property(property="email", type="string", example="user@example.com"),
     *             @OA\Property(property="password", type="string", example="string")
     *         )
     *     ),
     *     @OA\Response(
     *         response=200,
     *         description="Success",
     *         @OA\JsonContent(ref="#/components/schemas/UserResource")
     *     ),
     *     @OA\Response(
     *         response=401,
     *         description="Authentication error",
     *         @OA\JsonContent(
     *             @OA\Property(property="message", type="string", example="Unauthenticated.")
     *         )
     *     ),
     *     @OA\Response(
     *         response=403,
     *         description="Authorization error",
     *         @OA\JsonContent(
     *             @OA\Property(property="message", type="string", example="Forbidden.")
     *         )
     *     ),
     *     @OA\Response(
     *         response=422,
     *         description="Validation error",
     *         @OA\JsonContent(
     *             @OA\Property(property="message", type="string", example="The given data was invalid."),
     *             @OA\Property(
     *                 property="errors",
     *                 type="object",
     *                 @OA\Property(
     *                     property="email",
     *                     type="array",
     *                     @OA\Items(type="string", example="メールアドレスは必須です。")
     *                 ),
     *                 @OA\Property(
     *                     property="password",
     *                     type="array",
     *                     @OA\Items(type="string", example="パスワードは必須です。")
     *                 )
     *             )
     *         )
     *     )
     * )
     */
    public function __invoke(LoginRequest $request): UserResource
    {
        // ...
        return (new UserResource($user))
            ->additional(['api_token' => $token]);
    }
}

うーん、、、ちょっと思ってたのと違う感
自分は導入に至りませんでしたが、利用されている方は多いと思います。

  • swagger-php
    こちらはGitHubのStar数的にも一番使われている数が多いのではないでしょうか。
    サンプルを記載してみます
#[OA\Tag(
    name: 'auth',
    description: 'ログイン'
)]
class Login extends Controller
{
    /**
     * @param LoginRequest $request
     * @return UserResource
     */
    #[OA\Post(
        path: '/login',
        operationId: 'authLogin',
        tags: ['auth'],
        summary: 'ログイン',
        description: 'ユーザーのログイン処理を行い、ユーザー情報とAPIトークンを返却します。',
        requestBody: new OA\RequestBody(
            required: true,
            content: new OA\JsonContent(
                required: ['email', 'password'],
                properties: [
                    new OA\Property(property: 'email', type: 'string', example: 'user@example.com'),
                    new OA\Property(property: 'password', type: 'string', example: 'string')
                ]
            )
        ),
        responses: [
            new OA\Response(
                response: 200,
                description: 'Success',
                content: new OA\JsonContent(ref: '#/components/schemas/UserResource')
            ),
            new OA\Property(property: 'api_token', type: 'string', example: '1|abcdefghijklmnopqrstuvwxyz')
            new OA\Response(
                response: 401,
                description: 'Authentication error',
                content: new OA\JsonContent(
                    properties: [
                        new OA\Property(property: 'message', type: 'string', example: 'Unauthenticated.')
                    ]
                )
            ),
            new OA\Response(
                response: 403,
                description: 'Authorization error',
                content: new OA\JsonContent(
                    properties: [
                        new OA\Property(property: 'message', type: 'string', example: 'Forbidden.')
                    ]
                )
            ),
            new OA\Response(
                response: 422,
                description: 'Validation error',
                content: new OA\JsonContent(
                    properties: [
                        new OA\Property(property: 'message', type: 'string', example: 'The given data was invalid.'),
                        new OA\Property(
                            property: 'errors',
                            type: 'object',
                            properties: [
                                new OA\Property(
                                    property: 'email',
                                    type: 'array',
                                    items: new OA\Items(type: 'string', example: 'メールアドレスは必須です。')
                                ),
                                new OA\Property(
                                    property: 'password',
                                    type: 'array',
                                    items: new OA\Items(type: 'string', example: 'パスワードは必須です。')
                                )
                            ]
                        )
                    ]
                )
            )
        ]
    )]
    public function __invoke(LoginRequest $request): UserResource
    {
        // ...
        return (new UserResource($user))
            ->additional(['api_token' => $token]);
    }
}

Attributeで書けるのでタイポはこちらのほうが気付きやすそうですね。
これは結構いい感じです。

そして今回紹介するdedoc/Scramble

/**
 * @tags Auth
 */
class Login extends Controller
{
    /**
     * ログイン
     *
     * ユーザーのログイン処理を行い、ユーザー情報とAPIトークンを返却します。
     *
     * @unauthenticated
     */
    public function __invoke(LoginRequest $request): UserResource
    {
        // ...
        return (new UserResource($user))
            ->additional(['api_token' => $token]);
    }
}

...!?
ほぼなんも書いてねぇ!!

そう、このdedoc/Scrambleは型などを推論して自動出力することに特化したライブラリになっています。
そのため、細かな記述はなくともそこそこのドキュメントを作ってくれます
上に書いたコードで実際に作られたドキュメントがこちら

Image from Gyazo

Modelに$casts指定したEnumやResource内の->whenLoadedしたコレクションまで読み取ってくれています。
更になにも書いていないのに422など発生し得るLaravelのデフォルト例外のレスポンスも勝手に書いてくれます。
additionalとかも読み取ってくれています。すごい。

ただし、Exampleの値は"string"とかばっかりになってちょっと微妙かも
これはPHPDocに@exampleを書けば好きなサンプルを書けるようになるらしい
https://github.com/dedoc/scramble/issues/264
これみた感じ次の0.9.1アップデートで@exampleは実装されそうです。

できること

Requestの自動ドキュメント

FormRequestの自動ドキュメント化
public function __invoke(LoginRequest $request): UserResource
この部分。
LoginRequestは別途FormRequestで定義しているが、それを読み取ってくれるらしい、すごい

または$request->validateValidator::makeしているコントローラーのRequestの自動ドキュメント化
コントローラー内にバリデーション書いてもOK、フォームリクエストでもOKって感じらしい

対応しているルール一覧

  • required
  • string
  • bool,boolean
  • number
  • int,integer
  • array
  • in,Rule::in
  • nullable
  • email
  • uuid
  • exists(属性名がintid*_id)
  • min(数値型のみ)
  • max(数値型のみ)
  • Enum
  • confirmed

公式ドキュメントに詳しく書いている

レスポンスの自動ドキュメント

謎の技術を使ってかなり高い精度で型の推論をしてくれている
うまく推論が効かない場合はPHPDocを書けば良いみたいです
下記の図の1,2,3の優先度でドキュメントに反映されるそうです

    /**
     * @response TodoItemResource // 1 - PhpDoc
     */
    public function update(Request $request, TodoItem $item): TodoItemResource // 2 - typehint
    {
        return new TodoItemResource($item); // 3 - code inference
    }

対応しているレスポンス一覧

  • API Resource
  • API Resource Collection (匿名コレクションと専用クラスの両方)
  • response()
  • ResponseFactory
  • response()->make(...)
  • response()->json(...)
  • response()->noContent()
  • JsonResponse、Response
  • Model
  • 単純型:配列、文字列、数値など

公式ドキュメントに詳しく書いている

コントローラーのメソッドのPHPDocがsummaryとdescriptionになる

    /**
     * ログイン
     *
     * ユーザーのログイン処理を行い、ユーザー情報とAPIトークンを返却します。
     *
     * @unauthenticated
     */

この部分ですね、行数でチェックしているようで、一番上の行がsummary、スペース空けて次のがdescriptionのようです。

グルーピングに@tagsを使う

今回はシングルアクションコントローラのため、グルーピングするためにPHPDocに記述しましたが
コントローラー単位でグルーピングする場合、特に記述の必要はありません

Sanctum

APIのトークン認証なんかにも対応しています
デフォルトのSanctumのcreateToken()とかで作るやつの場合、AppServiceProviderに下記のように記述すると良いです

use Dedoc\Scramble\Scramble;
use Dedoc\Scramble\Support\Generator\OpenApi;
use Dedoc\Scramble\Support\Generator\SecurityScheme;

/**
 * Bootstrap any application services.
 *
 * @return void
 */
public function boot()
{
    Scramble::extendOpenApi(function (OpenApi $openApi) {
        $openApi->secure(
            SecurityScheme::http('bearer', 'token')
        );
    });
}

Image from Gyazo

画像のようにリクエストに認証がつきます

詳しくは公式ドキュメント

@throwsをどうも読み取ってくれるらしい

->findOrFail()はどうも404の自動推論が効かなかったので、コントローラーのPHPDocに
@throws ModelNotFoundExceptionを記載してみたところ、404のレスポンスが自動生成された。
401については自動生成されなかったのでこのページを読みながらAuthenticationExceptionがあったら401を追加するコードを自前で追加した。

ドキュメント生成にコマンドがいらない

ソースコードの更新とともにリアルタイムで反映されていきます
jsonのコマンドによる生成もできるようです

その他

開発が活発に行われている

Laravel11リリースと同時に速攻で対応していました。ありがたい。
またどんどん良い機能が追加されていきそうですね。

最後に

また導入したばかりであまり使いこなせていないので
また追記していきます
皆さんもよきScrambleライフを!!

GitHubで編集を提案

Discussion