Slide 1

Slide 1 text

ࢲͷѪͨ͠ Laravel ϨʔϧΛ௒͑ͨͦͷઌ΁

Slide 2

Slide 2 text

ຊ౰ʹਖ਼͍͠ূ໌͸ɺ Ұ෼ͷܺ΋ͳ͍׬શͳڧݻ͞ͱ͠ͳ΍͔͕͞ɺ ໃ६ͤͣௐ࿨͍ͯ͠Δ΋ͷͳͷͩɻ ⼩川 洋⼦ (2003), 新潮社 博⼠の愛した数式

Slide 3

Slide 3 text

ͨͱ͑ؒҧͬͯ͸͍ͳͯ͘΋ɺ ͏Δͯ͘͞Ԛͯ͘ᚉʹোΔূ໌͸ ͍͘ΒͰ΋͋Δɻ෼͔Δ͔͍ʁ ⼩川 洋⼦ (2003), 新潮社 博⼠の愛した数式

Slide 4

Slide 4 text

-BSBWFM'SBNFXPSLͱ ΞϓϦέʔγϣϯͷௐ࿨

Slide 5

Slide 5 text

⾃⼰紹介: 過去の登壇 Laravelへの異常な愛情 / PHPerKaigi 2023 再⽣回数: 歴代3位 2025/3/22 #phperkaigi #a 私の愛したLaravel 4

Slide 6

Slide 6 text

⾃⼰紹介: インタビュー記事 「コンセプト」に気づけば実装の意図が分かる。 Laravelスペシャリストに聞く、OSSを読む意義 「貢献」ではなく「⾃分が楽をしたいから」。 Laravelスペシャリストが語る、肩の⼒を抜いたOSS活動のススメ 2025/3/22 #phperkaigi #a 私の愛したLaravel 5

Slide 7

Slide 7 text

⾃⼰紹介: PHP⽇本語マニュアル php/doc-ja#150 PHP 8.4 マニュアル翻訳状況 PHP Manual プロパティフック 2025/3/22 #phperkaigi #a 私の愛したLaravel 6

Slide 8

Slide 8 text

⾃⼰紹介: php-src php/php-src#14260 ext/pdo_pgsql: Retrieve the memory usage of the query result resource php/php-src# #15893 ext/pdo_pgsql: Expanding COPY input from an array to an iterable 2025/3/22 #phperkaigi #a 私の愛したLaravel 7

Slide 9

Slide 9 text

アジェンダ 2025/3/22 #phperkaigi #a 私の愛したLaravel 8 • Laravelアプリケーションはなぜ破綻するのか? • 拡張の典型的なパターン • 拡張の実例 • config() で拡張を設定する例 • 専⽤APIから拡張を登録する例 • サービスコンテナ結合を上書きする例 • イベントやミドルウェアから介⼊する例 • インターフェースを利⽤する例 • 最適なアプローチを選ぶ

Slide 10

Slide 10 text

Laravelアプリケーションはなぜ破綻するのか?

Slide 11

Slide 11 text

Laravelへの意⾒ 肯定的 • 開発スピードが速い • コードが短く済む 否定的 • 中規模以上の開発で破綻する 「安く早く作るのだから破綻して当然」 と思考停⽌してはいけない

Slide 12

Slide 12 text

破綻の理由を分析する: Laravelの特徴 2025/3/22 #phperkaigi #a 私の愛したLaravel 11 Øリソース志向フレームワーク • アクティブレコードパターン • フルスタックフレームワーク • ドキュメンテーションポリシー

Slide 13

Slide 13 text

リソース志向フレームワーク 2025/3/22 #phperkaigi #a 私の愛したLaravel 12 武⽥ 憲太郎 (2023) Laravelへの異常な愛情 または私は如何にして⼼配するのを⽌めてEloquentを愛するようになったか PHPerKaigi 2023

Slide 14

Slide 14 text

リソース志向フレームワーク 2025/3/22 #phperkaigi #a 私の愛したLaravel 13 @mizchi (2024) Rails vs Node.js 最終章 「Prisma」 Cloudflare Meet-up Tokyo Vol.6

Slide 15

Slide 15 text

リソース志向フレームワーク 2025/3/22 #phperkaigi #a 私の愛したLaravel 14 @MadakaHeri (2024) Laravelが如何にダメで時代遅れかを説明する

Slide 16

Slide 16 text

リソース志向 + 簡潔なコード 2025/3/22 #phperkaigi #a 私の愛したLaravel 15 役割 ファイル 実装作業 マイグレーション database/migrations/ xxx_create_tasks_table.php • テーブル定義 バリデーション app/Http/Requests/ StoreTaskRequest.php • バリデーション バリデーション app/Http/Requests/ UpdateTaskRequest.php • バリデーション Eloquentモデル app/Models/ Task.php • $fillable • リレーションシップ コントローラー app/Http/Controllers/ TaskController.php • CRUD操作 # Taskモデルとそれに対応するマイグレーション、バリデーション、APIコントローラーを作成 $ ./artisan make:model Task --migration --controller --requests --api

Slide 17

Slide 17 text

リソース志向 + 簡潔なコード 2025/3/22 #phperkaigi #a 私の愛したLaravel 16 // routes/api.php:`tasks` リソースにTaskControllerをアタッチ Route::apiResource('tasks', TaskController::class); メソッド パス 役割 GET api/tasks ⼀覧取得 POST api/tasks 作成 GET api/tasks/{task} 1件取得 PUT api/tasks/{task} 更新 DELETE api/tasks/{task} 削除

Slide 18

Slide 18 text

リソース志向 + 簡潔なコード 2025/3/22 #phperkaigi #a 私の愛したLaravel 17 • ボイラープレート作成 1コマンド • テーブル定義 数⾏のコード • リレーションシップ 4⾏ × 2箇所(参照‧被参照) • 操作フィールドを$fillableへ設定 数⾏のコード • バリデーション実装 数⾏のコード × 2箇所(作成‧更新) • ルート追加 1⾏のコード • コントローラー実装 最短で5⾏のコード 数分で全て実装完了する世界観 class TaskController extends Controller { public function index() { return Task::all(); // 一覧取得 } public function store(StoreTaskRequest $request) { return $request->user()->tasks()->create($request->validated()); // 作成 } public function show(Task $post) { return $post; // 1件取得 } public function update(UpdateTaskRequest $request, Task $post) { return tap($post)->update($request->validated()); // 更新 } public function destroy(Task $post) { return tap($post)->delete(); // 削除 } }

Slide 19

Slide 19 text

リソース志向 + 簡潔なコード 2025/3/22 #phperkaigi #a 私の愛したLaravel 18 「簡潔なコード」の条件 • 要件をCRUD操作として表現できること • 操作対象リソースがEloquentモデルであること 破綻への道 • CRUD操作として表現できない要件 • 横断的関⼼事や対向要件 • 他の設計パターンを中途半端に導⼊

Slide 20

Slide 20 text

破綻の理由を分析する: Laravelの特徴 2025/3/22 #phperkaigi #a 私の愛したLaravel 19 • リソース志向フレームワーク Øアクティブレコードパターン • フルスタックフレームワーク • ドキュメンテーションポリシー

Slide 21

Slide 21 text

アクティブレコードパターン 2025/3/22 #phperkaigi #a 私の愛したLaravel 20 @mpyw (2023) 「Laravelへの異常な愛情」トーク中ポスト @hanhan1978 (2023) 「Laravelへの異常な愛情」トーク中ポスト

Slide 22

Slide 22 text

アクティブレコードパターン 2025/3/22 #phperkaigi #a 私の愛したLaravel 21 アクティブレコード データベーステーブルまたはビューの⾏をラップし、データベースアク セスをカプセル化してデータにドメインロジックを追加するオブジェク ト。 (中略) 動作⽅法 アクティブレコードの本質はドメインモデルであり、アクティブレコー ド内のクラスは、基盤となるデータベースレコード構造とほぼ⼀致して いる。それぞれのアクティブレコードはデータベースへの保存や読み込 みを⾏い、またデータに適⽤されるドメインロジックとしての役割も果 たす。 マーチン ファウラー (著), テクノロジックアート (翻訳), 翔泳社 エンタープライズアプリケーションアーキテクチャパターン

Slide 23

Slide 23 text

アクティブレコードパターン 2025/3/22 #phperkaigi #a 私の愛したLaravel 22 ⼤嶋勇樹 (2023) 改めて整理するアプリケーション設計の基本 重要なのは「基本を押さえ、適したものを採⽤すること」 “本来の役割”を押さえたアプリケーション設計

Slide 24

Slide 24 text

アクティブレコードパターン 2025/3/22 #phperkaigi #a 私の愛したLaravel 23 チーズがトマトを!トマトがチーズをひき⽴てる! 「ハーモニー」っつーんですかあ〜 「味の調和」っつーんですかあ〜っ たとえるならサイモンとガーファンクルのデュエット! ウッチャンに対するナンチャン! ⾼森朝雄の原作に対するちばてつやの「あしたのジョー」! 荒⽊⾶呂彦 (1998), 集英社 ジョジョの奇妙な冒険 33巻 LaravelとEloqunt MVCとアクティブレコードは お互いがお互いをひき⽴て合う関係

Slide 25

Slide 25 text

アクティブレコードパターン 2025/3/22 #phperkaigi #a 私の愛したLaravel 24 「簡潔なコード」の条件 • アクティブレコードパターンを受容していること 破綻への道 • 他の設計パターンを中途半端に導⼊ • 導⼊したパターンの良さは活かせない • Laravelの良さも活かせない • RDB以外のインフラ層やドメイン層の変則的な要件

Slide 26

Slide 26 text

破綻の理由を分析する: Laravelの特徴 2025/3/22 #phperkaigi #a 私の愛したLaravel 25 • リソース志向フレームワーク • アクティブレコードパターン Øフルスタックフレームワーク • ドキュメンテーションポリシー

Slide 27

Slide 27 text

フルスタックフレームワーク 2025/3/22 #phperkaigi #a 私の愛したLaravel 26 Laravelが対応する外部システムのドライバ • データベース • MariaDB, MySQL, PostgreSQL, SQLite, SQL Server • キャッシュ • Memcached, Redis, DynamoDB, MongoDB, ファイル, データベー ス • メール • SMTP, Mailgun, Postmark, Resend, Amazon SES, MailerSend • ストレージ • ファイル, S3, FTP, SFTP • ログ • ファイル, Slack, syslog, 標準エラー出⼒, 任意のMonologハンドラ

Slide 28

Slide 28 text

フルスタックフレームワーク 2025/3/22 #phperkaigi #a 私の愛したLaravel 27 このコードが操作する対象 • リクエスト (ファイルアップロード) • ストレージ • 認証 • Eloquentモデル • メール Laravelの「フルスタック」は リクエストやEloquentを媒介に 調和ながら動作する public function update(UpdateUserRequest $request) { $filename = $request ->file('photo') // 1. アップロードファイルを ->store('uploads'); // 2. ストレージに保存し // 3. 認証ユーザーを更新 $request->user()->update([ 'photo' => $filename, ]); // 4. 完了のメールを送信した後 $request->user()->notify(new ProfileUpdated()); // 5. 更新結果をjsonで返却 return new UserResource($request->user()); }

Slide 29

Slide 29 text

フルスタックフレームワーク 2025/3/22 #phperkaigi #a 私の愛したLaravel 28 「簡潔なコード」の条件 • 採⽤している技術や設計がLaravelのカバーする「フ ルスタック」に収まること 破綻への道 • 「フルスタック」に収まらない設計上の要件 • 例: Eloquent⾮対応機能を使いたい • 例: 独⾃の認証基盤と連携したい • 例: Laravelが対応していない製品を使いたい

Slide 30

Slide 30 text

破綻の理由を分析する: Laravelの特徴 2025/3/22 #phperkaigi #a 私の愛したLaravel 29 • リソース志向フレームワーク • アクティブレコードパターン • フルスタックフレームワーク Øドキュメンテーションポリシー

Slide 31

Slide 31 text

ドキュメンテーションポリシー 2025/3/22 #phperkaigi #a 私の愛したLaravel 30 重⼤な原⽂の間違いがない限り、和⽂に情報を付け 加えません。LaravelメンテナのTaylor Otwell⽒は、 「ドキュメントに何もかも詰め込んでしまうと、メ ンテしづらく、読み⼿にも負担になる」という⽅針 を持っています。 その⽅針を基準に現在の情報量と なっています。それを尊重しています。 (中略) ⽇本語訳に⾜りない部分は、原⽂に⾜りない部分で す。公式ドキュメントに⾜りない部分を追加するPR を⾏うか、もしくはブログ記事などを書き、内容を 補ってください。 Laravel公式ドキュメント 翻訳リポジトリ メンテナンス⽅針

Slide 32

Slide 32 text

ドキュメンテーションポリシー 2025/3/22 #phperkaigi #a 私の愛したLaravel 31 「簡潔なコード」の条件 • 多くの⼈が「簡潔なコード」を最短で書けるよう、ド キュメントは必要最⼩限の情報のみに絞っている。 破綻への道 • 未知の要件への対応で「ドキュメント未記載 = ⾮対応」 と判断しアプリケーション側で独⾃に実装してしまう

Slide 33

Slide 33 text

破綻の理由を分析する 2025/3/22 #phperkaigi #a 私の愛したLaravel 32 傾向 • ドキュメントの情報不⾜もあり、 • Laravel対応外に⾒える要件や将来の⼤規模化への対応のため、 • 他の設計パターンを中途半端に導⼊しながら、 • コードを不必要に難解にしてしまう。 対策 • 対策1: 責務を分離した疎結合で⾼凝集な設計(本トークのテーマ外) • 対策2: Laravelを拡張する(本トークのテーマ) • ドキュメントの情報不⾜のためある程度のコードリーディングが必要

Slide 34

Slide 34 text

2025/3/22 #phperkaigi #a 私の愛したLaravel 33 Extend Rails instead of melting it with something else RAILS を他のものと混ぜ合わせるのではなく、拡張しよう Growing the Rails Way is possible if you don't fight the framework フレームワークと戦わなければ Rails Way で成⻑することは可能です Vladimir Dementyev (2024) Rails Way, or the highway Kaigi on Rails 2024

Slide 35

Slide 35 text

拡張の典型的なパターン

Slide 36

Slide 36 text

フレームワークの設計を知る ポイント: Laravel本体の機能は「サービスプ ロバイダ」で初期化されている Laravel⽇本語ドキュメント サービスプロバイダ リクエストのライフサイクル 2025/3/22 #phperkaigi #a 私の愛したLaravel 35

Slide 37

Slide 37 text

フレームワークの設計を知る 2025/3/22 #phperkaigi #a 私の愛したLaravel 36 ポイント: 「サービスプロバイダ」はアプリ ケーション側でも利⽤可能 Laravel⽇本語ドキュメント サービスプロバイダ イントロダクション // bootstrap/providers.php return [ App¥Providers¥AppServiceProvider::class, ];

Slide 38

Slide 38 text

フレームワークの設計を知る 2025/3/22 #phperkaigi #a 私の愛したLaravel 37 ポイント: 「サービスプロバイダ」で「サービ スコンテナによる結合」「イベント リスナ」「ミドルウェア」などの登 録することでLaravelを初期化する Laravel⽇本語ドキュメント サービスプロバイダ イントロダクション

Slide 39

Slide 39 text

フレームワークの設計を知る 2025/3/22 #phperkaigi #a 私の愛したLaravel 38 岡⽥ 正平(2023) いろいろなフレームワークの仕組みを index.php から読み解こう / index.php of each framework PHPerKaigi 2023

Slide 40

Slide 40 text

フレームワークの設計を知る 2025/3/22 #phperkaigi #a 私の愛したLaravel 39 • アプリケーション側とほぼ同じコードで、Laravelは⾃分⾃⾝を初期 化している。 • Laravel側の初期化は、アプリケーション側で上書きできる。 // サービスの登録(公式ドキュメント抜粋) $this->app->bind(Transistor::class, function (Application $app) { return new Transistor($app- >make(PodcastParser::class)); }); // イベントリスナの登録(公式ドキュメント抜粋) Event::listen( PodcastProcessed::class, SendPodcastNotification::class, ); // Laravel本体: DBファサードの実体を登録する処理 // src/Illuminate/Database/DatabaseServiceProvider.php $this->app->singleton('db.factory', function ($app) { return new ConnectionFactory($app); }); $this->app->singleton('db', function ($app) { return new DatabaseManager($app, $app['db.factory']); }); $this->app->bind('db.connection', function ($app) { return $app['db']->connection(); });

Slide 41

Slide 41 text

Laravelを簡単に拡張して良いのか? 2025/3/22 #phperkaigi #a 私の愛したLaravel 40 補⾜: • 「ブレーキングチェンジ」にはクラスやインター フェースのシグネチャ変更も含まれる • シグネチャ変更はアップグレードガイドに記載さ れる • 通常の機能と⽐較しシグネチャへの破壊的変更の 頻度は低い • 意図せずエコシステムを壊す可能性 Laravel⽇本語ドキュメント リリースノート バージョニング規約

Slide 42

Slide 42 text

拡張の設計⽅針 2025/3/22 #phperkaigi #a 私の愛したLaravel 41 • 偶有的複雑性をアプリケーションの外に追い出す • Laravel と Infrastructure の依存の向きを逆転 • Laravel と Laravel Extension の境界は腐敗防⽌層で保護 • アプリケーション層では「Laravelの書き⽅」だけ考えれば良い 従来のレイヤー化 拡張を介したレイヤー化

Slide 43

Slide 43 text

拡張の実例 2025/3/22 #phperkaigi #a 私の愛したLaravel 42 Øconfig() で拡張を設定する例 • 専⽤APIから拡張を登録する例 • サービスコンテナ結合を上書きする例 • イベントやミドルウェアから介⼊する例 • インターフェースを利⽤する例

Slide 44

Slide 44 text

認証の拡張 2025/3/22 #phperkaigi #a 私の愛したLaravel 43 例えば次のような要件 1. 認証は外部の認証基盤が発⾏するJWTトークンを使う 2. JWTトークンの検証はアプリケーション側の要件 3. デコード結果には認証認可に必要な全ての情報が含まれる 4. ユーザー情報はJWTトークンのみから取得可能 • ⼆重管理を避けるためデータベースは使わない Eloquentにもデータベースにも依存しない場合 デフォルト状態のままでは Laravelの認証の仕組みを使えない

Slide 45

Slide 45 text

認証の拡張: 素朴な実装 2025/3/22 #phperkaigi #a 私の愛したLaravel 44 public function create(StoreTaskRequest $request) { $jwtToken = $request->cookie('jwt-token'); abort_unless($jwtToken, 400); try { $jwtUser = JWT::decode($jwtToken, new Key(config('app.jwt_secret'), 'HS256')); } catch (¥Exception $e) { abort(400, $e->getMessage()); } if (!in_array('post_edit', $jwtUser->permissions ?? [])) { abort(403); } return Task::create([ ...$request->validated(), 'user_id' => $jwtUser->id, ]); }

Slide 46

Slide 46 text

認証の拡張: 従来のレイヤー化 2025/3/22 #phperkaigi #a 私の愛したLaravel 45 本質と関係のない処理を別クラスに切り出す • クラスは単⼀責務であるべき • 「認証結果」を保持するDTOクラス • 「認証」を⾏うサービスクラス • 「認可」を⾏うサービスクラス • サービスクラスをサービスコンテナへ登録 • コントローラーへへサービスを注⼊

Slide 47

Slide 47 text

認証の拡張: 単⼀責務とDI 2025/3/22 #phperkaigi #a 私の愛したLaravel 46 public function create(StoreTaskRequest $request) { $jwtUser = ($this->authenticationService)($request); $canCreate = ($this->authorizationService)('post.create', $jwtUser); abort_unless($canCreate, 403); return Task::create([ ...$request->validated(), 'user_id' => $jwtUser->id, ]); }

Slide 48

Slide 48 text

認証の拡張: 本来はこう書きたい 2025/3/22 #phperkaigi #a 私の愛したLaravel 47 public function create(StoreTaskRequest $request) { return Task::create([ ...$request->validated(), 'user_id' => $request->user()->id, ]); } // 認可: app/Policies/TaskPolicy.php public function create(JwtUser $jwtUser) { return $jwtUser ->permissions ->contains('task.create'); } // routes/api.php Route::post('posts', [TaskController::class, 'create']) ->middleware(['auth:jwtUser', 'can:create,task']); • 認証認可はミドルウェアに⾏わせる • 認可ロジックはポリシーとして実装 • コントローラーは何も変える必要がない • 認証結果は $request->user() から取得可能

Slide 49

Slide 49 text

解法: 認証ガードのカスタマイズ 2025/3/22 #phperkaigi #a 私の愛したLaravel 48 • Auth::viaRequest()でリクエストによる 認証を実装 • このクロージャでJWTトークンの検証やデ コードを⾏う • 実装した認証をconfig/auth.phpへ設定 Laravel⽇本語ドキュメント 認証 クロージャリクエストガード

Slide 50

Slide 50 text

解法: 認証結果の変更 2025/3/22 #phperkaigi #a 私の愛したLaravel 49 • 認証結果を⽰すクラスを実装 • class JwtUser implements Authenticatable {} • カスタムユーザープロバイダ や認証ガードからこれを返し 認証の動作を変更する: • Auth::user() • $request->user() • ここまでの拡張でLaravelの 認証認可を引き続き使える

Slide 51

Slide 51 text

理想的な例: Laravel Doctrine 2025/3/22 #phperkaigi #a 私の愛したLaravel 50 データベースやセッショ ンを使った完全な例は Laravel Doctrineのコード を参考 Laravel Doctrine - Authentication

Slide 52

Slide 52 text

拡張の実例 2025/3/22 #phperkaigi #a 私の愛したLaravel 51 • config() で拡張を設定する例 Ø専⽤APIから拡張を登録する例 • サービスコンテナ結合を上書きする例 • イベントやミドルウェアから介⼊する例 • インターフェースを利⽤する例

Slide 53

Slide 53 text

データベース機能の拡張 2025/3/22 #phperkaigi #a 私の愛したLaravel 52 $ psql app=# explain select * from users where id = 284 and exists (select * from posts where users.id = posts.user_id); QUERY PLAN ---------------------------------------------------------------------------------------- Nested Loop Semi Join (cost=4.36..22.94 rows=1 width=1798) -> Index Scan using users_pkey on users (cost=0.14..8.16 rows=1 width=1798) Index Cond: (id = 284) -> Bitmap Heap Scan on posts (cost=4.22..14.76 rows=9 width=8) Recheck Cond: (user_id = 284) -> Bitmap Index Scan on posts_user_id_index (cost=0.00..4.22 rows=9 width=0) Index Cond: (user_id = 284) EXPLAINによるSQL実⾏計画の表⽰

Slide 54

Slide 54 text

データベース機能の拡張 2025/3/22 #phperkaigi #a 私の愛したLaravel 53 > App¥Models¥User::query()->where('id', 284)->whereHas('posts')->explain(); = Illuminate¥Support¥Collection {#6038 all: [ {#5969 +"QUERY PLAN": "Nested Loop Semi Join (cost=4.36..22.94 rows=1 width=1798)"}, {#5970 +"QUERY PLAN": " -> Index Scan using users_pkey on users (cost=0.14..8.16 rows=1 width=1798)"}, {#5968 +"QUERY PLAN": " Index Cond: (id = '284'::bigint)"}, {#5919 +"QUERY PLAN": " -> Bitmap Heap Scan on posts (cost=4.22..14.76 rows=9 width=8)"}, {#6185 +"QUERY PLAN": " Recheck Cond: (user_id = '284'::bigint)"}, {#6186 +"QUERY PLAN": " -> Bitmap Index Scan on posts_user_id_index (cost=0.00..4.22 rows=9 width=0)"}, {#6184 +"QUERY PLAN": " Index Cond: (user_id = '284'::bigint)"}, ], } $query->explain() でORMが⽣成したSQL実⾏計画を直接表⽰

Slide 55

Slide 55 text

データベース機能の拡張 2025/3/22 #phperkaigi #a 私の愛したLaravel 54 app=# explain (format json) select * from users where id = 220 and exists (select * from posts where users.id = posts.user_id); QUERY PLAN ---------------------------------------------------- [ + { + "Plan": { + "Node Type": "Nested Loop", + // 省略 + "Plans": [ + { + "Node Type": "Index Scan", + // 省略 + "Index Cond": "(id = 220)" + }, + { + "Node Type": "Bitmap Heap Scan", + // 省略 + "Plans": [ + // 省略 + ] + } + ] + } + } + ] (1 row) PostgreSQL EXPLAIN FORMAT JSON EXPLAIN結果をJSON形式で出⼒: • メトリクスが構造化されている • 問い合わせのツリー構造が表現されている $query->explain() でこれを取得したい

Slide 56

Slide 56 text

解法: データベースドライバの上書き 2025/3/22 #phperkaigi #a 私の愛したLaravel 55 • 登録名 • pgsql • mysql • sqlite • ... • ドライバを返すクロー ジャ // データベースドライバ登録API // src/Illuminate/Database/Connection.php class Connection implements ConnectionInterface { /** * Register a connection resolver. * * @param string $driver * @param ¥Closure $callback * @return void */ public static function resolverFor($driver, Closure $callback) { static::$resolvers[$driver] = $callback; } }

Slide 57

Slide 57 text

データベースドライバの構成 2025/3/22 #phperkaigi #a 私の愛したLaravel 56 • Illuminate¥Database¥Query¥Grammars¥*Grammar: SQL⽂の⽣成 • Illuminate¥Database¥Query¥Processors¥*Processor: SQL⽂の実⾏と結果取得 • Illuminate¥Database¥*Connection: データベース接続とPDOドライバの管理 • Illuminate¥Database¥Query¥Builder: クエリビルダ 拡張の対象に応じてカスタマイズするクラスが異なる

Slide 58

Slide 58 text

explain()をオーバーライドした独⾃クエリビルダ 2025/3/22 #phperkaigi #a 私の愛したLaravel 57 // app/Database/ExtendedPostgresQueryBuilder.php class ExtendedPostgresQueryBuilder extends Illuminate¥Database¥Query¥Builder { #[Override] public function explain() { $sql = $this->toSql(); $bindings = $this->getBindings(); // $explanation = $this->getConnection()->select('EXPLAIN '.$sql, $bindings); $json = $this->getConnection()->scalar('EXPLAIN(FORMAT JSON) '.$sql, $bindings); // return new Collection($explanation); return new Collection(json_decode($json, true)); } }

Slide 59

Slide 59 text

独⾃クエリビルダを参照する独⾃ドライバ 2025/3/22 #phperkaigi #a 私の愛したLaravel 58 // app/Database/ExtendedPostgresConnection.php class ExtendedPostgresConnection extends PostgresConnection { #[Override] public function query() { return new ExtendedPostgresQueryBuilder( $this, $this->getQueryGrammar(), $this->getPostProcessor() ); } }

Slide 60

Slide 60 text

独⾃ドライバをサービスプロバイダから登録 2025/3/22 #phperkaigi #a 私の愛したLaravel 59 // app/Providers/AppServiceProvider.php: register() Connection::resolverFor( 'pgsql', // この例では `pgsql` ドライバを上書き(別名を指定し新規作成も可能) fn () => new ExtendedPostgresConnection(...func_get_args()) );

Slide 61

Slide 61 text

実⾏結果 2025/3/22 #phperkaigi #a 私の愛したLaravel 60 > App¥Models¥User::query()->where('id', 220)->whereHas('posts')->explain() = Illuminate¥Support¥Collection {#5970 all: [ [ "Plan" => [ "Node Type" => "Nested Loop", // 省略 "Inner Unique" => false, "Plans" => [ // 省略 ], ], ], ], }

Slide 62

Slide 62 text

完全な例: Laravel PostgreSQL Enhanced 2025/3/22 #phperkaigi #a 私の愛したLaravel 61 •マイグレーションの 拡張 •migrateコマンドの拡 張 •クエリビルダの拡張 •Eloquentの拡張 •SQL⽂法の拡張 tpetry/laravel-postgresql-enhanced

Slide 63

Slide 63 text

拡張の実例 2025/3/22 #phperkaigi #a 私の愛したLaravel 62 • config() で拡張を設定する例 • 専⽤APIから拡張を登録する例 Øサービスコンテナ結合を上書きする例 • イベントやミドルウェアから介⼊する例 • インターフェースを利⽤する例

Slide 64

Slide 64 text

ビューの出⼒変更 2025/3/22 #phperkaigi #a 私の愛したLaravel 63 例: 次のような要件 • サーバ側でHTMLをレンダ • 「モバイル」「デスクトップ」「タブレット」それ ぞれ異なるHTMLを返す場合がある • テンプレートは必ず3種類⽤意されているとは限らない

Slide 65

Slide 65 text

ビューの出⼒変更: 素朴な実装 2025/3/22 #phperkaigi #a 私の愛したLaravel 64 // ログイン: レスポンシブデザイン テンプレートは共通 Route::get( '/login', fn () => view('login') ); // ダッシュボード: 3デバイス個別テンプレート Route::get( 'dashboard', fn (MobileDetect $m) => match (true) { $m->isMobile() => view('dashboard.mobile'), $m->isTablet() => view('dashboard.tablet'), default => view('dashboard.desktop'), } ); // アラート一覧画面: モバイルのみ別テンプレート Route::get( 'alerts', fn (MobileDetect $m) => match (true) { $m->isMobile() => view('alerts.index.mobile'), default => view('alerts.index'), } ); // アラート詳細画面: デスクトップ表示 Route::get( 'alerts/{alert}', fn (Alert $alert) => view('alerts.show', [ 'alert' => $alert, ]), ); 画⾯毎のデバイス対応有無をコントローラーに個別に実装

