👶

OpenAPI generatorとlaravelの組み合わせでインターフェースをいい感じに守る

2022/01/29に公開

概要

OpenAPI generator と laravel の組み合わせで
laravel の自由度を残しながらいい感じにインターフェースを守れたので
generator の設定とかをメモとして残しておきます。

バージョン

Laravel 8.55.0
OpenAPI 3.0.0

ディレクトリ構成

├── open_api_generator
│   ├── api.yaml
│   ├── config_php.json
│   └── generator_php.sh
└── project
    ├── app
    │   ├── Http
    │   │   └── Controllers
    │   │       ├── Api
    │   │       │   └── Master
    │   │       │       └── JobCategoryController.php
    │   │       └── Controller.php
    │   ├── Libs
    │   │   └── OpenAPIUtility.php
    │   ├── Models
    │   │   └── JobCategory.php
    └── routes
        └── api.php

https://github.com/ritogk/laravel-openapi-generator

OpenAPI

generator 都合でクエリパラメータ、リクエストボディ、レスポンス等はモデル化してあります。

api.yaml
openapi: 3.0.0
info:
  title: OpenAPI Tutorial
  description: OpenAPI Tutorial by halhorn
  version: 0.0.0
servers:
  - url: http://localhost:80/api
    description: 開発用
paths:
  /job_categories:
    get:
      tags:
        - "職種"
      summary: 一覧取得
      description: 詳細内容
      parameters:
        - in: query
          name: name
          schema:
            type: string
          description: 名称
          required: false
        - in: query
          name: content
          schema:
            type: string
          description: 内容
          required: false
      responses:
        "200":
          description: 職種一覧
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/jobCategory"
components:
  schemas:
    queryJobCategoryList:
      description: クエリパラメータ 職種一覧
      type: object
      properties:
        name:
          type: string
          description: 名称
        content:
          type: string
          description: 内容
    jobCategory:
      description: レスポンス 職種
      type: object
      properties:
        id:
          type: integer
          description: id
        name:
          type: string
          description: 名称
        content:
          type: string
          description: 内容
        image:
          type: string
          description: 画像URL
        sortNo:
          type: integer
          description: 並び順
        createdAt:
          type: string
          format: date
          description: 作成日時
        updatedAt:
          type: string
          format: date
          description: 更新日時

OpenAPI generator

ジェネレーターには laravel 用ではなく php 用を使います。
laravel 用はルートとコントローラに関するコードのみ生成されるだけでリクエスト、レスポンスの内容が oas に沿っているかどうかを保障するコードは生成されません。
また、コントローラとルートは手で書き換えたい部分でもあり、極力自動生成されたコードには手を加えたくないです。
laravel 用は使いにくいですね。。。

比べて php 用はリクエスト、レスポンスを oas に沿わせるコードが生成されるので、それを laravel に埋め込んで使っていきます。

config_php.json
{
    "invokerPackage": "App\\OpenAPI",
    "variableNamingConvention": "camel_case"
}
php_generator.sh
WORK_PATH='/out/php'
OUTPUT_PATH='../project/app/OpenAPI'

# コード生成
docker run --rm -v ${PWD}:/local openapitools/openapi-generator-cli generate -i /local/api.yaml -g php -o /local${WORK_PATH} -c /local/config_php.json

# enum型の定数名の頭に勝手につく文字を削除
grep -l 'NUMBER_' .${WORK_PATH}/lib/Model/* | xargs sed -i 's/NUMBER_//g'

# laravel側にコピー
mkdir -p ${OUTPUT_PATH}
cp -r .${WORK_PATH}/lib/* ${OUTPUT_PATH}
cp api.yaml ${OUTPUT_PATH}

# 作業ディレクトリ削除
rm -rf ./out

スクリプトを実行すると laravel 側にコードが生成されます。

Laravel

JobCategoryController.php
use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
// model
use App\Models\JobCategory;
// request
use App\Http\Requests\Master\JobCategoryListRequest;
// openapi
use App\OpenAPI;
use App\Libs\OpenAPIUtility;

class JobCategoryController  extends Controller
{
    /**
     * 職種 一覧取得
     *
     * @param  Request $request
     * @return JsonResponse
     */
    public function list(Request $request): JsonResponse
    {
        // oasのリクエストモデルに変換
        $parameters = new OpenAPI\Model\QueryJobCategoryList($request->all());

        // メインロジック
        $name = $parameters->getName();
        $content = $parameters->getContent();
        $items = JobCategory::when(isset($name), function ($query) use ($name) {
            return $query->where('name', 'like', "%$name%");
        })->when(isset($content), function ($query) use ($content) {
            return $query->where('content', 'like', "%$content%");
        })->orderBy('sort_no')->get()->toArray();

        # oasのレスポンスモデルに変換して返す。
        return response()->json(
            OpenAPIUtility::dicstionariesToModelContainers(OpenAPI\Model\JobCategory::class, $items),
            Response::HTTP_OK
        );
    }
}

やってる事はコントローラーに入ってくるリクエストと、レスポンスを oas に沿わせるようにしただけ。
この形なら生成されたコードも触らなくて良いはずです。

テスト

この oas モデルはテスト等でも大活躍します。
以下のような感じでモデルからリクエスト、レスポンスを生成するようにしておけば openapi 定義が変更されると静的チェックでエラーになるので修正が超簡単です。

/**
 * 正しいレスポンスが返ってくる事
 *
 * @return void
 */
public function test_正しいレスポンスが返ってくる事()
{
    // ダミデータを生成
    $job_category = JobCategory::factory()->create();

    // 返却されるはずのレスポンスディを生成
    $response_model = new OpenAPI\Model\JobCategory();
    $response_model->setName($job_category->name);
    $response_model->setContent($job_category->content);
    $response_model->setImage($job_category->image);
    $response_model->setImageUrl(Storage::url($job_category->image));
    $response_model->setSortNo($job_category->sort_no);
    $response_body = OpenAPIUtility::convertDict($response_model);

    /**
     * 確認項目
     */
    $response = $this->get(sprintf('/api/v1/job_categories/%s', $job_category->id));
    // レスポンスに想定する内容が含まれている事。
    $response->assertJson($response_body);
    // 正しいステータスコードが返ってくる事
    $response->assertStatus(200);
}

突っ込まれそうな事

■oas を変えるたびに laravel 側のルートとリクエストクラスを合わせるのがめんどくさい。
→ 頑張って合わせてください。

■oas のヘッダーとかトークン認証とか保障されてなくね?
→ されてません。Openapi で表現できる以上の事を laravel のルートで制御できるのでまあ良いかなあと。あくまで laravel が主。

GitHubで編集を提案

Discussion