こんにちは。AI・IoTサービス開発部の青木です。
Laravelの最新バージョンに追いつけていない系エンジニアです。 今回はOpenAPIの導入をLaravelでやっていきたいと思います。
序盤はドキュメントをなぞるような形となってしまいますが、後半はそれらをGitLab CIでPagesに登録し マージされたらAPIリファレンスが自動更新されるような仕組みを作りたいと思います。
利用するOSS
今回メインで利用させて頂くOSSはこちらです。
providers
として登録し、スキーマの定義を行えば自動でスキーマを出力してくれる便利なツールとなります。
導入
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の定義を行います。 example
や description
などプロパティが用意されているのでそれに沿って実装します。
どのようなSchemaが利用できるかは以下のリポジトリを参照頂ければと思います。
若干面倒ではありますが、APIリファレンスを1から作成することを考えるとこちらのほうが楽に実装できますし テスト時にこれらのSchemaを利用することで実際のレスポンス内容が一致しているかなどが確認しやすくなります。
Let's Generate
php artisan openapi:generate
yamlファイルが生成されますので、それらをお好みのViewerで表示してみます。
今回は Redoc を利用します。
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まで実行されていることがわかります。
最後に
弊社ではマイクロサービスアーキテクチャを採用しているのでこのAPIサーバだけでなく、複数のサーバが混在して成り立っています。 このような場合にOpenAPIのスキーマが存在すると OpenAPI Generator などを利用して爆速開発することも可能となります。
OpenAPI生活... 悪くないね。
では、また今度。
オプティムでは、一緒に働く仲間を募集しています。興味のある方は、こちらをご覧ください。