ローカルで開発中にUI上からAPIを実行して結果をjson形式で見たいと思ってLaravelプロジェクトで実現した件をまとめておきます。
Laravelは5.5を使っています。
最終的にはこういう事ができます
必要な作業
必要な対応は下記の通りです。
- swagger-uiの環境を作る
- swagger-phpを使ってopenapi.yamlを自動生成できるようにする
- Cross Originなリクエストをswagger-uiから実行可能にする
- swagger-uiからのリクエストの時はレスポンスをjsonで返す
- swagger-uiからのリクエストの時は認証を無効にする
- swagger-uiからのPOSTリクエスト時のトークンチェックを無効にする
swagger-uiの環境を作る
swager-ui用の階層をプロジェクトのrootに作って下記のファイルを作ります。
ポートは衝突しない何かしらを選んで指定します。
swagger-ui:
image: swaggerapi/swagger-ui
container_name: "swagger-ui"
ports:
- "8001:8080"
volumes:
- ./openapi.yaml:/openapi.yaml
environment:
SWAGGER_JSON: /openapi.yaml
SWAGGER_JSON という環境変数で、apiを定義するファイルを指定します。
openapi.yamlは同階層に作成し、マウントしておきます。
swagger-phpを使ってopenapi.yamlを自動生成できるようにする
openapi.yamlを手で書くこともできますが、コントローラに記述したPHPDocから自動生成するツールがあるのでそれを使います。どの道何かしらを追記する必要がありますが、編集対象をphpファイルに統一できるのでこれを使います。
下記の手順でパッケージを追加します。
composer require zircote/swagger-php
これで生成コマンドが使えるようになるので、Makefileに生成コマンドを作っておきます。
実行するとプロジェクト内のPHPDocを見てopenapi.yamlが生成されます。
build: ## openapiの定義ファイルを更新する
./vendor/bin/openapi app -o ./swagger-ui/
swagger-phpの記法については下記URLを参考にします。
http://zircote.com/swagger-php
GETリクエストの例
/**
* @OA\Get(
* path="/sample",
* tags={"SampleController"},
* @OA\Parameter(name="some_id",
* in="query",
* description="何かしらのid",
* @OA\Schema(type="integer")
* ),
* @OA\Response(
* response="200",
* description="レスポンス内容の説明"
* )
* )
*/
POSTリクエストの例
/**
* @OA\Post(
* path="/sample/register",
* tags={"SampleController"},
* description="何かしらの登録処理",
* @OA\RequestBody(
* required=true,
* @OA\MediaType(
* mediaType="application/x-www-form-urlencoded",
* @OA\Schema(
* type="object",
* @OA\Property(
* property="name",
* description="登録する何かの名前",
* type="string"
* ),
* @OA\Property(
* property="user_id",
* description="ユーザid",
* type="integer"
* )
* )
* )
* ),
* @OA\Response(
* response="200",
* description="レスポンス内容の説明",
* )
* )
*/
Cross Originなリクエストをswagger-uiから実行可能にする
異なるhostからのリクエストはデフォルトでは禁じられているため、ローカルでかつswagger-uiからのアクセスのみ許可する設定をします。
下記のページにあるように、許可するhostを指定する必要があります。
https://fetch.spec.whatwg.org/#http-cors-protocol
Middlewareとして下記のクラスを追加します。
<?php
namespace App\Http\Middleware;
use Closure;
class Cors
{
// from swagger-ui
const ACCESS_ALLOW_ORIGIN = 'http://localhost:8001';
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
$response = $next($request);
if (app()->isLocal()) {
$response
->header("Access-Control-Allow-Origin" , self::ACCESS_ALLOW_ORIGIN)
->header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
}
return $response;
}
}
swagger-uiからのリクエストの時はレスポンスをjsonで返す
これでswagger-uiでAPIの一覧を見れるようになりました。
ただし、このままだとレスポンスがhtmlになってしまうので、形式をjsonに変更します。
LaravelはデフォルトでBaseControllerを継承していますが、その間に一つ中間クラスを作ってそこに共通処理を書いていきます。
まずはswagger-uiからのリクエスト判定です。
ローカルからのアクセスであることと、headerの中のアクセスoriginを取得して判定します。
use Illuminate\Routing\Controller as BaseController;
use App\Http\Middleware\Cors;
class Controller extends BaseController
{
protected function isSwagger() {
return app()->isLocal() && request()->headers->get('origin') === Cors::ACCESS_ALLOW_ORIGIN;
}
次にこれを使ってresponseの形式を変換します。
実装に手を入れることになり嫌な気持ちがありますが、responseをviewのパラメータを渡す処理をラップする関数を作り、Controllerの記述を書き換えます。
protected function response($view, $response) {
if ($this->isSwagger()) {
return response()->json($response);
}
return view($view)->with($response);
}
これでswagger-uiからのアクセス時にのみresponse形式をjsonにすることができました。
他にうまい方法があればそうしたいところ。
swagger-uiからのリクエストの時は認証を無効にする
何かしらの仕組みで認証を使っている場合は無効にしないとswagger-ui上から突破するのは困難です。
これは認証の使い方次第ですが、標準搭載されているMiddlewareを使っている場合は下記のように対応することで無効化できます。
public function __construct()
{
if (!$this->isSwagger()) {
$this->middleware('auth')->only(['getUpdate', 'postRegister', 'postUpdate']);
}
}
swagger-uiからのPOSTリクエスト時のトークンチェックを無効にする
ここまででGETリクエスウトについては動くようになりますが、まだPOSTリクエスト時にのCSRFトークンのチェックに引っかかります。
これもswagger-ui上から適切なtokenを送るのは困難であるため無効化します。
VerifyCsrfToken.phpというファイルがあるので、handleをオーバーライドし、判定条件を変えます。
UnitTest時に無効にしてるのと挙動としては近いですね。
isSwaggerと同じ条件を書いてしまっているのは良くないですが、雑に作るとこうなります。
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
class VerifyCsrfToken extends Middleware
{
/**
* @return bool
*/
protected function runningSwaggerUi()
{
return app()->isLocal() && request()->headers->get('origin') === Cors::ACCESS_ALLOW_ORIGIN;
}
/**
* @param \Illuminate\Http\Request $request
* @param Closure $next
* @return mixed|\Symfony\Component\HttpFoundation\Response
* @throws TokenMismatchException
*/
public function handle($request, Closure $next)
{
if (
$this->runningSwaggerUi() ||
$this->isReading($request) ||
$this->runningUnitTests() ||
$this->inExceptArray($request) ||
$this->tokensMatch($request)
) {
return $this->addCookieToResponse($request, $next($request));
}
throw new TokenMismatchException;
}
最後に
判定処理が共通化できてないのと、Corsクラスの定数に依存してる辺りはなんとかしたいところです。その辺りは次の課題ということで。いい方法知ってる人がいたら教えてほしいです。
とりあえずswagger-uiからAPIを実行してjsonで結果を見る環境はこれでできました。