Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Laravel OpenAPIによる "辛くない" スキーマ駆動開発

Laravel OpenAPIによる "辛くない" スキーマ駆動開発

PHPerKaigi 2024 day2 11:15 Track-C

テキスト版資料: https://no-hack-no.life/post/2024-03-08-Schema-Driven-Development-with-Laravel-OpenAPI/

武田 憲太郎

March 08, 2024
Tweet

More Decks by 武田 憲太郎

Other Decks in Programming

Transcript

  1. プロポーザル 1/4 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 1

    スキーマ駆動開発は⾮常に強⼒な開発⼿法です。 • API仕様とサーバ実装が確実に⼀致し、 クライアントライブラリは⾃動⽣成されます。 • フロントエンドは型システムの⼒により、 「サーバ」を意識せずに開発が可能です。 • 「APIの繋ぎ込み」タスクや 結合テスト時の問題切り分けが不要になります。
  2. プロポーザル 2/4 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 4

    スキーマ駆動開発はしばしば「⾟い」と⾔われます。 • スキーマと実装とを それぞれ書かなければいけません。 • 開発中の変更が フロントエンドのCIを予期せず壊すことがあります。 • 破壊的変更を避けるために 類似のエンドポイントが乱⽴しがちです。 • 実際には、 仕様と実装が常に⼀致しているとは限りません。
  3. プロポーザル 3/4 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 6

    これらの課題を LaravelおよびLaravel OpenAPIを使⽤して解決します。 • ライブラリの機能を活⽤し、 スキーマと実装との⼆重化を解消します。 • 仕様と実装との不⼀致を ⾃動的に検出します。 • フロントエンドのCIを壊さない スキーマの運⽤を⾏います。 • そもそもスキーマ駆動開発とは何かを解説します。
  4. プロポーザル 4/4 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 7

    これまでOpenAPIやスキーマ駆動開発に 苦労したことのある⽅はもちろん、 これから導⼊を検討している⽅々にとって 有益な内容です。
  5. ⾃⼰紹介・課題感・登壇の動機 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 8 @KentarouTakeda

    / 武⽥ 憲太郎 / Webアプリケーションエンジニア • 得意な⾔語: PHP >= 4.3 • 好きな⾔語: TypeScript >= 0.9 PHPは4の頃から • register_globals • error_reporting(E_ALL & ~E_NOTICE) • 型に関する保証が何もない。 TypeScript 0.9でフロント開発を学び始めた • 型安全、null安全な実装。
  6. ⾃⼰紹介・課題感・登壇の動機 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 9 LaravelやSymfonyでAPI開発を⾏うようになった

    • フロントエンド同等の型安全性を得られない。 • 異なる⾔語で同じ意味のコードを2度書くストレス。 登壇のモチベーション • APIへの型付けや実装の⼆重化に、 課題感を持ち続けている。 • 解決のノウハウを共有し、 知⾒を深め合いたい。
  7. APIファーストが⽣む困難 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 11 Ø解釈の余地のある仕様書

    • 仕様書の冗⻑化に過ぎない実装 • 仕様と実装の乖離 • 信じられない仕様書 • 隠蔽されない知識
  8. 解釈の余地のある仕様書 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 12 •⽂字列の⻑さ

    •⽂字列の形式 •処理系による型の違い •⽇付や時刻の表現 •List要素の型 •nullとプロパティ未定義 •表現の難しい複雑なオブジェクト
  9. APIファーストが⽣む困難 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 13 •

    解釈の余地のある仕様書 Ø仕様書の冗⻑化に過ぎない実装 • 仕様と実装の乖離 • 信じられない仕様書 • 隠蔽されない知識
  10. 仕様書の冗⻑化に過ぎない実装 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 14 class

    CreatePHPerRequest extends FormRequest { public function rules(): array { return [ 'email' => [ 'required', 'email', 'max:128' ], 'password' => [ 'required', 'string', 'max:32' ], 'name' => [ 'required', 'string', 'max:20' ], 'birthdate' => [ 'nullable', 'date' ], 'introduction' => [ 'required', 'string', 'max:1024' ], 'frameworks' => [ 'required', 'array' ], 'frameworks.*' => [ 'string' ], ]; } } 仕様書と全く同じ内容を別の書き⽅に書き直しているに過ぎない
  11. 仕様書の冗⻑化に過ぎない実装 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 15 <label

    for="email">メールアドレス</label> <input type="email" name="email" id="email" maxlength="128"> <label for="password">パスワード</label> <input type="password" name="password" id="password" maxlength="32"> <label for="name">名前</label> <input type="text" name="name" id="name" maxlength="20"> <label for="birthdate">生年月日</label> <input type="date" name="birthdate" id="birthdate"> <!-- 省略 --> 仕様書と全く同じ内容を別の書き⽅に書き直しているに過ぎない
  12. 仕様書の冗⻑化に過ぎない実装 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 16 axios.post("/api/phpers",

    { email: form.email, password: form.password, name: form.name, birthdate: form.birthdate, introduction: form.imtroduction, frameworks: form.frameworks, }).then(response => { // 省略: レスポンスを描画 }); 仕様書と全く同じ内容を別の書き⽅に書き直しているに過ぎない • ⾔語もコードベースも異なるため、再利⽤が効かない。 • ⼿作業には常に、ミスのリスクが付きまとう。 • 再利⽤を⾏えない意味の薄い単純作業に割かれる多くの時間。
  13. APIファーストが⽣む困難 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 17 •

    解釈の余地のある仕様書 • 仕様書の冗⻑化に過ぎない実装 Ø仕様と実装の乖離 • 信じられない仕様書 • 隠蔽されない知識
  14. 仕様と実装の乖離 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 18 問い:

    形の誤りを指摘してください。DBはMySQLとします。 Schema::create('items', function (Blueprint $table) { $table->id()->comment('商品ID'); $table->string('name')->comment('商品名'); $table->decimal('price')->comment('販売価格'); $table->boolean('in_sale')->comment('販売中かどうか'); }); public function show(Item $item) { return [ 'id' => $item->id, // 1. 商品ID: number 'name' => $item->name, // 2. 商品名: string 'price' => $item->price, // 3. 販売価格: number ‘in_sale’ => $item->in_sale, // 4. 販売中かどうか: boolean ]; }
  15. 仕様と実装の乖離 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 19 •

    numeric型(PostgreSQL) / decimal型(MySQL) • 固定⼩数点はfloatでは表現できないためPHPではstringとして扱われる。 • boolean型(MySQL) • tinyint(1)型へのエイリアス。 正解: 3, 4 { "id": 42, "name": "The answer", "price": "100.00", // 3. numberではなくstring "in_sale": 1 // 4. booleanではなくnumber }
  16. APIファーストが⽣む困難 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 20 •

    解釈の余地のある仕様書 • 仕様書の冗⻑化に過ぎない実装 • 仕様と実装の乖離 Ø信じられない仕様書 • 隠蔽されない知識
  17. 信じられない仕様書 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 21 •

    履歴を管理できない形式 • バックアップによる運⽤ •ブランチ毎に並列管理 •マージを⾏えない形式
  18. APIファーストが⽣む困難 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 22 •

    解釈の余地のある仕様書 • 仕様書の冗⻑化に過ぎない実装 • 仕様と実装の乖離 • 信じられない仕様書 Ø隠蔽されない知識
  19. 隠蔽されない知識 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 23 API仕様書に準拠した実装

    • ⽬的: IDを指定しPHPer情報 を取得する。 • ⼿段: 仕様通りに URLを組み ⽴てGETリクエストを送る。 ネットワークの知識が フロントに漏れ出している。 抽象化された実装 • ⽬的: IDを指定しPHPer情報 を取得する。 • ⼿段: IDを指定しPHPer情報 を取得する。 適切な隠蔽が ⽬的と⼿段を⼀致させる。 // GET /api/phpers/{id} const phper = await axios .get('/api/phpers/' + id); // PhperApiクラスのメソッド呼び出し const phper = await phperApi .getPhperById(id);
  20. 仕様管理における課題 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 25 Ø仕様書の無い開発

    • 仕様書のある開発 • 「仕様書」は依存に値するか? • 登壇者による「スキーマ起動開発」の再定義 • 依存の強制
  21. 仕様書の無い開発 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 26 •

    サーバサイドが出⼒するプロパティ名と フロントエンドのそれとが連動 • フロントエンドのコンポーネントが サーバサイドの実装に依存 // サーバサイド: PhperController.php public function show(Phper $phper): array { return response()->json([ 'name' => $phper->name, 'email' => $phper->email, 'birthdate' => $phper->birthdate ->toIso8601String(), ]); } // フロントエンド: PhperComponent.tsx useEffect(() => { axios.get("/api/phpers/" + id) .then(response => setPhper({ name: response.data.name, email: response.data.email, birthdate: new Date(response.data.birthdate), }) });
  22. 仕様書の無い開発 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 27 依存関係

    • インターネットを跨いで依存している。 • 担当者を跨いで依存している。 • リポジトリを跨いで依存している。
  23. 仕様管理における課題 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 28 •

    仕様書の無い開発 Ø仕様書のある開発 • 「仕様書」は依存に値するか? • 登壇者による「スキーマ起動開発」の再定義 • 依存の強制
  24. 仕様書のある開発 - 依存関係逆転の原則 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発

    29 依存関係逆転の原則 上位のモジュールは下位のモジュールに依存してはならない。どちらの モジュールも「抽象」に依存すべきである。 抽象(仕様書)への依存により詳細(インターネット)を隠蔽 APIファーストはここが出発点
  25. 仕様管理における課題 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 30 •

    仕様書の無い開発 • 仕様書のある開発 Ø「仕様書」は依存に値するか? • 登壇者による「スキーマ起動開発」の再定義 • 依存の強制
  26. 「仕様書」は依存に値するか? 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 32 プリミティブな値は

    Json Schemaで表現可能 • 数値 { "type": "number" } • 整数 { "type": "integer" } • ⾃然数 { "type": "integer", "minimum": 1 } • non-empty-array<string> { "type": "array": "items": { "type": "string" }, minItems: 1 } プリミティブなドメインは OpenAPI Specificationで表現可能 • ⽂字列・UI上はマスクを推奨・8⽂字以上 { "type": "string", "format": "password", "minLength": 8 } • ⽇時・ISO8601形式の⽂字列 { "type": "string", "format": "date-time" } • URL⽂字列 { "type": "string", "format": "uri" } 「解釈の余地」は争い → 争いをなくす 争いのないものから作る
  27. 「仕様書」は依存に値するか? 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 33 •

    間違いが機械的に指摘される。 • 補完(エディタ・IDE) • Lint / 静的解析 • Spectral • IBM OpenAPI Validator • 実⾏時検査 • 履歴が残る。 • Git ⼈間が書くから間違える → 機械に任せれば間違えない 間違えにくい道具
  28. 仕様管理における課題 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 34 •

    仕様書の無い開発 • 仕様書のある開発 • 「仕様書」は依存に値するか? Ø「スキーマ駆動開発」の再定義 • 依存の強制
  29. 「スキーマ駆動開発」の再定義 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 35 安定したインターフェース記述⾔語へ依存することで、

    秩序を強制し、品質を向上させる、開発⼿法 •強制が、メリットでありデメリット。 •強制のコントロールが、開発の成否を決める。
  30. 仕様管理における課題 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 37 •

    仕様書の無い開発 • 仕様書のある開発 • 「仕様書」は依存に値するか? • 登壇者による「スキーマ起動開発」の再定義 Ø依存の強制
  31. 依存の強制 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 38 OpenAPIドキュメントそれ⾃体に強制⼒は無い。

    実⽤的に使うには、これと同じ強制が必要。 class Phper extends Model implements Authenticatable { } // Fatal error: // Class PhperModel contains 6 abstract methods and must therefore be declared ...
  32. 依存の強制 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 39 ツールによる依存の強制

    • OpenAPI Generator • 仕様に準拠(=正しく依存)したライブラリが⾃動⽣成される • 「ビルドが通れば問題なし」と判断できる • API Gateway • OpenAPI への API Gateway 拡張機能の使⽤を使う • Apigee • OpenAPI 仕様から API プロキシを作成する • バリデーションの⾃動化 • OpenAPI PSR-7 Message Validator
  33. 依存の強制 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 40 成果物の再利⽤

    • ドキュメントの⽣成 • Redocly • テストツール • Swagger UI • e2eテストとの統合 • Integrate Postman with OpenAPI
  34. スピードと品質との両⽴ 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 42 ØOpenAPIドキュメントを書く

    • フロントエンド開発 • サーバサイド開発 • フロントとサーバの統合 - "⾟くない" 開発 • エラー原因の提供
  35. OpenAPIドキュメントを書く - ツールで書く 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発

    43 Swagger Editor Stoplight Studio / 紹介記事 メリット:導⼊が容易 / デメリット:コード管理・CI
  36. OpenAPIドキュメントを書く - 直接書く 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発

    44 • 分散システムなどで 仕様を⼀元管理するケース • ツールでは対応の難しい 複雑なドキュメントを書くケース • OpenAPIドキュメント単体を ⼀般公開するケース メリット:⾃由度が⾼い / デメリット:管理と運⽤が煩雑
  37. OpenAPIドキュメントを書く - サーバサイドに書く 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発

    45 • メリット • API仕様はサーバサイドの担当者が書くことが多い • URL(ルーティング)はサーバサイドに実装される • 型(バリデーション)はサーバサイドに実装される • 仕様と実装とを⼀致させやすい • 対応ツール • L5 Swagger / Swagger-PHP • Laravel OpenAPI
  38. L5 Swagger - APIの宣⾔ 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない"

    スキーマ駆動開発 46 #[OA¥Get( operationId: 'getPhperById', path: '/phpers/{id}',)] #[OA¥Response( response: '200', content: new OA¥JsonContent( properties: [ new OA¥Property(property: 'data', type: PhperResource::class) ] ) )] public function show(Phper $phper) { return new PhperResource($phper); } • 仕様をアトリビュートで宣⾔。すぐ下に実装。 • APIの仕様と実装とを紐づけ。
  39. L5 Swagger - 型の宣⾔ 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない"

    スキーマ駆動開発 47 • 仕様をアトリビュートで宣⾔。すぐ下に実装。 • レスポンスの型定義(仕様)と⽣成(実装)とを紐づけ。 #[OA¥Schema( properties: [ new OA¥Property(property: 'name', type: 'string'), new OA¥Property(property: 'email', type: 'string', format: 'email'), new OA¥Property(property: 'birthdate', type: 'string', format: 'date-time'), ] )] class PhperResource extends JsonResource { public function toArray(Request $request): array { return [ /* 省略 */ ]; } }
  40. Laravel OpenAPI 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 48

    phpコードで型を実装。 利⽤例: • OpenAPIのenumをPHP のenumから⾃動⽣成 • exampleやdescription などメタデータを⾃動⽣ 成 • propertiesとrequired を連動させる public function build(): Schema { $properties = [ /* 省略 */ Schema::string('status')->description('ステータス') ->enum(...Arr::pluck(PhperStatus::cases(), 'value')) ->example(PhperStatus::default()) ->description( implode(' / ', Arr::map( PhperStatus::cases(), fn ($enum) => "{$enum->value}:{$enum->display()}" )), ), ]; return Schema::object('Post') ->properties(...$properties) ->required(...$properties); }
  41. Laravel OpenAPI 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 49

    利⽤例: • 頻出パターン「data属性でのラップ」を共通化 /** * @param class-string<Schema> $schema */ public static function wrapSchemaWithData(string $schema): Schema { return Schema::object() ->required('data') ->properties($schema::ref('data')); } public function build(): Response { return Response::ok() ->content( MediaType::json()->schema( Utils::wrapSchemaWithData (PostSchema::class) ) ); }
  42. Laravel OpenAPI 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 50

    利⽤例: •ペジネータが⽣成する オブジェクトと同じ型 を動的に⽣成するユー ティリティ関数 /** * @param class-string<Schema> $schema */ public static function wrapSchemaWithPagination(string $schema): Schema { $properties = [ Schema::array('data')->items($schema::ref()), Schema::integer('total'), Schema::integer('per_page'), Schema::integer('current_page'), /* 省略 */ ]; return Schema::object() ->required(...$properties) ->properties(...$properties); }
  43. スピードと品質との両⽴ 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 51 •

    OpenAPIドキュメントを書く Øフロントエンド開発 • サーバサイド開発 • フロントとサーバの統合 - "⾟くない" 開発 • エラー原因の提供
  44. フロントエンド開発 - ドキュメントや開発ツールの⾃動⽣成 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発

    53 OpenAPIドキュメントから ⽣成されたAPI仕様書が⼀般 公開されている例: •PGマルチペイメントサー ビス OpenAPIタイプ •API仕様書から元の OpenAPIドキュメントを取 り出せる。
  45. フロントエンド開発 - ドキュメントや開発ツールの⾃動⽣成 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発

    55 ドキュメント⽣成コマンド例: # ファイル名 `schema.json` は適宜読み替え $ npx -y @redocly/cli@latest build-docs schema.json
  46. フロントエンド開発 - クライアントライブラリ⾃動⽣成 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発

    56 •⽇時の⾃動変換 •enumの⾃動⽣成 phper: type: object properties: # 省略 registerdAt: type: string format: date-time status: type: string example: beginner enum: - beginner - intermediate - expert export function PhperFromJSONTyped( json: any, ignoreDiscriminator: boolean ): Phper { // 省略 return { 'registerdAt': /* 省略 */ (new Date(json['registerdAt'])), 'status': /* 省略 */ json['status'], }; } export const PhperStatusEnum = { Beginner: 'beginner', Intermediate: 'intermediate', Expert: 'expert' } as const;
  47. フロントエンド開発 - クライアントライブラリ⾃動⽣成 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発

    57 TypeScript⽤クライアントライブラリ typescript-fetch • OpenAPIドキュメントのタグ毎にクラスが作成される。 • エンドポイント(オペレーションID)がメソッドになる。 • それに対しリクエストと成功時レスポンスが型付けされる。 class PhperApi { /* 省略 */ async getPhperById( // 1つのエンドポイントに対して1つのメソッド requestParameters: GetPhperByIdRequest, // リクエストの型が定義されている initOverrides?: RequestInit | runtime.InitOverrideFunction ): Promise<Phper> { // レスポンスの型も定義されている const response = await this.getPhperByIdRaw(requestParameters, initOverrides); return await response.value(); } }
  48. フロントエンド開発 - クライアントライブラリ⾃動⽣成 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発

    58 PHP⽤クライアントライブラリ - php • クラス(タグ)とメソッド(オペレーションID)はtypescript-fetchと同じ構造 • 型はクラスに変換され、レスポンスはそのインスタンス。 • エラーを含む返却されうる全ての型がUnion Typesで表現される。 class PhperApi { /** * @return ¥OpenAPI¥Client¥Model¥Phper|¥OpenAPI¥Client¥Model¥ErrorResponse */ public function getPhperById($id, string $contentType = /* 省略 */) { list($response) = $this->getPhperByIdWithHttpInfo($phperId, $contentType); return $response; } }
  49. フロントエンド開発 - クライアントライブラリ⾃動⽣成 対応⾔語⼀覧 • プログラム⾔語 PHP, TypeScript, JavaScript, Ruby,

    Go, Java, Objective-C, Kotlin, etc. • ライブラリ Angular, jQuery, RxJS , etc. • フレームワーク NestJS , etc. • ツール JMeter , etc. • シェル bash, Power Shell, etc. • SaaS Zapier, etc.
  50. フロントエンド開発 - クライアントライブラリ⾃動⽣成 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発

    60 クライアントライブラリ⽣成コマンド例 # ファイル名 `schema.json` は適宜読み替え # `typescript-fetch` の箇所で生成するライブラリの言語を指定 $ docker run --rm ¥ -v $PWD/schema.json:/in.json ¥ openapitools/openapi-generator-cli:latest-release ¥ generate -i /in.json -g typescript-fetch
  51. フロントエンド開発 - 実装例 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発

    61 • OpenAPIドキュメントが提供するスキーマを状態管理 の型として直接利⽤しても良い。 • レスポンスを利⽤してはいけない。レスポンスはス キーマをラップすべき。 // src/states/atoms/phper.ts import { atom } from "recoil"; import type { Phper } from "@/lib/openapi"; // OpenAPIドキュメントが提供するモデルをそのままRecoilStateとして利用 export const phperState = atom<Phper | null>({ key: "phper", default: null, });
  52. フロントエンド開発 - 実装例 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発

    62 • レスポンスから取り出したスキーマを状態へ直接代⼊。 • result: レスポンス / result.data: モデル(スキーマ) フロントエンドにHTTPを意識させない。 // src/hooks/use-login.ts export const useLogin = () => { const setPhper = useSetRecoilState(phperState); phperApi.getMyPhper().then((result) => { // サーバからのレスポンスをそのまま状態として管理 setPhper(result.data); }); };
  53. スピードと品質との両⽴ 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 63 •

    OpenAPIドキュメントを書く • フロントエンド開発 Øサーバサイド開発 • フロントとサーバの統合 - "⾟くない" 開発 • エラー原因の提供
  54. サーバサイド開発 - リクエストバリデーション 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発

    64 • OpenAPIドキュメントに定義されたリクエスト型をバリデー ションに利⽤。 • 以上の実装をミドルウェアで全ルートに⼀括適⽤。 public function handle(Request $request, ¥Closure $next): Response { $psrRequest = $this->psrHttpFactory->createRequest($request); try { $operationAddress = $this->schemaRepository->getRequestValidator()->validate($psrRequest); } catch (ValidationFailed $validationFailed) { // バリデーション失敗時は400 Bad Requestを返却 abort(400, 'リクエストの形式に誤りがあります。'); } // 成功時のみ次の処理に進む return $next($request); }
  55. サーバサイド開発 - バリデーション対象 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発

    65 バリデーションとは (validation) - IT⽤語辞典バイナリ バリデーションとは、⼊⼒されたデータが、あるいはプログ ラミング⾔語やマークアップ⾔語の記述が、規定された⽂法 に即して、または要求された仕様にそって、適切に記述され ているかどうかを検証することである。 バリデーションが必要な値とは? • 外部から⼊⼒された値 • 信頼できない値
  56. サーバサイド開発 - バリデーション対象 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発

    66 「信頼できない値」とは? ⾃分の書くプログラムに、 絶対の信頼を持てますか?
  57. サーバサイド開発 - バリデーション対象 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発

    67 仕様外レスポンスの先で、何が起きているか? • Uncaught TypeError: Cannot read properties of undefined (reading 'hoge') • ⼀瞥するとフロントエンドのバグ • 現にレスポンスコードは200 OK • 原因は実はサーバ側 • 開発中、想像以上の調査時間を要している。 • 本番環境で発⽣していても、知る術が無い。
  58. サーバサイド開発 - レスポンスバリデーション 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発

    68 NG例: 通常のAfterミドルウェア • Middleware - Laravel 10.x - Middleware and Responses • this middleware would perform its task after the request is handled by the application: 標準の⽅法では、次のケースに対応できない: • throw new HttpException($status) でエラーを返却するケース • abort($status) ヘルパーの場合も同様 • アプリケーションがクラッシュしたケース • レスポンスの⽣成を遅延するケース • StreamedResponse / StreamedJsonResponse / BinaryFileResponse public function handle(Request $request, Closure $next): Response { $response = $next($request); // Perform action return $response; }
  59. サーバサイド開発 - レスポンスバリデーション 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発

    69 OK例: レスポンスイベントのフック Middleware Pipelineの外側の処理のため 通常とは異なる⽅法でレスポンスを⽣成する必要がある点に注意 public function handle(Request $request, ¥Closure $next): Response { /* 省略 */ // ミドルウェアでの処理終了時にレスポンスイベントへのフックを登録 Event::listen(RequestHandled::class, function (RequestHandled $event) use ($operationAddress) { $psrResponse = $this->psrHttpFactory->createResponse($event->response); try { $schemaRepository->getResponseValidator()->validate($operationAddress, $psrResponse); } catch (ValidationFailed $validationFailed) { // 省略: レスポンスバリデーション失敗: ログや500エラー } }); return $next($request); }
  60. スピードと品質との両⽴ 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 70 •

    OpenAPIドキュメントを書く • フロントエンド開発 • サーバサイド開発 Øフロントとサーバの "⾟くない" 統合 • エラー原因の提供
  61. フロントとサーバの統合 - スキーマの破壊的変更 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発

    71 OpenAPIドキュメントの変更が フロントエンドに対して破壊的変更をもたらすことがある。 • OpenAPIドキュメントのバージョンがフロントとサーバとで ⼀致しない場合: • フロント側では問題なくビルドが通るコード(リクエスト)に 対し、サーバは400 Bad Requestを返却してしまう。 • 返却されたレスポンスを正しく認識できず、値の⽋落等が発⽣ する。 • フロント側でバージョンアップを⾏った場合: • 型の変更が原因となりビルドエラーが発⽣する。
  62. フロントとサーバの統合 - スキーマの破壊的変更 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発

    72 Q. クライアントライブラリ(⾃動⽣成)の運⽤ A. IMO: ⽣成結果ごとフロントエンドへcommit 理由: • 破壊的変更を受け⼊れるタイミングをフロントエンド が任意に決められる。 • ブランチに応じて新旧双⽅のバージョンを即座に切り 替えられる。
  63. フロントとサーバの統合 - 破壊的変更の発⽣条件 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発

    73 変更内容 更新(修正)前の挙動 エラー プロパティの追加 追加された型を認識できない - プロパティの削除・名前変更 Property 'foo' does not exists ビルド時 {nullable: true} をfalseへ - - {nullable: false} をtrueへ 'foo.bar' is possibly 'null' ビルド時 {required: true} をfalse へ - - {required: false} をtrue へ 'foo.bar' is possibly 'null' ビルド時 型の変更 Property 'foo' does not exists ビルド時 オペレーションIDの変項 Property 'foo' does not exists ビルド時 URLの変更 404エラー 実⾏時
  64. フロントとサーバの統合 - 破壊的変更の発⽣条件 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発

    74 条件を予め知っておくことで、 発⽣の際の対応も容易になる。 • 型の範囲を狭めるのはOK。 • 型の範囲を広げるのはNG。 • 型を変更するのはNG。 • URL変更は実⾏時エラー。
  65. フロントとサーバの統合 - 破壊的変更への対応(移⾏期間) 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発

    75 • OpenAPIドキュメントに deprecatedタグを付与する ことで、 • クライアントライブラリの DocBlockそれが付与され、 • 多くのエディタで、打ち消 し線と共に⾮推奨として表 ⽰される。 $properties = [ // 変更前: updated_at Schema::string('updated_at')->format('date-time') ->deprecated() // 旧プロパティ名にdeprecatedを付与 ->description('lastLoggedInAtを利用してください。'), // 変更後: last_logged_in_at Schema::string('last_logged_in_at') ->description('最終ログイン日時日時') ];
  66. フロントとサーバの統合 - 破壊的変更への対応(移⾏期間) 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発

    76 •eslint-plugin-deprecationで deprecatedを⾃動検出 •任意タイミングで修正し、 •サーバサイドより旧仕様を削除。 // .eslintrc.json "extends": [ "plugin:deprecation/recommended" ], "rules": { "deprecation/deprecation": "warn" } $ npx eslint src /path/to/src/hooks/use-login.ts 48:9 warning 'updatedAt' is deprecated. deprecation/deprecation
  67. フロントとサーバの統合 - 破壊的変更への対応(同時修正) 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発

    77 サーバの変更とフロントの修正を同時にプッシュ • 要修正箇所は、TypeScriptが教えてくれる。 • 多くの場合、修正は単純な書き換えのみ。 • サーバ担当者は、変更内容を把握している。 破壊的変更への対応はサーバ担当者が⾏うのも選択肢
  68. フロントとサーバの統合 - nullableを避ける 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発

    78 nullable: trueやrequired: falseに細⼼の注意 • nullableはデフォルトでfalse。問題ない。 • requiredはデフォルトでfalse。明⽰的に trueにする必要がある。 • Lintによる機械的な対応がお勧め。 仕様上はnullable、実質的にはnot nullというプロパティが多くあると: • フロントエンドでは「nullを握りつぶす対応」が必要になる。 • 常態化すると、不適切な握りつぶしが横⾏する。 スキーマ駆動開発を導⼊したメリットの多くを失う。
  69. フロントとサーバの統合 - 活⽤先の仕様を把握 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発

    79 採⽤ツールに応じて⾃動⽣成のカバー範囲が異なる • 成功レスポンスのみ定義するか?失敗レスポンスも必要か? • 苦労して失敗レスポンスを書いたがクライアントライブラリで使われていな い、という例 • @example を書くか? • 多くのテストツールでフォームのデフォルト⼊⼒値として使われる。 • 推奨: ログインAPIの@exampleに開発⽤ユーザーのログイン情報を記⼊ • @examples まで書くか? • フロントエンド側でモックサーバを⽴てる場合はこの値が使われる。 • ドキュメントやIDEでの表⽰を確認 • MarkdownやHTMLの解釈がツールによって異なる。 ⽣成結果のコードに最低限⽬を通すことを推奨
  70. フロントとサーバの統合 - 名付けとモデリング 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発

    80 • オペレーションIDはクライアントライブラリでメソッド名として使われる。 • スキーマ名はクライアントライブラリで型名として使われる。 名付けに細⼼の注意 • フロントエンド以外を含むシステムの広範で使われる。 サーバサイドに閉じないモデリング • これらへ準拠により、多くの破壊的変更を回避できる。 これらのベストプラクティスは RESTish APIと相性が良い
  71. フロントとサーバの統合 - CIの活⽤ 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発

    81 ツール毎にOpenAPIドキュメン トの解釈が若⼲異なる: • 誤ったOpenAPIドキュメント が⽣成されてしまうケース。 • あるツールは問題なく動作す るが別のツールはクラッシュ、 というケース。 プロジェクト固有の型ルール: • 機械的に準拠を確認したい。 サーバサイドのCIで ツールの動作やLint結果を確認 steps: # Laravelアプリケーションのセットアップ・省略 - name: OpenAPIドキュメントを出力 run: ./artisan openapi:generate > schema.json - name: クライアントライブラリを生成できるか? run: | docker run --rm ¥ -v $PWD/schema.json:/in.json ¥ openapitools/openapi-generator-cli:latest-release ¥ generate -i /in.json -g typescript-fetch - name: ドキュメントを生成できるか? run: npx -y @redocly/cli@latest build-docs schema.json # 必要に応じてOpenAPIドキュメントのLintなど
  72. スピードと品質との両⽴ 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 82 •

    OpenAPIドキュメントを書く • フロントエンド開発 • サーバサイド開発 • フロントとサーバの統合 - "⾟くない" 開発 Øエラー原因の提供
  73. エラー原因の提供 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 83 バリデーションの⽬的

    •データの正確性 •セキュリティ向上 •UX向上 •エラーの早期発⾒ • エラーとその原因の早期発⾒
  74. エラー原因の提供 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 84 •400エラーの理由

    を、不⾜なくフロ ントエンドに伝え る •500エラーの理由 を、容易に知れる ようにする { "title": "InvalidBody", "status": 500, // エラー理由 "detail": "Keyword validation failed: Value cannot be null", // エラー位置 "pointer": [ "data", "status" ], // オリジナルのレスポンス "originalResponse": { "data": { "id": 42, "status": null, // ←ここが原因 "name": "tomzoh", "content": "PHPerKaigi" } } }
  75. エラー原因の提供 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 85 •

    「200番台のステータスコードで仕様外のレスポンスを 返却することは、決してありません。」 • 「400だった場合は、フロントの実装に誤りがあります。 ⾃分のコードを⾒直してください。」 • 「500だった場合は、原因はバックエンドです。レスポ ンスにデバッグ情報が含まれるので、それを下さ い。」 以上の約束が、強制的に守られる。
  76. エラー原因の提供 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 86 本番リリース後も仕様外レスポンスを追跡

    • クラッシュさせずログに残しアラートと連携。 • 開発時と同じようにエラーとする。 • ⼗分に安定した場合、バリデーションを外しても良い。 • 巨⼤レスポンスをバリデーションする際のスループットへの配慮。 try { $schemaRepository->getResponseValidator()->validate($operationAddress, $psrResponse); } catch (ValidationFailed $validationFailed) { Log::warn( 'レスポンスバリデーション失敗', [ 'error' => $validationFailed, 'request' => $request, 'response' => $response, ], ); // キャッチした例外は、ログに残すがリスローはしない }
  77. PR: Laravel OpenAPI Validator 登壇者の作成したOpenAPIバリデーションライブラリ: • 本トーク内「スピードと品質の両⽴」がコンセプト。 • 資料中のサーバ側実装のほとんどは、このライブラリで実際に使われている。 主要な機能:

    • Laravel OpenAPI⼜はL5 Swagger導⼊済の場合、ゼロコンフィグで導⼊可能。 • それ以外の場合も、⼗数⾏のコードで統合が可能。 • バリデーションの対象やレベル、違反時の挙動をルート毎に設定可能。 • 開発効率の向上を⽬的とした、豊富なログとそのカスタマイズ。 • オプション機能として、Swagger UI でのAPIの表⽰に対応。 紹介記事: Laravelパッケージ「Laravel OpenAPI Validator」 - OpenAPIドキュメントによる透過的バリデーション
  78. まとめ 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 89 •

    APIファーストが⽣む困難 • 仕様書の解釈の余地 • 仕様書の冗⻑化に過ぎない実装 • 仕様管理やコミュニケーションコスト • 仕様管理における課題 • 争いのない仕様と間違えにくい道具 • 秩序の強制とコントロール • OpenAPIにより品質の向上を機械的に実現 • スピードと品質との両⽴ • ⽬的やプロジェクト要件に合ったツール選定や統合 • 破壊的変更のコントロール • バリデーションの意味や⽬的と⾃動化 • 開発時やリリース後の品質向上
  79. 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 90 スキーマ駆動開発は⾮常に強⼒な開発⼿法です。 •API仕様とサーバ実装が確実に⼀致し、

    クライアントライブラリは⾃動⽣成されます。 •フロントエンドは型システムの⼒により、 「サーバ」を意識せずに開発が可能です。 •「APIの繋ぎ込み」タスクや 結合テスト時の問題切り分けが不要になります。