Slide 66

Slide 66 text

解法: view()の動作を変更 2025/3/22 #phperkaigi #a 私の愛したLaravel 65 // コントローラーは表示デバイスに一切関心を持たない // ログイン画面 Route('login', fn () => view('login')); // ダッシュボード画面 Route('dashboard', fn () => view('dashboard')); // アラート一覧画面 Route('alerts', fn () => view('alerts.index')); // アラート詳細画面 Route('alerts/{alert}', fn (Alert $alert) => view('alerts.show', [ 'alert' => $alert ]) ); resources/views/ ├─ dashboard.mobile.blade.php ├─ dashboard.desktop.blade.php ├─ dashboard.tablet.blade.php ├─ login.blade.php └─ alerts ├─ index.mobile.blade.php ├─ index.blade.php └─ show.blade.php デバイスの種別と 命名規則に応じたファイルの有無で 表⽰するテンプレートを決定

Slide 67

Slide 67 text

ビューの出⼒変更: Laravel側の初期化 2025/3/22 #phperkaigi #a 私の愛したLaravel 66 // src/Illuminate/View/ViewServiceProvider.php public function registerViewFinder() { $this->app->bind('view.finder', function ($app) { return new FileViewFinder($app['files'], $app['config']['view.paths']); }); } view.finderサービス: テンプレートファイルを探す処理

Slide 68

Slide 68 text

ビューの出⼒変更: Laravel側を上書き 2025/3/22 #phperkaigi #a 私の愛したLaravel 67 class ExtendedFileViewFinder extends FileViewFinder { #[¥Override] protected function getPossibleViewFiles($name) { $deviceType = match(true) { $this->mobileDetect->isMobile() => 'mobile', $this->mobileDetect->isTablet() => 'tablet', default => 'desktop', } $extensions = array_merge( array_map( fn (string $path) => $deviceType.'.'.$path, $this->extensions ), $this->extensions ); return array_map(fn ($extension) => str_replace('.', '/', $name).'.'.$extension, $extensions); } } テンプレート名に応じて 探すべき全てのファイル名を返す (*.blade.php, *.php, *.md, ...) 元のコードはこの1⾏(相当)のみ オリジナルの拡張⼦リストと デバイス毎拡張⼦をマージ

Slide 69

Slide 69 text

ビューの出⼒変更: view.finder上書き 2025/3/22 #phperkaigi #a 私の愛したLaravel 68 view.finderサービスを⾃作クラスで上書き // app/Providers/AppServiceProvider.php: register() $this->app->bind('view.finder', function ($app) { // return new FileViewFinder($app['files'], $app['config']['view.paths']); return new ExtendedFileViewFinder( new MobileDetect(), $app['files'], $app['config']['view.paths'], ); });

Slide 70

Slide 70 text

拡張すべきサービスを探す 2025/3/22 #phperkaigi #a 私の愛したLaravel 69 • 結合キーが指定されたファ サードは(理論的には)全て 機能を変更可能 • 注意: 1つのファサードが複数 の結合キー(機能)を持つ ケース • 例: filesystemと filesystem.disk • __call()による処理の移譲 • 注意: 依存がLaravel内部で連 鎖しており表記以外の箇所の 拡張が必要になるケース • view.finderはこの表に載っ ていない

Slide 71

Slide 71 text

拡張すべきサービスを探す 2025/3/22 #phperkaigi #a 私の愛したLaravel 70 Laravel 12.2 の時点で50個のサービスプロバイダが存在 $ cd src/Illuminate $ ls **/*Provider.php Auth/AuthServiceProvider.php Filesystem/FilesystemServiceProvider.php Queue/Failed/DatabaseFailedJobProvider.php Auth/DatabaseUserProvider.php Foundation/Providers/ArtisanServiceProvider.php Queue/Failed/DatabaseUuidFailedJobProvider.php Auth/EloquentUserProvider.php Foundation/Providers/ComposerServiceProvider.php Queue/Failed/DynamoDbFailedJobProvider.php Auth/Passwords/PasswordResetServiceProvider.php Foundation/Providers/ConsoleSupportServiceProvider.php Queue/Failed/FileFailedJobProvider.php Broadcasting/BroadcastServiceProvider.php Foundation/Providers/FormRequestServiceProvider.php Queue/Failed/NullFailedJobProvider.php Bus/BusServiceProvider.php Foundation/Providers/FoundationServiceProvider.php Queue/Failed/PrunableFailedJobProvider.php Cache/CacheServiceProvider.php Foundation/Support/Providers/AuthServiceProvider.php Queue/QueueServiceProvider.php Concurrency/ConcurrencyServiceProvider.php Foundation/Support/Providers/EventServiceProvider.php Redis/RedisServiceProvider.php Contracts/Auth/UserProvider.php Foundation/Support/Providers/RouteServiceProvider.php Routing/RoutingServiceProvider.php Contracts/Cache/LockProvider.php Hashing/HashServiceProvider.php Session/SessionServiceProvider.php Contracts/Support/DeferrableProvider.php Log/Context/ContextServiceProvider.php Support/AggregateServiceProvider.php Contracts/Support/MessageProvider.php Log/LogServiceProvider.php Support/ServiceProvider.php Cookie/CookieServiceProvider.php Mail/MailServiceProvider.php Testing/ParallelTestingServiceProvider.php Database/DatabaseServiceProvider.php Notifications/NotificationServiceProvider.php Translation/TranslationServiceProvider.php Database/MigrationServiceProvider.php Pagination/PaginationServiceProvider.php Validation/ValidationServiceProvider.php Encryption/EncryptionServiceProvider.php Pipeline/PipelineServiceProvider.php View/ViewServiceProvider.php Events/EventServiceProvider.php Queue/Failed/CountableFailedJobProvider.php cd src/Illuminate; ls **/*Provider.php

