swagger-phpのアノテーションからAPIドキュメントを自動生成する

概要

PHPでAPIを開発していると、「実装は進むのにドキュメントが追いつかない」「手で書いたAPI仕様がすぐ古くなる」という問題に必ず突き当たります。

この記事では zircote/swagger-php を使い、コード中のアノテーション(PHP 8 の Attribute)から OpenAPI 定義(openapi.json / openapi.yaml)を自動生成し、Swagger UI で閲覧できるようにするまでをハンズオン形式でまとめます。「実装=仕様の唯一の正」にすることで、ドキュメントの陳腐化を構造的に防ぐのがゴールです。

環境

  • PHP 8.2 以上
  • Composer
  • zircote/swagger-php 6.x 系(本記事執筆時点の最新は 6.10)
  • Swagger UI もしくは Redoc(表示確認用)

swagger-php 6.x は PHP 8 の Attribute を第一級の記法として扱います。レガシーな docblock(@OA\...)記法は 6.x 系では既定で無効化されており、使い続けるには doctrine/annotations を別途手動インストールする必要があります(この経路自体も非推奨)。本記事では新規採用を想定し、Attribute 記法を中心に扱うため、追加ライブラリは不要です。

発生していた問題

手書きのAPI仕様には次の問題があります。

  • 実装を変えてもドキュメント更新が漏れ、仕様と実装が乖離する
  • レビュー時に「どちらが正しいのか」が分からなくなる
  • 古い仕様を連携先に渡し、手戻りが発生する

根本原因は ドキュメントがコードから分離している ことです。

方針

対策はシンプルで、ドキュメントの一次情報をコードに寄せることです。エンドポイントを実装しているコントローラの直近にアノテーションを書き、コマンド一発で OpenAPI 定義を再生成すれば、実装とドキュメントがずれる余地が減ります。

実装手順

1. インストール

composer require --dev zircote/swagger-php
./vendor/bin/openapi --version

2. Info を1つ用意する

swagger-php は最低でも1つ OA\Info を必要とします。

<?php

namespace App\Http\Controllers;

use OpenApi\Attributes as OA;

#[OA\Info(
    version: "1.0.0",
    title: "Sample API",
    description: "swagger-php で自動生成するサンプルAPIの仕様",
)]
#[OA\Server(url: "https://api.example.com", description: "本番環境")]
class OpenApiSpec
{
    // アノテーションを載せるための器
}

3. エンドポイントにアノテーションを付ける

<?php

namespace App\Http\Controllers;

use OpenApi\Attributes as OA;

class UserController extends OpenApiSpec
{
    #[OA\Get(
        path: "/api/users/{id}",
        summary: "ユーザーを1件取得する",
        tags: ["Users"],
        parameters: [
            new OA\Parameter(
                name: "id",
                in: "path",
                required: true,
                description: "ユーザーID",
                schema: new OA\Schema(type: "integer", example: 1),
            ),
        ],
        responses: [
            new OA\Response(
                response: 200,
                description: "取得成功",
                content: new OA\JsonContent(ref: "#/components/schemas/User"),
            ),
            new OA\Response(response: 404, description: "ユーザーが存在しない"),
        ],
    )]
    public function show(int $id)
    {
        // 実装
    }
}

4. スキーマを定義して使い回す

<?php

namespace App\Schemas;

use OpenApi\Attributes as OA;

#[OA\Schema(
    schema: "User",
    required: ["id", "name"],
    properties: [
        new OA\Property(property: "id", type: "integer", example: 1),
        new OA\Property(property: "name", type: "string", example: "山田太郎"),
        new OA\Property(property: "email", type: "string", format: "email", nullable: true),
        new OA\Property(property: "created_at", type: "string", format: "date-time"),
    ],
)]
class UserSchema
{
}

以降は ref: "#/components/schemas/User" で参照して使い回します。

5. POST とリクエストボディ

#[OA\Post(
    path: "/api/users",
    summary: "ユーザーを新規作成する",
    tags: ["Users"],
    requestBody: new OA\RequestBody(
        required: true,
        content: new OA\JsonContent(
            required: ["name", "email"],
            properties: [
                new OA\Property(property: "name", type: "string", example: "山田太郎"),
                new OA\Property(property: "email", type: "string", format: "email"),
            ],
        ),
    ),
    responses: [
        new OA\Response(response: 201, description: "作成成功"),
        new OA\Response(response: 422, description: "バリデーションエラー"),
    ],
)]
public function store()
{
    // 実装
}

6. 認証(Bearer トークン)を宣言する

#[OA\SecurityScheme(
    securityScheme: "bearerAuth",
    type: "http",
    scheme: "bearer",
    bearerFormat: "JWT",
)]

エンドポイント側で有効化します。

#[OA\Get(
    path: "/api/me",
    summary: "自分の情報を取得する",
    security: [["bearerAuth" => []]],
    responses: [ new OA\Response(response: 200, description: "OK") ],
)]
public function me()
{
    // 実装
}

7. 定義ファイルを生成する

# YAML で出力
./vendor/bin/openapi app --output public/openapi.yaml

# JSON で出力
./vendor/bin/openapi app --output public/openapi.json --format json

プログラムから生成する場合は Generator を直接呼びます。

<?php

require __DIR__ . '/vendor/autoload.php';

$openapi = \OpenApi\Generator::scan([__DIR__ . '/app']);
file_put_contents(__DIR__ . '/public/openapi.yaml', $openapi->toYaml());

8. Swagger UI で表示する

docker run -p 8080:8080 \
  -e SWAGGER_JSON=/spec/openapi.yaml \
  -v $(pwd)/public:/spec \
  swaggerapi/swagger-ui

http://localhost:8080 を開けば、生成された仕様がインタラクティブに表示されます。

確認方法

  • 生成コマンドの終了コードが 0 であること
  • 出力された YAML/JSON が OpenAPI 3.x として妥当であること(Swagger Editor に貼るのが手軽)
  • Swagger UI 上で各エンドポイント・スキーマ・認証が意図通り表示されること

CIに組み込むなら、生成とバリデーションを1ステップにまとめておくと安全です。

./vendor/bin/openapi app --output /tmp/openapi.json --format json
# 生成物を lint / diff で検証する

注意点

  • 走査対象ディレクトリを絞る。vendor を含めると重く、無関係なアノテーションを拾うので app など必要なパスだけ指定する。
  • Info は必ず1つ。無いとエラーになる。
  • ref の綴りミスは気づきにくい。参照名を統一し、スキーマ名を機械的に管理すると事故が減る。
  • Attribute と docblock を混在させない。6.x 系では docblock(@OA\...)記法が既定で無効化されており、doctrine/annotations を入れずに残すと黙って無視される。新規なら Attribute に統一するのが素直。
  • 生成物の運用方針を決める。CIで毎回生成して配布するほうが陳腐化しにくい。

まとめ

swagger-php を使えば、実装コードのアノテーションを一次情報として OpenAPI 定義を機械生成できます。要点は3つです。

  1. Info を1つ用意し、エンドポイントに Attribute を付ける
  2. スキーマは OA\Schema として定義して ref で使い回す
  3. vendor/bin/openapi で生成し、CIで再生成・検証する

「ドキュメントをコードに寄せる」だけで、仕様と実装の乖離という慢性的な問題を構造的に解消できます。まずは1エンドポイントから始めてみてください。

\ 最新情報をチェック /

コメントを残す

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください。