Laravelで作る、OpenAPIによるAPIリファレンス自動生成 with GitLab CI

こんにちは。AI・IoTサービス開発部の青木です。

Laravelの最新バージョンに追いつけていない系エンジニアです。 今回はOpenAPIの導入をLaravelでやっていきたいと思います。

序盤はドキュメントをなぞるような形となってしまいますが、後半はそれらをGitLab CIでPagesに登録し マージされたらAPIリファレンスが自動更新されるような仕組みを作りたいと思います。

利用するOSS

laravel.com

vyuldashev.github.io

今回メインで利用させて頂くOSSはこちらです。

providersとして登録し、スキーマの定義を行えば自動でスキーマを出力してくれる便利なツールとなります。

導入

vyuldashev.github.io

Installation

You can install the package via composer:

composer require vyuldashev/laravel-openapi

The service provider will automatically get registered. Or you may manually add the service provider in your config/app.php file:

'providers' => [
   // ...
    Vyuldashev\LaravelOpenApi\OpenApiServiceProvider::class,
];

You can publish the config file with:

php artisan vendor:publish --provider="Vyuldashev\LaravelOpenApi\OpenApiServiceProvider" --tag="openapi-config"

OSSのドキュメントで丁寧に解説されているためこちらを引用いたしました。

基本的に3STEPで導入可能で、既存のプロジェクトにも簡単に導入できるような内容となっています。

OpenAPI Generate は以下のコマンドでおこないます。

php artisan openapi:generate

API EndpointをControllerから読み取る

このライブラリは基本的にController(あるいはRouter)のメソッドに属性を追加することでそれらを読み取ってOpenAPIとして出力してくれます。 簡単なControllerを作成してみます。

<?php
use Vyuldashev\LaravelOpenApi\Attributes as OpenApi;

#[OpenApi\PathItem]
class UserController extends Controller
{
    /**
     * Create new user.
     *
     * Creates new user or returns already existing user by email.
     */
     #[OpenApi\Operation]
    public function store(Request $request)
    {
        //
    }
}

#[OpenApi\PathItem]

Classの属性とすることでOpenAPI化の対象となります。

#[OpenApi\Operation]

RouterのPathを調べ、OpenAPIのPathとして定義されます。 ここにtagやHTTPメソッドの定義などの行えます。メソッドはRouterから読み取ってくれるので基本的には記述不要です。

API EndpointのRequestBody,ResponseBody等のスキーマ定義を行う

このライブラリではスキーマは別ファイルで定義しなければなりません。 ここではわかりやすく Response Request Schema という3つの観点をもったディレクトリ構成にしてみます。

app/OpenApi/
├── RequestBodies
│   └── CreateUserRequestBody.php
├── Responses
│   ├── HealthCheckResponse.php
│   ├── UserResponse.php
└── Schemas
    ├── HealthCheckSchema.php
    └── UserSchema.php

自動でModelまで作成してくれるコマンドが用意されていますが、今回はそれを利用せずに実装します。

use Vyuldashev\LaravelOpenApi\Contracts\Reusable; を定義し、class UserSchema extends SchemaFactory implements Reusable のようにInterfaceとして利用しなければエラーとなります。ドキュメント上ではこの記載が無いため注意が必要です。

HealthCheckControllerの実装

HealthCheckとする理由は特にありませんが、サービスとして展開する際に必ず実装されるものですのでこちらをサンプルとして利用したいと思います。

routes/api.php

<?php
Route::get('/status',[HealthCheckController::class, 'status']);

app/Http/Controllers/HealthCheckController.php

<?php

#[OpenApi\PathItem]
class HealthCheckController extends Controller
{
    /**
     * HealthCheck Endpoint
     *
     * ヘルスチェックエンドポイント
     */
    #[OpenApi\Operation]
    #[OpenApi\Response(factory: HealthCheckResponse::class)]
    public function status():HealthCheckResource{
        $db = $this->upDatabase();
        $redis = $this->upRedis();
        return HealthCheckResource::make(collect(compact('db','redis')));
    }
}

まずはこのようにControllerを実装します。

HealthEndpointには以下のようなレスポンスが返却されるような作りとします。

{
  "version": "v0.0.1",
  "connection": {
    "db": "ok",
    "redis": "ok"
  },
  "status": "ok"
}

このJSONスキーマをOpenAPIとして出力されるようにします。

HTTP Response の定義

HealthCheckResponse.php

<?php

namespace App\OpenApi\Responses;

use App\OpenApi\Schemas\HealthCheckSchema;
use GoldSpecDigital\ObjectOrientedOAS\Objects\MediaType;
use GoldSpecDigital\ObjectOrientedOAS\Objects\Response;
use Vyuldashev\LaravelOpenApi\Factories\ResponseFactory;

class HealthCheckResponse extends ResponseFactory
{
    public function build(): Response
    {
        return Response::ok()->description('Successful response')->content(
            MediaType::json()->schema(HealthCheckSchema::ref())
        );
    }
}

ここではスキーマの定義を省略していることがわかるかと思います。 これはOpenAPIでもスキーマは別で定義しているように、ファイルごと分割することでOpenAPIに寄せた定義方法としています。

MediaType::json()->schema(HealthCheckSchema::ref())

もちろんですが、 schema(Schema::string('HealthCheck')) のように記述することで別ファイルにせずそのまま定義することも可能です。 場合によって使い分けるのが良いでしょう。

Schemaの定義

HealthCheckSchema.php

<?php

namespace App\OpenApi\Schemas;

use GoldSpecDigital\ObjectOrientedOAS\Contracts\SchemaContract;
use GoldSpecDigital\ObjectOrientedOAS\Objects\AllOf;
use GoldSpecDigital\ObjectOrientedOAS\Objects\AnyOf;
use GoldSpecDigital\ObjectOrientedOAS\Objects\Not;
use GoldSpecDigital\ObjectOrientedOAS\Objects\OneOf;
use GoldSpecDigital\ObjectOrientedOAS\Objects\Schema;
use Vyuldashev\LaravelOpenApi\Contracts\Reusable;
use Vyuldashev\LaravelOpenApi\Factories\SchemaFactory;

class HealthCheckSchema extends SchemaFactory implements Reusable
{
    /**
     * @return AllOf|OneOf|AnyOf|Not|Schema
     */
    public function build(): SchemaContract
    {
        return Schema::object('HealthCheck')
            ->properties(
                Schema::string('version')
                    ->description('APIのサーババージョン'),
                Schema::object('connection')->properties(
                    Schema::string('db')
                        ->description('Databaseの接続確認')
                        ->example('ok'),
                    Schema::string('redis')
                        ->description('Redisの接続確認')
                        ->example('ok')
                )->description('接続オブジェクト'),
                Schema::string('status')
                    ->description('総合ステータス どれか1つでもErrorの場合Errorとなる')
                    ->example('ok')
            );
    }
}

ここでSchemaの定義を行います。 exampledescription などプロパティが用意されているのでそれに沿って実装します。 どのようなSchemaが利用できるかは以下のリポジトリを参照頂ければと思います。

github.com

若干面倒ではありますが、APIリファレンスを1から作成することを考えるとこちらのほうが楽に実装できますし テスト時にこれらのSchemaを利用することで実際のレスポンス内容が一致しているかなどが確認しやすくなります。

Let's Generate

php artisan openapi:generate

yamlファイルが生成されますので、それらをお好みのViewerで表示してみます。

今回は Redoc を利用します。

github.com

https://cdn-ak.f.st-hatena.com/images/fotolife/o/optim-tech/20220106/20220106135514.png

GitLab Pages へ反映

Artisan コマンドを利用するために、GitLab CI上でPHPの構築を行う必要があります。 これは大変な作業なので、Dockerを利用して簡単にCI実装できるようにしてみます。

BaseとなるDockerfileの作成

PHPが実行できるBaseイメージを作成します。 Laravelの実行ができるように必要なパッケージのインストールやphp-fpmのHealthCheckなどができるように構築しておきます。

docker/base.docker

FROM php:8.0.10-fpm-alpine3.13

COPY --from=composer:latest /usr/bin/composer /usr/bin/composer

