Slide 1

Slide 1 text

Laravel OpenAPIʹΑΔ "ਏ͘ͳ͍" εΩʔϚۦಈ։ൃ 2024/3/8 #phperkaigi #c

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 2 なるほど、完璧な作戦っスね―――ッ 不可能だという点に⽬をつぶればよぉ〜

Slide 4

Slide 4 text

2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 3

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 5

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

プロポーザル 4/4 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 7 これまでOpenAPIやスキーマ駆動開発に 苦労したことのある⽅はもちろん、 これから導⼊を検討している⽅々にとって 有益な内容です。

Slide 9

Slide 9 text

⾃⼰紹介・課題感・登壇の動機 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安全な実装。

Slide 10

Slide 10 text

⾃⼰紹介・課題感・登壇の動機 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 9 LaravelやSymfonyでAPI開発を⾏うようになった • フロントエンド同等の型安全性を得られない。 • 異なる⾔語で同じ意味のコードを2度書くストレス。 登壇のモチベーション • APIへの型付けや実装の⼆重化に、 課題感を持ち続けている。 • 解決のノウハウを共有し、 知⾒を深め合いたい。

Slide 11

Slide 11 text

アジェンダ 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 10 •APIファーストが⽣む困難 •仕様管理における課題 •スピードと品質との両⽴

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

解釈の余地のある仕様書 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 12 •⽂字列の⻑さ •⽂字列の形式 •処理系による型の違い •⽇付や時刻の表現 •List要素の型 •nullとプロパティ未定義 •表現の難しい複雑なオブジェクト

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

仕様書の冗⻑化に過ぎない実装 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' ], ]; } } 仕様書と全く同じ内容を別の書き⽅に書き直しているに過ぎない

Slide 16

Slide 16 text

仕様書の冗⻑化に過ぎない実装 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 15 メールアドレス パスワード 名前 生年月日 仕様書と全く同じ内容を別の書き⽅に書き直しているに過ぎない

Slide 17

Slide 17 text

仕様書の冗⻑化に過ぎない実装 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 => { // 省略: レスポンスを描画 }); 仕様書と全く同じ内容を別の書き⽅に書き直しているに過ぎない • ⾔語もコードベースも異なるため、再利⽤が効かない。 • ⼿作業には常に、ミスのリスクが付きまとう。 • 再利⽤を⾏えない意味の薄い単純作業に割かれる多くの時間。

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

仕様と実装の乖離 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 ]; }

Slide 20

Slide 20 text

仕様と実装の乖離 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 }

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

信じられない仕様書 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 21 • 履歴を管理できない形式 • バックアップによる運⽤ •ブランチ毎に並列管理 •マージを⾏えない形式

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

隠蔽されない知識 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);

Slide 25

Slide 25 text

2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 24

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

仕様書の無い開発 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), }) });

Slide 28

Slide 28 text

仕様書の無い開発 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 27 依存関係 • インターネットを跨いで依存している。 • 担当者を跨いで依存している。 • リポジトリを跨いで依存している。

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

「仕様書」は依存に値するか? 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 31 すばやく実装するための戦略とテクニック2023年版 by @77web • 「争いのないものから作る」 • 「間違いにくい道具」

Slide 33

Slide 33 text

「仕様書」は依存に値するか? 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 32 プリミティブな値は Json Schemaで表現可能 • 数値 { "type": "number" } • 整数 { "type": "integer" } • ⾃然数 { "type": "integer", "minimum": 1 } • non-empty-array { "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" } 「解釈の余地」は争い → 争いをなくす 争いのないものから作る

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

仕様管理における課題 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 34 • 仕様書の無い開発 • 仕様書のある開発 • 「仕様書」は依存に値するか? Ø「スキーマ駆動開発」の再定義 • 依存の強制

Slide 36

Slide 36 text

「スキーマ駆動開発」の再定義 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 35 安定したインターフェース記述⾔語へ依存することで、 秩序を強制し、品質を向上させる、開発⼿法 •強制が、メリットでありデメリット。 •強制のコントロールが、開発の成否を決める。

Slide 37

Slide 37 text

2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 36

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

依存の強制 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 ...

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

依存の強制 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 40 成果物の再利⽤ • ドキュメントの⽣成 • Redocly • テストツール • Swagger UI • e2eテストとの統合 • Integrate Postman with OpenAPI

Slide 42

Slide 42 text

