Slide 1

Slide 1 text

PHP のアノテーション(アトリビュー ト)からOpenAPI のドキュメントを出 力し、レスポンスもそれを元にシリア ライズすることで仕様と実装を乖離さ せず、色々楽できたよって話 Toshiyuki Fujita (@kalibora) 2024/11/08

Slide 2

Slide 2 text

最初に 今回の発表は2019 年11 月の KyotoLT 第25 回 で喋った Swagger (OpenAPI) と PHPStan で REST API でも型安全っぽく使う の焼き 直しです でも多分誰も知らないと思うのここでも話します 現状はOpenAPI は3 系ですが、昔の話なので2 系(まだSwagger と呼 ばれてた時代)が話のベースとなっていますがご容赦ください 2024/11/11 追記 この発表後に現代では API Platform を使えばもっといい感じに できると教えていただきました なのでこの資料は過去の歴史としてご笑覧ください

Slide 3

Slide 3 text

自己紹介 Toshiyuki Fujita ID: @kalibora 20 年くらいPHP いじってご飯を食べてます Yahoo! JAPAN -> (Crocos) -> Otobank -> RABO 今回のは Otobank 時代にやってた話です Symfony と Doctrine が好きです

Slide 4

Slide 4 text

OpenAPI とは - (WebAPI の仕様書くツールって、昔はいろいろあったけど結局もう これが勝ったという認識でよろしいか?) OpenAPI Specification (以前はSwagger Specification として知ら れていた)は、Web サービスを記述、生成、消費[ 訳語疑問点] 、 可視化するための機械可読なインターフェース記述言語の仕様で ある[1] 。 See: https://ja.wikipedia.org/wiki/OpenAPI_Specification “ “

Slide 5

Slide 5 text

突然ですが・・・ OpenAPI の仕様書くのって ダルいですよね?

Slide 6

Slide 6 text

OpenAPI の仕様書くのってダルいですよね? swagger: "2.0" info: version: "1.0.0" description: "こういうやつですね" 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 7

Slide 7 text

OpenAPI の仕様を書くための手段 Swagger Editor https://editor.swagger.io/ ブラウザベースで記述できるエディター swagger-php https://github.com/zircote/swagger-php PHP のアトリビュート(もしくはアノテーション)を使って仕様 を記述できる これを使って楽する!! (※これ調べたの昔なので最近はもっと色々増えてるかもね!)

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

出力結果 swagger: '2.0' definitions: User: properties: name: description: ユーザー名 type: string

Slide 10

Slide 10 text

便利! でもデフォルトのswagger-php だと 不便な点があった

Slide 11

Slide 11 text

デフォルトのswagger-php だと不便な点 getter などのアクセサに対応していない 実際の業務で使っているエンティティクラスは private なプロパ ティ + getter な構成が多いのでそのままだとうまく使えない (※2024/11/08 現在ではできるようになったのか未確認です) ので、自前で swagger-php を拡張して対応した

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

いい!! けどせっかくなら そのままAPI の挙動にも反映したい

Slide 15

Slide 15 text

API の挙動にも反映したい そもそもだけど、アノテーション(アトリビュート)書いたらその ままAPI の挙動に反映できたら便利 アノテーション(アトリビュート)から 仕様(OpenAPI 仕様の出力結果) 実装(実際にレスポンスとして返す値) この両方に影響を与えたい

Slide 16

Slide 16 text

オブジェクトから配列に変換する処理を書いた ようするにシリアライザー(ノーマライザー?)を実装した。 /** * @SWG\Definition() */ class User { private sring $name; /** * ユーザ名 * @SWG\Property() */ public function getName() : string { return $this->name; } } 先ほどと同じこういうクラスの定義から、

Slide 17

Slide 17 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 18

Slide 18 text

PHPStan と組み合わせる

Slide 19

Slide 19 text

PHPStan と組み合わせる PHPStan と組み合わせれば、サーバー(API )側とクライアント側 との間でAPI 呼び出しをしても型安全にできる

Slide 20

Slide 20 text

例えばこんなケース

Slide 21

Slide 21 text

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

Slide 22

Slide 22 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 23

Slide 23 text

OpenAPI 仕様書から自動生成されたクライアント側のクラス class Campaign implements ModelInterface, ArrayAccess { /** * Gets started_at * @return \DateTime */ public function getStartedAt() { /* 実装は省略 */ } /** * Gets ended_at * @return \DateTime|null */ public function getEndedAt() { /* 実装は省略 */ } } ※ クライアントの自動生成は swagger-api/swagger-codegen をベースに x-nullable を うまく解釈できるようにテンプレートはカスタマイズしています

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

さらにいろんなことに活用できる

Slide 26

Slide 26 text

その他の活用事例 public なAPI がセンシティブな情報を返していないことをCI で担保する 仕様と実装が乖離していないため、仕様であるyaml(or json) ファイルを機械 的にパースして特定のキーワード(password など)がレスポンスにあった らCI で落とす 詳しくは: API が不必要にセンシティブなデータを返していないことをCI で 担保する - OTOBANK Engineering Blog API のI/F 変更があったらサマリをGitHub Actions でPR に書いてもらう OpenAPITools/openapi-diff を使って仕様の差分を取る さらに(プロパティが削除されるなどの理由で)後方互換性が崩れていたら CI で落とす

Slide 27

Slide 27 text

おしまい