RUN apk update && apk add --no-cache \
  freetype-dev libjpeg-turbo-dev libpng-dev libmcrypt-dev fcgi \
  postgresql-dev git zip unzip tzdata \
  libmcrypt-dev libzip-dev libltdl oniguruma-dev \
  && docker-php-ext-install zip pdo pdo_pgsql mbstring gd iconv opcache \
  && docker-php-ext-configure zip \
  && apk del tzdata \
  && rm -rf /var/cache/apk/*
RUN pecl install redis \
  && docker-php-ext-enable redis
RUN wget -O /usr/local/bin/php-fpm-healthcheck \
  https://raw.githubusercontent.com/renatomefi/php-fpm-healthcheck/master/php-fpm-healthcheck \
  && chmod +x /usr/local/bin/php-fpm-healthcheck
RUN set -xe && echo "pm.status_path = /status" >> /usr/local/etc/php-fpm.d/zz-docker.conf

RUN { \
  echo 'short_open_tag = On'; \
  echo 'fastcgi.logging = 1'; \
  echo 'opcache.enable=1'; \
  echo 'opcache.optimization_level=0x7FFFBBFF' ; \
  echo 'opcache.revalidate_freq=0'; \
  echo 'opcache.validate_timestamps=1'; \
  echo 'opcache.memory_consumption=128'; \
  echo 'opcache.interned_strings_buffer=8'; \
  echo 'opcache.max_accelerated_files=4000'; \
  echo 'opcache.revalidate_freq=60'; \
  echo 'opcache.fast_shutdown=1'; \
  echo 'xdebug.remote_enable=0'; \
} > /usr/local/etc/php/conf.d/overrides.ini

EXPOSE 9000

このDockerfileでBuildしたイメージはアプリケーションの実行時にも利用できるものとなります。 ソースコード内からCOPYなどを記述しないことでBuildCacheを活かし、Dockerfileの肥大化を防ぎます。

BaseイメージのBuildをGitLab CIで実装

詳細は省きますが、Dockerfileのビルドを行い生成されたイメージをGitLab Container Registry に格納しています。

.gitlab-ci.yml

base:package:
  image: docker
  stage: cache
  script:
    - docker login -u ${CI_REGISTRY_USER} -p ${CI_REGISTRY_PASSWORD} ${CI_REGISTRY}
    - docker pull $BASE_IMAGE_NAME || true
    - docker build --cache-from $BASE_IMAGE_NAME -t $BASE_IMAGE_NAME -f docker/base.docker .
    - docker push $BASE_IMAGE_NAME

Baseイメージを利用し、OpenAPI Generateを実行する

OpenAPI Generateにはマイグレーションが必要なので、PostgreSQLも実行されるように定義します。 rules に対象のブランチを指定しています。また、artifactsに生成されたOpenAPIのファイルを格納しています。

.gitlab-ci.yml

oepn-api:generate:
  image: $BASE_IMAGE_NAME
  stage: running
  services:
    - postgres:12.2-alpine
  script:
    - mv .env.example .env
    - export DB_HOST=postgres
    - composer install --prefer-dist --no-progress --ansi
    - php artisan key:generate
    - php artisan migrate
    - php artisan openapi:generate >api.yaml
  artifacts:
    paths:
      - api.yaml
    expire_in: 1 days
  rules:
    - if: '$CI_COMMIT_BRANCH == "develop"'
      allow_failure: true
      when: on_success
    - if: '$CI_COMMIT_BRANCH'
      when: manual
      allow_failure: true

ReDoc によりAPIリファレンス化し、Pagesに公開

SwaggerなどのViewerがありますが、今回はReDocを利用しHTMLファイルをGenerateしてPagesに公開します。 ReDocはredoc-cliを利用して簡単にBuildできます。

Pagesでは、 environment の記述でMergeRequest等に View App というリンクが表示されます。ここでどのような変更がされたかが簡単に確認ができるようになりますね。

.gitlab-ci.yml

oepn-api:bundle:
  needs: ["oepn-api:generate"]
  stage: build
  image: node:15.14.0-alpine3.10
  script:
    - npm install -g redoc-cli
    - mv public public_bk && mkdir public
    - redoc-cli bundle -o $(pwd)/public/index.html api.yaml
  artifacts:
    paths:
      - public
  rules:
    - if: '$CI_COMMIT_BRANCH == "develop"'
    - if: '$CI_COMMIT_BRANCH'
      when: on_success

pages:
  stage: deploy
  needs: ["oepn-api:bundle"]
  script:
    - echo 'Nothing to do...'
  artifacts:
    paths:
      - public
  rules:
    - if: '$CI_COMMIT_BRANCH == "develop"'
    - if: '$CI_COMMIT_BRANCH'
      when: on_success
  environment:
    name: openapi/${CI_PROJECT_NAME}-${CI_BUILD_REF_SLUG}
    url: ここにPagesでアクセスするURLを記述する

実際にCIを回しました。 他のJobが合って見づらくなっていますが、、BaseイメージのビルドからPagesまで実行されていることがわかります。

https://cdn-ak.f.st-hatena.com/images/fotolife/o/optim-tech/20220106/20220106135512.png

最後に

弊社ではマイクロサービスアーキテクチャを採用しているのでこのAPIサーバだけでなく、複数のサーバが混在して成り立っています。 このような場合にOpenAPIのスキーマが存在すると OpenAPI Generator などを利用して爆速開発することも可能となります。

OpenAPI生活... 悪くないね。

では、また今度。


オプティムでは、一緒に働く仲間を募集しています。興味のある方は、こちらをご覧ください。

www.optim.co.jp