依存の強制 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 41 OpenAPIによるスキーマ駆動開発 多くの⽤途への正しい転⽤がOpenAPI活⽤のポイント

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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の仕様と実装とを紐づけ。

Slide 48

Slide 48 text

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 [ /* 省略 */ ]; } }

Slide 49

Slide 49 text

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); }

Slide 50

Slide 50 text

Laravel OpenAPI 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 49 利⽤例: • 頻出パターン「data属性でのラップ」を共通化 /** * @param class-string $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) ) ); }

Slide 51

Slide 51 text

Laravel OpenAPI 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 50 利⽤例: •ペジネータが⽣成する オブジェクトと同じ型 を動的に⽣成するユー ティリティ関数 /** * @param class-string $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); }

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

フロントエンド開発 - ドキュメントや開発ツールの⾃動⽣成 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 52 GitHub REST APIのOpenAPIドキュメントから RedoclyでAPI仕様書を⾃動⽣成。

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

フロントエンド開発 - ドキュメントや開発ツールの⾃動⽣成 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 54 API⼀覧 認証 テストリクエスト レスポンス

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

フロントエンド開発 - クライアントライブラリ⾃動⽣成 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;

Slide 58

Slide 58 text

フロントエンド開発 - クライアントライブラリ⾃動⽣成 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 { // レスポンスの型も定義されている const response = await this.getPhperByIdRaw(requestParameters, initOverrides); return await response.value(); } }

Slide 59

Slide 59 text

フロントエンド開発 - クライアントライブラリ⾃動⽣成 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; } }

Slide 60

Slide 60 text

フロントエンド開発 - クライアントライブラリ⾃動⽣成 対応⾔語⼀覧 • プログラム⾔語 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.

Slide 61

Slide 61 text

フロントエンド開発 - クライアントライブラリ⾃動⽣成 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

Slide 62

Slide 62 text

フロントエンド開発 - 実装例 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({ key: "phper", default: null, });

Slide 63

Slide 63 text

フロントエンド開発 - 実装例 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); }); };

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

サーバサイド開発 - リクエストバリデーション 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); }

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

サーバサイド開発 - レスポンスバリデーション 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; }

Slide 70

Slide 70 text

サーバサイド開発 - レスポンスバリデーション 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); }

Slide 71

Slide 71 text

スピードと品質との両⽴ 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 70 • OpenAPIドキュメントを書く • フロントエンド開発 • サーバサイド開発 Øフロントとサーバの "⾟くない" 統合 • エラー原因の提供

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

フロントとサーバの統合 - 破壊的変更の発⽣条件 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エラー 実⾏時

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

フロントとサーバの統合 - 破壊的変更への対応(移⾏期間) 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('最終ログイン日時日時') ];

Slide 77

Slide 77 text

フロントとサーバの統合 - 破壊的変更への対応(移⾏期間) 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

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

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

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

フロントとサーバの統合 - 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など

Slide 83

Slide 83 text

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

Slide 84

Slide 84 text

エラー原因の提供 2024/3/8 #phperkaigi #c Laravel OpenAPIによる "⾟くない" スキーマ駆動開発 83 バリデーションの⽬的 •データの正確性 •セキュリティ向上 •UX向上 •エラーの早期発⾒ • エラーとその原因の早期発⾒

Slide 85

Slide 85 text

エラー原因の提供 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" } } }

Slide 86

Slide 86 text

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

Slide 87

Slide 87 text

エラー原因の提供 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, ], ); // キャッチした例外は、ログに残すがリスローはしない }

Slide 88

Slide 88 text

PR: Laravel OpenAPI Validator

Slide 89

Slide 89 text

PR: Laravel OpenAPI Validator 登壇者の作成したOpenAPIバリデーションライブラリ: • 本トーク内「スピードと品質の両⽴」がコンセプト。 • 資料中のサーバ側実装のほとんどは、このライブラリで実際に使われている。 主要な機能: • Laravel OpenAPI⼜はL5 Swagger導⼊済の場合、ゼロコンフィグで導⼊可能。 • それ以外の場合も、⼗数⾏のコードで統合が可能。 • バリデーションの対象やレベル、違反時の挙動をルート毎に設定可能。 • 開発効率の向上を⽬的とした、豊富なログとそのカスタマイズ。 • オプション機能として、Swagger UI でのAPIの表⽰に対応。 紹介記事: Laravelパッケージ「Laravel OpenAPI Validator」 - OpenAPIドキュメントによる透過的バリデーション

Slide 90

Slide 90 text

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

Slide 91

Slide 91 text

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