Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

⾃⼰紹介 ID: @kalibora 所属: 株式会社オトバンク オーディオブック(⽿で聴く本)を作ってる会社 サーバーサイド(API, batch, web ... )の開発をしております 今⽇話すのは API の開発をこういう感じでやってるけど、なかなかいいよ という共有の話です

Slide 3

Slide 3 text

おさらい : Swagger とは RESTful Web サービスの設計、構築、⽂書化、使⽤に役⽴つツール で、⼤規模なエコシステムによってバックアップされたオープンソー スのソフトウェアフレームワーク。 で、その中にある Swagger Specification (Swagger 仕様)に関して は、 OpenAPI Specification に改名された。

Slide 4

Slide 4 text

実際に OpenAPI で仕様書書いてる⼈どれくら いいますか? swagger: "2.0" info: description: " こういうやつですね" version: "1.0.0" title: "Swagger Petstore" termsOfService: "http://swagger.io/terms/" contact: email: "[email protected]" license: name: "Apache 2.0" url: "http://www.apache.org/licenses/LICENSE-2.0.html" host: "petstore.swagger.io" basePath: "/v2"

Slide 5

Slide 5 text

ぶっちゃけ OpenAPI で仕様書くの、ダルくな いですか?

Slide 6

Slide 6 text

OpenAPI 仕様を書くための⼿段 Swagger Editor ブラウザベースでOpenAPI を記述できるエディター https://editor.swagger.io/ 普通はこれを使う(のかな) swagger-php アノテーションベースでOpenAPI 仕様を記述できる https://github.com/zircote/swagger-php ダルさを解消するために、これを拡張して使っております

Slide 7

Slide 7 text

swagger-php (v2) の使⽤例 例えば User というクラスに name プロパティがあって、 これをAPI のレスポンスのI/F として定義したい場合、 /** * @SWG\Definition() */ class User { /** * ユーザー名 * @var string * @SWG\Property() */ public $name; }

Slide 8

Slide 8 text

出⼒結果 ↑ 先程のアノテーションを解析して、下記のようなOpenAPI 仕様が書 き出されます。 swagger: '2.0' definitions: User: properties: name: description: ユーザー名 type: string

Slide 9

Slide 9 text

でもまだダルい

Slide 10

Slide 10 text

まだダルポイントその 1 getter などのアクセサに対応していない 実際の業務で使っているエンティティクラスは private なプロパ ティ + getter の構成が多いのでこのままだとうまく使えない

Slide 11

Slide 11 text

対策 1: swagger-php を拡張して getter に対応した /** * @SWG\Definition() */ class User { private $name; /** * ユーザー名 * @SWG\Property() */ public function getName() : string { return $this->name; } }

Slide 12

Slide 12 text

出⼒結果 先程とほぼ同じ結果のOpenAPI 仕様が書き出されるように拡張。 return type hint を使い、nullable ではないものは required に。 swagger: '2.0' definitions: User: required: - name properties: name: description: ユーザー名 type: string

Slide 13

Slide 13 text

でもやっぱりまだダルい

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

対策 2: オブジェクトから配列に変換する処理を書いた /** * @SWG\Definition() */ class User { private $name; /** * ユーザー名 * @SWG\Property() */ public function getName() : string { return $this->name; } }

Slide 16

Slide 16 text

連想配列に変換 SwaggerSerializer という⾃前で実装したクラスが @SWG\Property() アノテーションを読み取り、連想配列にする。 $user = new User('Otobank Taro'); var_dump($swaggerSerializer->serialize($user)); // [name => 'Otobank Taro'] これにより、 @SWG\Property() を定義した getter は API のレスポンス のI/F としても露出するし、実際にAPI のレスポンスとしても返却され るようにした。

Slide 17

Slide 17 text

PHPStan と組み合わせる

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

例えばこんなのは 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'); } }

Slide 20

Slide 20 text

こうなる $ ./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() メソッドを使⽤しているため。

Slide 21

Slide 21 text

と⾔うわけで、例えばこんなケース

Slide 22

Slide 22 text

API 側のエンティティクラス class Campaign { /** * キャンペーン開始⽇時 * @SWG\Property() */ public function getStartedAt() : \DateTimeInterface { /* 実装は省略 */ } /** * キャンペーン終了⽇時 * @SWG\Property() */ public function getEndedAt() : ?\DateTimeInterface { /* 省略 */ } }

Slide 23

Slide 23 text

出⼒される 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

Slide 24

Slide 24 text

API を呼び出すクライアント側 OpenAPI 仕様から⾃動的にAPI クライアントを作るツールは⾊々あ るので好きなものを使えば良いと思う 今回 OpenAPI 仕様で required などを⽤い nullable か否かをきちん と出⼒するようにしたので、それを加味して⽣成するツールであれ ば良い 加味しない場合でもクラス⽣成のテンプレートをカスタマイズでき たりするので、それを使うことも可能 弊社では swagger-api/swagger-codegen + テンプレートをカスタマ イズして nullable かどうかも加味するようにしている

Slide 25

Slide 25 text

⾃動⽣成されたクラス class Campaign implements ModelInterface, ArrayAccess { /** * Gets started_at * @return \DateTime */ public function getStartedAt() { /* 実装は省略 */ } /** * Gets ended_at * @return \DateTime|null */ public function getEndedAt() { /* 実装は省略 */ } }

Slide 26

Slide 26 text

PHPStan でのチェック クライアント側で⾃動⽣成されたクラスでも ちゃんと getEndedAt() が nullable になっているので、 API を呼び出す側の実装者が、うっかりキャンペーンの終了⽇時が nullable である。 という仕様を知らなかった(読み取り損ねた)としても、⾃動的にエ ラーを検出可能。

Slide 27

Slide 27 text

まとめ

Slide 28

Slide 28 text

まとめ(ツール) API 側 zircote/swagger-php getter にも適⽤する拡張(type hint で nullable も加味) アノテーションからオブジェクトを連想配列にする実装(仕様 と実装をあわせる) クライアント側 swagger-api/swagger-codegen テンプレートをちょっとカスタマイズ(nullable も加味) phpstan/phpstan

Slide 29

Slide 29 text

まとめ(図)