【Laravel】Swagger-phpライブラリを使ったOPEN APIドキュメント生成

# Swagger-phpとは

ソースコードに記載したAnnotationsもしくはAttributesの内容を元に、RESTful APIのOPEN APIドキュメントを生成するライブラリです。

※Attributeを使用するにはPHP8.1以上であること。

[Swagger-PHP公式](https://zircote.github.io/swagger-php/ "Swagger-PHP")

# 環境

Laravel 9.35.0
PHP 8.1.2

# インストール
```shell
$ composer require zircote/swagger-php
```
# 準備

ベースとなるControllerに以下のAttributesを記述し、コントローラで使用できるように設定します。

```php
#[OA\Info(title: "My First API", version: "0.1")]
#[OA\Server(url: "http://localhost:8080")]
class Controller extends BaseController
{
    use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
}
```

**#[OA**から始まるのがAttributes記法です。

# サンプル実装

例として、入力した名前とsubクレームでユーザテーブルにデータを登録する、という割とシンプルなユーザ追加APIに対してAttributesで記述してみます。

```php
/**
 * Store a newly created resource in storage.
 *
 * @param  UserCreateRequest  $request
 * @return \Illuminate\Http\Response
 */
#[OA\Post(path: '/api/users', tags: ['User'])]
#[OA\RequestBody(content: [new OA\JsonContent(ref: Constant::SCHEMA_URL. 'UserCreateInput')])]
#[OA\Response(response: Response::HTTP_CREATED, description: 'The data', content: [new OA\JsonContent(ref: Constant::SCHEMA_URL. 'UserCreateOutput')])]
public function store(UserCreateRequest $request)
{
    $validated = $request->validated();

    $input = new UserCreateInput(
        $validated['cognito_sub'],
        $validated['name']
    );

    $res = $this->user_service->create($input);

    return response()->json($res->toArray());
}
```

1つずつ読み解いていきます。

まず以下の記述で、どのメソッドでどのURLかを定義します。

```php
#[OA\Post(path: '/api/users', tags: ['User'])]
```

pathは該当APIのリクエストルート、tagsはOPEN APIドキュメントにおいて分類分けに使用します。

続いて、リクエストボディ用にスキーマを定義します。

```php
#[OA\RequestBody(content: [new OA\JsonContent(ref: Constant::SCHEMA_URL. 'UserCreateInput')])]
```

contentにはリクエストボディの具体的な内容を定義します。

上記ではJsonContent内でrefを使用していますが、使用しない場合の記述は以下の通りになります。

```php
#[OA\RequestBody(content: [new OA\JsonContent(required: ['cognito_sub', 'name'], properties: [new OA\Property(property: 'cognito_sub', type:'string', description:'cognito_sub'), new OA\Property(property: 'name', type:'string', description:'ユーザ名')])])]
```

refを使用することで、別途作成したスキーマを参照することができます。

そのスキーマの内容は以下の通りです。


```php
#[OA\Schema(required: ['cognito_sub', 'name'])]
final class UserCreateInput extends CreateInput
{
    #[OA\Property(description: 'cognito_sub')]
    protected string $cognito_sub;

    #[OA\Property(description: 'ユーザ名')]
    protected string $name;

    /**
     * @param string $cognito_sub
     * @param string $name
     */
    public function __construct(string $cognito_sub, string $name)
    {
        $this->cognito_sub    = $cognito_sub;
        $this->name           = $name;
    }
}
```

スキーマ名は明示的に定義しない場合、クラス名と同じになります。cognito_sub、nameプロパティに対して、requiredの設定をしています。

型については、明示的に定義しない場合、各プロパティに定義してある（ここではstring）の型がスキーマの各プロパティに反映されます。


最後に、レスポンス内容に関してもスキーマ定義します。

```php
#[OA\Response(response: Response::HTTP_CREATED, description: 'The data', content: [new OA\JsonContent(ref: Constant::SCHEMA_URL. 'UserCreateOutput')])]
```

contentにはレスポンスの具体的な値を定義します。

ここでも、refを使用することで、別途作成したスキーマを参照することができます。

スキーマの内容は以下の通りです。

```php
#[OA\Schema(required: ['id', 'cognito_sub', 'name', 'profile', 'icon'])]
final class UserCreateOutput
{
    #[OA\Property(description: 'ユーザID')]
    private int $id;

    #[OA\Property(description: 'cognito_sub')]
    private string $cognito_sub;

    #[OA\Property(description: 'ユーザ名')]
    private string $name;

    #[OA\Property(description: '自己紹介')]
    private string $profile;

    #[OA\Property(description: 'ユーザアイコン')]
    private string $icon;


    /**
     * @param int $id
     * @param string $cognito_sub
     * @param string $name
     * @param string $profile
     * @param string $icon
     */
    public function __construct(int $id, string $cognito_sub, string $name, string $profile, string $icon)
    {
        $this->id             = $id;
        $this->cognito_sub    = $cognito_sub;
        $this->name           = $name;
        $this->profile        = $profile;
        $this->icon           = $icon;
    }

    /**
     * @return array
     */
    public function toArray(): array
    {
        return [
            'id'             => $this->id,
            'cognito_sub'    => $this->cognito_sub,
            'name'           => $this->name,
            'profile'        => $this->profile,
            'icon'           => $this->icon,
        ];
    }
}
```

以下のコマンドでOPEN APIドキュメントを生成します。

```shell
$ ./vendor/bin/openapi app -o storage/app/openapi.yaml
```
-o 以降は出力先のファイルパスをファイル名および拡張子込みで記載します。

コマンドを実行して生成されたopenapi.yamlの内容は以下の通りになります。

```yaml
info:
  title: "My First API"
  version: "0.1"
servers:
  - url: "http://localhost:8080"

... 省略 ...

paths:
	/api/users:
	    post:
	      tags:
	        - User
	      summary: 'Store a newly created resource in storage.'
	      operationId: be551c1d694a01c164966f58bfa77013
	      requestBody:
	        content:
	          application/json:
	            schema:
	              $ref: '#/components/schemas/UserCreateInput'
	      responses:
	        '201':
	          description: 'The data'
	          content:
	            application/json:
	              schema:
	                $ref: '#/components/schemas/UserCreateOutput'

... 省略 ...

components:
  schemas:
    UserCreateInput:
      required:
        - cognito_sub
        - name
      properties:
        cognito_sub:
          description: cognito_sub
          type: string
        name:
          description: ユーザ名
          type: string
      type: object

... 省略 ...

  UserCreateOutput:
      required:
        - id
        - cognito_sub
        - name
        - profile
        - icon
      properties:
        id:
          description: ユーザID
          type: integer
        cognito_sub:
          description: cognito_sub
          type: string
        name:
          description: ユーザ名
          type: string
        profile:
          description: 自己紹介
          type: string
        icon:
          description: ユーザアイコン
          type: string
      type: object
```

Swagger UIで見たときに以下のように表示されています。
![localhost_8002_ (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3112812/fc829508-b63a-20ab-f784-4f325c817a36.png)

Swagger UIでもちゃんと表示されていることがわかります。

# Annotationsで記述すると

上記のユーザ追加APIをAnnotationsで置き換えた場合以下の通りになります。

```php
/**
 * Store a newly created resource in storage.
 *
 * @param  UserCreateRequest  $request
 * @return \Illuminate\Http\Response
 *
 * @OA\Post(
 *     path="/api/users",
 *     tags={"User"},
 *     @OA\RequestBody(
 *         required=true,
 *         @OA\JsonContent(type="object",ref="#/components/schemas/UserCreateInput")
 *     ),
 *     @OA\Response(
 *         response=\Symfony\Component\HttpFoundation\Response::HTTP_CREATED,
 *         description="The data",
 *         @OA\JsonContent(type="object",ref="#/components/schemas/UserCreateOutput")
 *      )
 *   )
 * )
 */
public function store(UserCreateRequest $request)
{

... 省略 ...

}
```

リクエストボディのスキーマ
```php
/**
 * @OA\Schema(
 *     required={"cognito_sub", "name"}
 * )
 */
final class UserCreateInput extends CreateInput
{
    /**
     * @OA\Property(
     *      property="cognito_sub",
     *      description="cognito_sub",
     * )
     */
    protected string $cognito_sub;

    /**
     * @OA\Property(
     *      property="name",
     *      description="ユーザ名",
     * )
     */
    protected string $name;

    /**
     * @param string $cognito_sub
     * @param string $name
     */
    public function __construct(string $cognito_sub, string $name)
    {
        $this->cognito_sub    = $cognito_sub;
        $this->name           = $name;
    }

... 省略 ..

}
```

レスポンス用のスキーマ
```php
/**
 * @OA\Schema(
 *     required={"id", "cognito_sub", "name", "profile", "icon"}
 * )
 */
final class UserCreateOutput
{
    /**
     * @OA\Property(
     *      description="ユーザID",
     * )
     */
    private int $id;

    /**
     * @OA\Property(
     *      description="cognito_sub",
     * )
     */
    private string $cognito_sub;

    /**
     * @OA\Property(
     *      description="ユーザ名",
     * )
     */
    private string $name;

    /**
     * @OA\Property(
     *      description="自己紹介",
     * )
     */
    private string $profile;

    /**
     * @OA\Property(
     *      description="ユーザアイコン",
     * )
     */
    private string $icon;


    /**
     * @param int $id
     * @param string $cognito_sub
     * @param string $name
     * @param string $profile
     * @param string $icon
     */
    public function __construct(int $id, string $cognito_sub, string $name, string $profile, string $icon)
    {
        $this->id             = $id;
        $this->cognito_sub    = $cognito_sub;
        $this->name           = $name;
        $this->profile        = $profile;
        $this->icon           = $icon;
    }

... 省略 ...

}
```

どちらも生成されるOPEN APIドキュメントに差異は無いですが、Attributesで記述する方が補完を効かせることができるので、個人的にはおすすめです。

# その他
パスパラメータ、クエリパラメータを持つAPIの場合はRequestBodyではなく以下の通りで定義します。

パスパラメータ（api/users/{id}のようなケース）
```php
#[OA\PathParameter(required:true, name:'id', schema: new OA\Schema(type:'integer'))]
```

クエリパラメータ（api/users?page=1のようなケース）
```php
#[OA\Parameter(in:"query", required:true, name:'page', description:'ページ番号', schema: new OA\Schema(type:'integer'))]
```