Slide 72

Slide 72 text

拡張の実例 2025/3/22 #phperkaigi #a 私の愛したLaravel 71 • config() で拡張を設定する例 • 専⽤APIから拡張を登録する例 • サービスコンテナ結合を上書きする例 Øイベントやミドルウェアから介⼊する例 • インターフェースを利⽤する例

Slide 73

Slide 73 text

Laravel Debug Bar: バーの表⽰ 2025/3/22 #phperkaigi #a 私の愛したLaravel 72 インストールするだけで表⽰される • 割愛: Laravel⽇本語ドキュメント/ パッケージ開発/パッケージディスカ バリーを参照 全画⾯に⾃動的に表⽰される: • ⾃動的に表⽰されユーザー側のコード 変更は不要 • レスポンスは追加のHTML/JSを含む • 追加はHTMLレスポンスのみ(JSONレ スポンスを壊したりしない) どうやって表⽰している?

Slide 74

Slide 74 text

解法: ミドルウェアから介⼊ 2025/3/22 #phperkaigi #a 私の愛したLaravel 73 •レスポンスが⽣成 された後 •ミドルウェアがそ れを書き換え // src/Middleware/InjectDebugbar.php public function handle($request, Closure $next) { // 省略 try { /** @var ¥Illuminate¥Http¥Response $response */ $response = $next($request); } catch (Throwable $e) { $response = $this->handleException($request, $e); } $this->debugbar->modifyResponse($request, $response); return $response; }

Slide 75

Slide 75 text

解法: ミドルウェアから介⼊ 2025/3/22 #phperkaigi #a 私の愛したLaravel 74 •

Slide 76

Slide 76 text

Laravel Debug Bar: メトリクスの収集 2025/3/22 #phperkaigi #a 私の愛したLaravel 75 ルーティング情報 処理フェーズ毎の所要時間 レンダされたビュー 実⾏されたSQLクエリ

Slide 77

Slide 77 text

解法: イベントリスナから介⼊ 2025/3/22 #phperkaigi #a 私の愛したLaravel 76 例: 実⾏されたSQLクエリ記録 • QueryExecutedイベントを購読 • 発⽕した場合その元となるSQLをスタック • レスポンス終了時にスタックされたクエリをレンダ // src/LaravelDebugbar.php $events->listen( function (¥Illuminate¥Database¥Events¥QueryExecuted $query) { // 省略 $this['queries']->addQuery($query); } );

Slide 78

Slide 78 text

Laravel Nightwatch 2025Q1 リリース予定 2025/3/22 #phperkaigi #a 私の愛したLaravel 77 Laravel Nightwatch Maru, (2024) Laravel Nightwatch - Laravel専⽤監視サービス

Slide 79

Slide 79 text

Laravel Nightwatch: メトリクス収集 2025/3/22 #phperkaigi #a 私の愛したLaravel 78 laravel/nightwatch/src/Hooks laravel/nightwatch/src/NightwatchServiceProvider.php

Slide 80

Slide 80 text

購読可能なイベントを探す 2025/3/22 #phperkaigi #a 私の愛したLaravel 79 クラスとして実装されたイベントの⼀覧 • ls src/**/Events/*.php • Laravel 12.2 の時点で111個のイベントが存在 ⽂字列をキーに発⽕するイベント • 検証⽬的であればEvent::listen('*')で収集可能 • 実⽤的にはEvent::listen('foo.*')で収集

Slide 81

Slide 81 text

拡張の実例 2025/3/22 #phperkaigi #a 私の愛したLaravel 80 • config() で拡張を設定する例 • 専⽤APIから拡張を登録する例 • サービスコンテナ結合を上書きする例 • イベントやミドルウェアから介⼊する例 Øインターフェースを利⽤する例

Slide 82

Slide 82 text

Laravel Starter Kit + Inertia.js 2025/3/22 #phperkaigi #a 私の愛したLaravel 81 # Laravel Installerを最新版に更新 $ composer global update laravel/installer ## またはインストール # composer global require laravel/installer # プロジェクトの作成とビルド $ laravel new --react --phpunit --npm starterkit $ cd starterkit $ npm run build:ssr # 開発サーバを2つ起動 prompt-1 $ ./artisan serve prompt-2 $ ./artisan inertia:start-ssr # ブラウザで開く $ open http://localhost:8000

Slide 83

Slide 83 text

Inertia.jsの画⾯遷移 2025/3/22 #phperkaigi #a 私の愛したLaravel 82 トップページを表⽰ ログインページへ遷移

Slide 84

Slide 84 text

Inertia.jsの画⾯遷移 2025/3/22 #phperkaigi #a 私の愛したLaravel 83 ログインページをリロード JavaScriptオフでリロード

Slide 85

Slide 85 text

Next.js型フルスタックアーキテクチャ 2025/3/22 #phperkaigi #a 私の愛したLaravel 84 1. 初回ロードはフルページ遷移(SSR) 2. 画⾯遷移時はSPA遷移(CSR) 画⾯遷移の⽅法に応じてレスポンスが変わる: • SSR: propsでcomponentをレンダしたtext/html • CSR: 遷移先ルートとpropsを含むapplication/json

Slide 86

Slide 86 text

Inertia.js利⽤時のコントローラー実装 2025/3/22 #phperkaigi #a 私の愛したLaravel 85 • view() ではなくInertia::render()を返す • 「テンプレートと変数(componentとprops)」という構造は維持 • アプリケーションはレスポンス形式(SSR /CSR)を⼀切関知しない // app/Http/Controllers/Settings/PasswordController.php public function edit(Request $request): Response { return Inertia::render('settings/password', [ 'mustVerifyEmail' => $request->user() instanceof MustVerifyEmail, 'status' => $request->session()->get('status'), ]); }

Slide 87

Slide 87 text

