2024-02-01

Laravelパッケージ「Laravel OpenAPI Validator」 - OpenAPIドキュメントによる透過的バリデーション

Laravel用パッケージ Laravel OpenAPI Validator をバージョン1.0.0としpackagistへ公開した。

GitHub - KentarouTakeda/laravel-openapi-validator: Request and response validators based on the OpenAPI Specification.
GitHub - KentarouTakeda/laravel-openapi-validator: Request and response validators based on the OpenAPI Specification.
Request and response validators based on the OpenAPI Specification. - KentarouTakeda/laravel-openapi-validator

この記事では、1.0.0時点での機能の紹介と共に、それぞれの機能の実装意図や設計思想に簡単に触れたい。

概要

OpenAPIドキュメントによるリクエストやレスポンスのバリデーションを行うLaravelパッケージ。

  • Laravel OpenAPI又はL5 Swagger導入済の場合、ゼロコンフィグで導入可能。
    • それ以外の場合も、十数行のコードで統合が可能。
  • バリデーションの対象やレベル、違反時の挙動をルート毎に設定可能。
  • 開発効率の向上を目的とした、豊富なログとそのカスタマイズ。
  • オプション機能として、Swagger UI でのAPIの表示に対応。

simpleではなくeasyに寄せた多機能なライブラリとした。理由はこの後で述べる。

導入手順

  1. パッケージのインストール

    1
    composer require kentaroutakeda/laravel-openapi-validator
  2. OpenAPIドキュメントの読み込み設定

    • Laravel OpenAPIを使っている場合
      何もしなくて良い

    • L5 Swaggerを使っている場合

      .env
      1
      OPENAPI_VALIDATOR_PROVIDER="l5-swagger"
    • (それ以外の方法は割愛)

  3. ミドルウェアをルートにアタッチ

    • 個別にアタッチする場合

      routes/api.php
      1
      2
      Route::resource('users', UserController::class) // 特定のルートへ
      ->middleware(OpenApiValidator::class); // アタッチ
    • グループ全体にアタッチする場合

      app/Providers/RouteServiceProvider.php
      1
      2
      3
      4
      Route::middleware('api')                // APIルート全体へ
      ->middleware(OpenApiValidator::class); // アタッチ
      ->prefix('api')
      ->group(base_path('routes/api.php'));
  4. 必要に応じてカスタマイズ(抜粋・環境変数を使う例)

    • OPENAPI_VALIDATOR_RESPOND_WITH_ERROR_ON_RESPONSE_VALIDATION_FAILURE:

      • レスポンスバリデーションの失敗をエラーとするか?
        デフォルト: APP_DEBUG に従う
    • OPENAPI_VALIDATOR_INCLUDE_RES_ERROR_IN_RESPONSE:

      • レスポンスバリデーションに失敗した場合、エラー情報をレスポンスに含めるか?
        デフォルト: APP_DEBUG に従う
    • OPENAPI_VALIDATOR_REQUEST_ERROR_LOG_LEVEL:

      • リクエストバリデーション失敗のログレベル
        デフォルト: info
    • OPENAPI_VALIDATOR_RESPONSE_ERROR_LOG_LEVEL:

      • レスポンスバリデーション失敗のログレベル
        デフォルト: warning

カスタマイズの実施は任意なので、導入は実質「.envへ1行追加」「ミドルウェアのアタッチ」のみ行えば良い。デバッグモードの場合はリクエストとレスポンスの両方が、プロダクションモードの場合リクエストがOpenAPIドキュメントに従ってバリデーションされる。

開発ライフサイクルに応じた設定

Laravel OpenAPI Validatorには上に抜粋した以外にも多くの設定が用意されている、多くの項目のデフォルト値が APP_DEBUG に従う となっている通り、開発環境での利用、とかく 「開発効率」を強く意識した設計 となっている。

具体的に、どのような設定をどういった状況で利用すべきか、幾つかの例を紹介する。

レスポンスバリデーションの有無

類似するライブラリの多くはレスポンスバリデーションを行わない。本来なら返却できたはずのレスポンスなのにバリデーションのおかげでエラー扱いされては本末転倒、この考え方には一理ある。

だが、Laravel OpenAPI Validatorでは敢えて デフォルト: APP_DEBUG に従う としている。多くの開発者のローカル環境、テスト用の検証環境、これらではレスポンスの型に少しでも誤りがある場合、500エラーで自らをクラッシュさせる。

そのように設計した理由を、過去の和田卓人氏の講演(原典は達人プログラマー)から引用する。

ヒント32: 早めにクラッシュさせること

何らかの疑いがあるのであれば、どのような場合でも速やかに停止させるべき。 通常の場合、障害を抱えて中途半端に動いているプログラムよりも死んだプログラムのほうがダメージは少ない

PHP7 で堅牢なコードを書く - 例外処理、表明プログラミング、契約による設計 / PHP Conference 2016
PHP7 で堅牢なコードを書く - 例外処理、表明プログラミング、契約による設計 / PHP Conference 2016
2016/11/03 @ PHPカンファレンス2016 2016/12/15 @ PHPカンファレンス2016再演イベントにて改訂 2017/06/10 @ PHPカンファレンス福岡2017にて改訂 2017/...

サーバ単独で見て動作可能だったとしても、それが原因でフロントエンドをクラッシュさせる可能性が存在する以上、システム全体としては「障害を抱えて中途半端に動いている」状態だ。ネットワーク境界を超えた “fail fast” と言えるだろう。

