Swagger (OpenAPI) と PHPStan で REST API でも型安全っぽく使う

C123355918153416d5db4c346140ddc8?s=47 kalibora
November 26, 2019

Swagger (OpenAPI) と PHPStan で REST API でも型安全っぽく使う

Swagger と swagger-php(+自作拡張)と swagger-codegen と phpstan を使ってやや型安全に開発をしている話。

C123355918153416d5db4c346140ddc8?s=128

kalibora

November 26, 2019
Tweet

Transcript

  1. Swagger (OpenAPI) と PHPStan で REST API でも型安全っぽく使う

  2. ⾃⼰紹介 ID: @kalibora 所属: 株式会社オトバンク オーディオブック(⽿で聴く本)を作ってる会社 サーバーサイド(API, batch, web ...

    )の開発をしております 今⽇話すのは API の開発をこういう感じでやってるけど、なかなかいいよ という共有の話です
  3. おさらい : Swagger とは RESTful Web サービスの設計、構築、⽂書化、使⽤に役⽴つツール で、⼤規模なエコシステムによってバックアップされたオープンソー スのソフトウェアフレームワーク。 で、その中にある

    Swagger Specification (Swagger 仕様)に関して は、 OpenAPI Specification に改名された。
  4. 実際に OpenAPI で仕様書書いてる⼈どれくら いいますか? swagger: "2.0" info: description: " こういうやつですね"

    version: "1.0.0" title: "Swagger Petstore" termsOfService: "http://swagger.io/terms/" contact: email: "apiteam@swagger.io" license: name: "Apache 2.0" url: "http://www.apache.org/licenses/LICENSE-2.0.html" host: "petstore.swagger.io" basePath: "/v2"
  5. ぶっちゃけ OpenAPI で仕様書くの、ダルくな いですか?

  6. OpenAPI 仕様を書くための⼿段 Swagger Editor ブラウザベースでOpenAPI を記述できるエディター https://editor.swagger.io/ 普通はこれを使う(のかな) swagger-php アノテーションベースでOpenAPI

    仕様を記述できる https://github.com/zircote/swagger-php ダルさを解消するために、これを拡張して使っております
  7. swagger-php (v2) の使⽤例 例えば User というクラスに name プロパティがあって、 これをAPI のレスポンスのI/F

    として定義したい場合、 /** * @SWG\Definition() */ class User { /** * ユーザー名 * @var string * @SWG\Property() */ public $name; }
  8. 出⼒結果 ↑ 先程のアノテーションを解析して、下記のようなOpenAPI 仕様が書 き出されます。 swagger: '2.0' definitions: User: properties:

    name: description: ユーザー名 type: string
  9. でもまだダルい

  10. まだダルポイントその 1 getter などのアクセサに対応していない 実際の業務で使っているエンティティクラスは private なプロパ ティ + getter

    の構成が多いのでこのままだとうまく使えない
  11. 対策 1: swagger-php を拡張して getter に対応した /** * @SWG\Definition() */

    class User { private $name; /** * ユーザー名 * @SWG\Property() */ public function getName() : string { return $this->name; } }
  12. 出⼒結果 先程とほぼ同じ結果のOpenAPI 仕様が書き出されるように拡張。 return type hint を使い、nullable ではないものは required に。

    swagger: '2.0' definitions: User: required: - name properties: name: description: ユーザー名 type: string
  13. でもやっぱりまだダルい

  14. まだダルポイントその 2 そもそもだけど、アノテーション書いたらそのままAPI の挙動にも 反映したい アノテーションから 仕様(OpenAPI 仕様の出⼒結果) 実装(実際にAPI のレスポンスとして返す値)

    この両⽅に影響を与えたい
  15. 対策 2: オブジェクトから配列に変換する処理を書いた /** * @SWG\Definition() */ class User {

    private $name; /** * ユーザー名 * @SWG\Property() */ public function getName() : string { return $this->name; } }
  16. 連想配列に変換 SwaggerSerializer という⾃前で実装したクラスが @SWG\Property() アノテーションを読み取り、連想配列にする。 $user = new User('Otobank Taro');

    var_dump($swaggerSerializer->serialize($user)); // [name => 'Otobank Taro'] これにより、 @SWG\Property() を定義した getter は API のレスポンス のI/F としても露出するし、実際にAPI のレスポンスとしても返却され るようにした。
  17. PHPStan と組み合わせる

  18. おさらい : PHPStan とは PHP の静的解析ツールの1 つ コードを実⾏せずとも分かる、いろんなエラーを検出してくれる そのためには型が重要になる

  19. 例えばこんなのは class Main { public function execute() : void {

    echo Util::getTomorrow(new \DateTimeImmutable('1980-01-01')) ->format('Y/m/d'), PHP_EOL; } } class Util { public static function getTomorrow(\DateTimeImmutable $today) : ?\DateTimeImmutable { // ノストラダムスの⼤予⾔によって世界が滅びるので明⽇は無い $endDayOfTheWorld = new \DateTimeImmutable('1999-07-31'); return ($today >= $endDayOfTheWorld) ? null : $today->modify('+1 day'); } }
  20. こうなる $ ./vendor/bin/phpstan analyse src --level=max 1/1 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100% ------

    -------------------------------------------------------- Line Main.php ------ -------------------------------------------------------- 4 Cannot call method format() on DateTimeImmutable|null. ------ -------------------------------------------------------- [ERROR] Found 1 error getTommorow() の戻り値が nullable なのに null チェックをしないで format() メソッドを使⽤しているため。
  21. と⾔うわけで、例えばこんなケース

  22. API 側のエンティティクラス class Campaign { /** * キャンペーン開始⽇時 * @SWG\Property()

    */ public function getStartedAt() : \DateTimeInterface { /* 実装は省略 */ } /** * キャンペーン終了⽇時 * @SWG\Property() */ public function getEndedAt() : ?\DateTimeInterface { /* 省略 */ } }
  23. 出⼒される OpenAPI 仕様書 swagger: '2.0' definitions: Campaign: required: - startedAt

    properties: startedAt: description: キャンペーン開始⽇時 type: string format: date-time endedAt: description: キャンペーン終了⽇時 type: string format: date-time x-nullable: true
  24. API を呼び出すクライアント側 OpenAPI 仕様から⾃動的にAPI クライアントを作るツールは⾊々あ るので好きなものを使えば良いと思う 今回 OpenAPI 仕様で required

    などを⽤い nullable か否かをきちん と出⼒するようにしたので、それを加味して⽣成するツールであれ ば良い 加味しない場合でもクラス⽣成のテンプレートをカスタマイズでき たりするので、それを使うことも可能 弊社では swagger-api/swagger-codegen + テンプレートをカスタマ イズして nullable かどうかも加味するようにしている
  25. ⾃動⽣成されたクラス class Campaign implements ModelInterface, ArrayAccess { /** * Gets

    started_at * @return \DateTime */ public function getStartedAt() { /* 実装は省略 */ } /** * Gets ended_at * @return \DateTime|null */ public function getEndedAt() { /* 実装は省略 */ } }
  26. PHPStan でのチェック クライアント側で⾃動⽣成されたクラスでも ちゃんと getEndedAt() が nullable になっているので、 API を呼び出す側の実装者が、うっかりキャンペーンの終了⽇時が

    nullable である。 という仕様を知らなかった(読み取り損ねた)としても、⾃動的にエ ラーを検出可能。
  27. まとめ

  28. まとめ(ツール) API 側 zircote/swagger-php getter にも適⽤する拡張(type hint で nullable も加味)

    アノテーションからオブジェクトを連想配列にする実装(仕様 と実装をあわせる) クライアント側 swagger-api/swagger-codegen テンプレートをちょっとカスタマイズ(nullable も加味) phpstan/phpstan
  29. まとめ(図)