そもそもコントローラーは何を返せるのか? 2025/3/22 #phperkaigi #a 私の愛したLaravel 86 public function question(): Response { // 完全なレスポンス return new Response( '君の電話番号は何番かね?', Response::HTTP_OK ); } public function answer(): int { // スカラー値 return 5761455; } public function me(): View { // ビュー return view('root', [ 'age' => 10, ]); } public function index(): Model { // Eloquentコレクション return PrimeNumber::all(); }

Slide 88

Slide 88 text

Laravel Framework: レスポンス⽣成 2025/3/22 #phperkaigi #a 私の愛したLaravel 87 処理の概要: • mixed $responseを • Response $responseに変換 • その際$requestを参照できる 今回のポイント: • mixed $responseが • interface Responsableのサブ タイプだった場合 • Responsable::toResponse()の 変換を⾏う • ここでも$requestを参照できる // src/Illuminate/Routing/Router.php /** * Static version of prepareResponse. * * @param ¥Symfony¥Component¥HttpFoundation¥Request $request * @param mixed $response * @return ¥Symfony¥Component¥HttpFoundation¥Response */ public static function toResponse($request, $response) { if ($response instanceof Responsable) { $response = $response->toResponse($request); } // 省略 return $response->prepare($request); }

Slide 89

Slide 89 text

CSR/SSR両対応: Inertia::Render() 2025/3/22 #phperkaigi #a 私の愛したLaravel 88 「レスポンス」ではなく 「レスポンスの⽣成に必要なcomponentとpropsを持つオブジェクト」 // src/ResponseFactory.php public function render(string $component, $props = []): Response { if ($props instanceof Arrayable) { $props = $props->toArray(); } return new Response( $component, array_merge($this->sharedProps, $props), $this->rootView, $this->getVersion(), $this->encryptHistory ?? config('inertia.history.encrypt', false), ); } Inertia\Response

Slide 90

Slide 90 text

解法: インターフェースを介しLaravelを制御 2025/3/22 #phperkaigi #a 私の愛したLaravel 89 // inertia-laravel/src/Response.php class Response implements Responsable { public function toResponse($request) { // 省略 if ($request->header(Header::INERTIA)) { // 注: Illuminate¥Http¥JsonResponse return new JsonResponse($page, 200, [Header::INERTIA => 'true']); } // 注: Illuminate¥Http¥Response return ResponseFactory::view($this->rootView, $this->viewData + ['page' => $page]); } } Inertia¥Response::toResponse($request)をLaravelに「呼ばせる」構造 public const INERTIA = 'X-Inertia';

Slide 91

Slide 91 text

解法: インターフェースを介しLaravelを制御 2025/3/22 #phperkaigi #a 私の愛したLaravel 90

Slide 92

Slide 92 text

インターフェースによる依存逆転 2025/3/22 #phperkaigi #a 私の愛したLaravel 91 • ⼊⼒(例: DIによる注⼊)ではなく出⼒(例: return)の依存を逆転 • ⾃分たちが依存先を作成するのではなく既存の依存先を利⽤する • 依存元の実装を「呼ぶ」のではなく「呼ばせる」 「典型的に紹介される例」との⽐較 • 典型的な例「コントローラーは表⽰に関知しない」 • 関知せずに済むための追加のコードがコントローラーに必要 • (結局、関知しているのでは?) • 今回の例「コントローラーは表⽰に関知しない」 • コントローラー内での追加のコードは不要 • (単純なコードの置き換えで完結)

Slide 93

Slide 93 text

最適なアプローチを選ぶ

Slide 94

Slide 94 text

ਖ਼ղ͑͞ग़ͤ͹॓୊͸ऴΘΓɺ ͱ͍͏΋ͷͰ͸ͳ͍ɻ ΁౸ண͢Δɺ ΋͏Ұͭผͷಓॱ͕͋ΔΜͩͧɻ ͦ͜Λ௨ͬͯΈ͍ͨͱࢥΘͳ͍͔͍ʁ ⼩川 洋⼦ (2003), 新潮社 博⼠の愛した数式

Slide 95

Slide 95 text

偶有的な要件にどう対応するか? 2025/3/22 #phperkaigi #a 私の愛したLaravel 94 メリット リスク 素朴に実装 • 難度が低い • 保守性 パターンの適⽤ • 保守性の向上 • FWロックインのリスク低減 • オンボーディングコスト • 品質が⼈に依存 • FWとの相性 FWの拡張 • 多くの箇所の難度が下がる • FWメリットを引き続き享受 • ⼀部の難度が極端に上がる • 属⼈化 • 魔改造

Slide 96

Slide 96 text

リスクを最⼩化にするには 2025/3/22 #phperkaigi #a 私の愛したLaravel 95 • MUST: 「拡張」にはドメインロジックを決して持ち込まない • アプリケーション開発者ではなくライブラリ作者の気持ちで設計する • 実装すべき要件は他のアプリケーションでも再利⽤可能か? • 例えばパッケージとして公開できるか? • SHOULD: 標準状態のLaravelから開発者体験やインターフェースを変えない • シニア: 裏側で動いている動作の仕組みは他の開発者に意識させない」 • ビギナー: 仕組みは解らないが『いつものコード』で動く」 • laravel/*パッケージの仕様を参考にすると良い • SHOULD: アプリケーションから切り離した状態でテストできるようにする • Illuminate¥Foundation¥Testing¥TestCaseに依存しない • あるいは Testbenchを使う これらを満たせる場合「Laravelを拡張する」が有効

Slide 97

Slide 97 text

͋Δ΂͖΋ͷ͕͋Δ΂͖৔ॴʹೲ·ΓɺҰ੾ खΛՃ͑ͨΓɺ࡟ͬͨΓ͢Δ༨஍ͳͲͳ͘ɺ ੲ͔ΒͣͬͱมΘΒͣͦ͏Ͱ͔͋ͬͨͷΑ͏ ͳɺͦͯ͜͠Ε͔Β΋Ӭԕʹͦ͏Ͱ͋Γଓ͚ Δ֬৴ʹຬͪͨঢ়ଶɻ ⼩川 洋⼦ (2003), 新潮社 博⼠の愛した数式