それを踏まえ、開発中は、フロントエンドの担当者にはこのように伝えておくと良い。

  • 200番台のステータスコードで仕様外のレスポンスを返却することは、決してありません。

  • 400だった場合は、フロントの実装に誤りがあります。自分のコードを見直してください。

  • 500だった場合は、原因はバックエンドです。レスポンスにデバッグ情報が含まれるので、それを下さい。

当たり前の取り決めだが、これが意外と成立しないのだ。現場で個別に遭遇する事例として、例えば次のようなものがある。

  • フロントの不具合によるサーバエラー

    本来は400だがバリデーションの実装漏れにより処理の内部で型エラー。例えば次のコードだけで発生する。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    #[Test]
    public function test()
    {
    Route::get(
    '/{id}', // パスパラメータ `id` を入力する
    fn (int $id) => $id // `id` は数値を期待している(intにキャストされる)
    );

    // 数値を期待するパラメータに文字列を入力
    $this->get('/foo'); // Argument #1 ($id) must be of type int, string given
    }
  • サーバの不具合によるフロントのエラー

    • サーバはnot nullであるべきプロパティへ誤ってnullを返却。
    • フロントは仕様上のnot nullを信じ、例えば response.data.foo.bar.toFixed() と言った動作。

    APIは正常レスポンス、エラーの出処も一見するとフロントエンドだが、原因はサーバ側にある。

何れも「凡ミス」の類だが、ひと度ネットワーク境界(または担当者)を跨ぐとこんな凡ミスにも特定に多くの時間を要する。そして厄介なのが、これが常態化すると、400 Bad Request / 500 Internal Server Error などエラーコードが一切信じられなくなり、あらゆる不具合をあらゆる観点から調査する必要が生じてくる点だ。

本来あるべき形を再掲する。

  • 200番台のステータスコードで仕様外のレスポンスを返却することは、決してありません。

  • 400だった場合は、フロントの実装に誤りがあります。自分のコードを見直してください。

  • 500だった場合は、原因はバックエンドです。レスポンスにデバッグ情報が含まれるので、それを下さい。

この約束を「プログラマーの注意と努力」ではなく「ツールの機械的な判断」で実現する。これがLaravel OpenAPI Validatorの設計思想だ。

本番環境での運用

ここまでで述べた厳しい制限を、本番環境に課していいかは、時と場合によって変わるかもしれない。

「正当性」という観点では維持すべきだ。まだ安定していない新規開発のシステムであれば、敢えて厳し目の条件でサービスを公開しフィールドテストしてしまうやり方もあるかもしれない。

そして逆に、長期間運用し安定動作を確認できた場合、正当性よりスループットを優先しバリデーションを外すのも有効かもしれない。

しかし何れのやり方も、既存のレガシーシステムに後からOpenAPIを導入するような状況だった場合、現実的には不可能だろう。

バリデーションの要否や重要性は、プロダクトのライフサイクルや時々の開発状況に応じて変わる。

easyに寄せた多機能なライブラリ と前述したが、そう設計した理由がここにある。ツールのユースケースとして、時々に応じて設定をすぐさま変えられる必要があると考えた。

ログとログレベル

単なるバリデーションに対しログ機能まで持たせたこと、Laravel OpenAPI Validator独自のログレベルを持てること、リクエストとレスポンスとで別個に設定可能としたこと、これらも前項と同じ理由だ。

例えば「レスポンスバリデーションを行うか?」「違反した時にどう振る舞うか?」は、フェーズ毎に次のように使い分けると良いだろう。

フェーズ バリデーション クラッシュ ログ
1. 開発 -
2. 結合テスト info
3. リリース(初期) - warn
4. リリース(安定) - - -

リクエストバリデーションに違反した際のログレベルは、例えば次のような考え方と設定になる。

対向(クライアント) ログレベル 備考
ブラウザ(リリース後) info リクエスト改竄の可能性があるため warn にできない
ブラウザ(結合テスト) warn テスト中はフロントのバグもサーバのログに出力する
サーバ(BFFなど) warn リクエスト改竄の余地が無いため対向のバグもアラート可能

これらを設定した上で、CloudWatch logsなりSentryなりにダッシュボードやアラームを設定すれば良い。

以上の通り、エラー有無とログレベルの組み合わせで、一通りのユースケースに対応できるだろう。

余談: 作成の顛末とPHPerKaigi

Laravel OpenAPI Validatorは元々、PHPerKaigi 2024 のレギュラートーク資料に使うサンプルコードとして書き始めたものだった。

登壇資料にせよブログ記事にせよ、掲載するサンプルコードは「すぐに転用可能な、確実に動作するコード」を心がけている。実コードと自然と近づくよう実務と同じエディタを使うし、動作するか不安な時はテストまで書く。

そうやって書き進めていたら、ライブラリとして公開できる程度に機能の揃ったコードが出来上がってしまった。必然的に、登壇で紹介する予定の開発エッセンスが凝縮された設計となった。

これを登壇資料に留めてしまうには惜しいと考えた。これが作成の顛末である。

この記事では折に触れて設計と開発生産性との関係に触れた。これらは、Laravel OpenAPI Validatorの設計思想であると同時に、PHPerKaigi 2024のトークでの主要なテーマでもある。興味を持たれた方は、3月8日、中野セントラルパークカンファレンス、ぜひご覧いただきたい。

設計やLaravel OpenAPI Validator自体に対するフィードバックも心待ちにしている。何かしらの形でトークに反映されると思う。実は、登壇資料よりも前にライブラリやこの記事を公開してしまい、本編であるべき登壇のネタに少し困り始めている。