Movatterモバイル変換


[0]ホーム

URL:


Upgrade to PRO for Only $50/Year—Limited-Time Offer! 🔥
Speaker DeckSpeaker Deck
Speaker Deck

私の愛したLaravel 〜レールを超えたその先へ〜

Avatar for 武田 憲太郎 武田 憲太郎
March 21, 2025

私の愛したLaravel 〜レールを超えたその先へ〜

PHPerKaigi 2025

2025-03-22 13:35 -
Track A / レギュラートーク(40分)

動画:https://youtu.be/LezQt4znOyE
プロポーザル:https://fortee.jp/phperkaigi-2025/proposal/3ff0f775-9601-4bf6-a1cf-9911b11787b3

Avatar for 武田 憲太郎

武田 憲太郎

March 21, 2025
Tweet

More Decks by 武田 憲太郎

See All by 武田 憲太郎

Other Decks in Programming

See All in Programming

Featured

See All Featured

Transcript

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

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

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

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

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

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

    5
  7. ⾃⼰紹介: PHP⽇本語マニュアル php/doc-ja#150 PHP 8.4 マニュアル翻訳状況 PHP Manual プロパティフック 2025/3/22

    #phperkaigi #a 私の愛したLaravel 6
  8. ⾃⼰紹介: 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
  9. アジェンダ 2025/3/22 #phperkaigi #a 私の愛したLaravel 8 • Laravelアプリケーションはなぜ破綻するのか? • 拡張の典型的なパターン

    • 拡張の実例 • config() で拡張を設定する例 • 専⽤APIから拡張を登録する例 • サービスコンテナ結合を上書きする例 • イベントやミドルウェアから介⼊する例 • インターフェースを利⽤する例 • 最適なアプローチを選ぶ
  10. Laravelアプリケーションはなぜ破綻するのか?

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

    と思考停⽌してはいけない
  12. 破綻の理由を分析する: Laravelの特徴 2025/3/22 #phperkaigi #a 私の愛したLaravel 11 Øリソース志向フレームワーク • アクティブレコードパターン

    • フルスタックフレームワーク • ドキュメンテーションポリシー
  13. リソース志向フレームワーク 2025/3/22 #phperkaigi #a 私の愛したLaravel 12 武⽥ 憲太郎 (2023) Laravelへの異常な愛情

    または私は如何にして⼼配するのを⽌めてEloquentを愛するようになったか PHPerKaigi 2023
  14. リソース志向フレームワーク 2025/3/22 #phperkaigi #a 私の愛したLaravel 13 @mizchi (2024) Rails vs

    Node.js 最終章 「Prisma」 Cloudflare Meet-up Tokyo Vol.6
  15. リソース志向フレームワーク 2025/3/22 #phperkaigi #a 私の愛したLaravel 14 @MadakaHeri (2024) Laravelが如何にダメで時代遅れかを説明する

  16. リソース志向 + 簡潔なコード 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
  17. リソース志向 + 簡潔なコード 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} 削除
  18. リソース志向 + 簡潔なコード 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(); // 削除 } }
  19. リソース志向 + 簡潔なコード 2025/3/22 #phperkaigi #a 私の愛したLaravel 18 「簡潔なコード」の条件 •

    要件をCRUD操作として表現できること • 操作対象リソースがEloquentモデルであること 破綻への道 • CRUD操作として表現できない要件 • 横断的関⼼事や対向要件 • 他の設計パターンを中途半端に導⼊
  20. 破綻の理由を分析する: Laravelの特徴 2025/3/22 #phperkaigi #a 私の愛したLaravel 19 • リソース志向フレームワーク Øアクティブレコードパターン

    • フルスタックフレームワーク • ドキュメンテーションポリシー
  21. アクティブレコードパターン 2025/3/22 #phperkaigi #a 私の愛したLaravel 20 @mpyw (2023) 「Laravelへの異常な愛情」トーク中ポスト @hanhan1978

    (2023) 「Laravelへの異常な愛情」トーク中ポスト
  22. アクティブレコードパターン 2025/3/22 #phperkaigi #a 私の愛したLaravel 21 アクティブレコード データベーステーブルまたはビューの⾏をラップし、データベースアク セスをカプセル化してデータにドメインロジックを追加するオブジェク ト。

    (中略) 動作⽅法 アクティブレコードの本質はドメインモデルであり、アクティブレコー ド内のクラスは、基盤となるデータベースレコード構造とほぼ⼀致して いる。それぞれのアクティブレコードはデータベースへの保存や読み込 みを⾏い、またデータに適⽤されるドメインロジックとしての役割も果 たす。 マーチン ファウラー (著), テクノロジックアート (翻訳), 翔泳社 エンタープライズアプリケーションアーキテクチャパターン
  23. アクティブレコードパターン 2025/3/22 #phperkaigi #a 私の愛したLaravel 22 ⼤嶋勇樹 (2023) 改めて整理するアプリケーション設計の基本 重要なのは「基本を押さえ、適したものを採⽤すること」

    “本来の役割”を押さえたアプリケーション設計
  24. アクティブレコードパターン 2025/3/22 #phperkaigi #a 私の愛したLaravel 23 チーズがトマトを!トマトがチーズをひき⽴てる! 「ハーモニー」っつーんですかあ〜 「味の調和」っつーんですかあ〜っ たとえるならサイモンとガーファンクルのデュエット!

    ウッチャンに対するナンチャン! ⾼森朝雄の原作に対するちばてつやの「あしたのジョー」! 荒⽊⾶呂彦 (1998), 集英社 ジョジョの奇妙な冒険 33巻 LaravelとEloqunt MVCとアクティブレコードは お互いがお互いをひき⽴て合う関係
  25. アクティブレコードパターン 2025/3/22 #phperkaigi #a 私の愛したLaravel 24 「簡潔なコード」の条件 • アクティブレコードパターンを受容していること 破綻への道

    • 他の設計パターンを中途半端に導⼊ • 導⼊したパターンの良さは活かせない • Laravelの良さも活かせない • RDB以外のインフラ層やドメイン層の変則的な要件
  26. 破綻の理由を分析する: Laravelの特徴 2025/3/22 #phperkaigi #a 私の愛したLaravel 25 • リソース志向フレームワーク •

    アクティブレコードパターン Øフルスタックフレームワーク • ドキュメンテーションポリシー
  27. フルスタックフレームワーク 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ハンドラ
  28. フルスタックフレームワーク 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()); }
  29. フルスタックフレームワーク 2025/3/22 #phperkaigi #a 私の愛したLaravel 28 「簡潔なコード」の条件 • 採⽤している技術や設計がLaravelのカバーする「フ ルスタック」に収まること

    破綻への道 • 「フルスタック」に収まらない設計上の要件 • 例: Eloquent⾮対応機能を使いたい • 例: 独⾃の認証基盤と連携したい • 例: Laravelが対応していない製品を使いたい
  30. 破綻の理由を分析する: Laravelの特徴 2025/3/22 #phperkaigi #a 私の愛したLaravel 29 • リソース志向フレームワーク •

    アクティブレコードパターン • フルスタックフレームワーク Øドキュメンテーションポリシー
  31. ドキュメンテーションポリシー 2025/3/22 #phperkaigi #a 私の愛したLaravel 30 重⼤な原⽂の間違いがない限り、和⽂に情報を付け 加えません。LaravelメンテナのTaylor Otwell⽒は、 「ドキュメントに何もかも詰め込んでしまうと、メ

    ンテしづらく、読み⼿にも負担になる」という⽅針 を持っています。 その⽅針を基準に現在の情報量と なっています。それを尊重しています。 (中略) ⽇本語訳に⾜りない部分は、原⽂に⾜りない部分で す。公式ドキュメントに⾜りない部分を追加するPR を⾏うか、もしくはブログ記事などを書き、内容を 補ってください。 Laravel公式ドキュメント 翻訳リポジトリ メンテナンス⽅針
  32. ドキュメンテーションポリシー 2025/3/22 #phperkaigi #a 私の愛したLaravel 31 「簡潔なコード」の条件 • 多くの⼈が「簡潔なコード」を最短で書けるよう、ド キュメントは必要最⼩限の情報のみに絞っている。

    破綻への道 • 未知の要件への対応で「ドキュメント未記載 = ⾮対応」 と判断しアプリケーション側で独⾃に実装してしまう
  33. 破綻の理由を分析する 2025/3/22 #phperkaigi #a 私の愛したLaravel 32 傾向 • ドキュメントの情報不⾜もあり、 •

    Laravel対応外に⾒える要件や将来の⼤規模化への対応のため、 • 他の設計パターンを中途半端に導⼊しながら、 • コードを不必要に難解にしてしまう。 対策 • 対策1: 責務を分離した疎結合で⾼凝集な設計(本トークのテーマ外) • 対策2: Laravelを拡張する(本トークのテーマ) • ドキュメントの情報不⾜のためある程度のコードリーディングが必要
  34. 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
  35. 拡張の典型的なパターン

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

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

    サービスプロバイダ イントロダクション // bootstrap/providers.php return [ App¥Providers¥AppServiceProvider::class, ];
  38. フレームワークの設計を知る 2025/3/22 #phperkaigi #a 私の愛したLaravel 37 ポイント: 「サービスプロバイダ」で「サービ スコンテナによる結合」「イベント リスナ」「ミドルウェア」などの登

    録することでLaravelを初期化する Laravel⽇本語ドキュメント サービスプロバイダ イントロダクション
  39. フレームワークの設計を知る 2025/3/22 #phperkaigi #a 私の愛したLaravel 38 岡⽥ 正平(2023) いろいろなフレームワークの仕組みを index.php

    から読み解こう / index.php of each framework PHPerKaigi 2023
  40. フレームワークの設計を知る 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(); });
  41. Laravelを簡単に拡張して良いのか? 2025/3/22 #phperkaigi #a 私の愛したLaravel 40 補⾜: • 「ブレーキングチェンジ」にはクラスやインター フェースのシグネチャ変更も含まれる

    • シグネチャ変更はアップグレードガイドに記載さ れる • 通常の機能と⽐較しシグネチャへの破壊的変更の 頻度は低い • 意図せずエコシステムを壊す可能性 Laravel⽇本語ドキュメント リリースノート バージョニング規約
  42. 拡張の設計⽅針 2025/3/22 #phperkaigi #a 私の愛したLaravel 41 • 偶有的複雑性をアプリケーションの外に追い出す • Laravel

    と Infrastructure の依存の向きを逆転 • Laravel と Laravel Extension の境界は腐敗防⽌層で保護 • アプリケーション層では「Laravelの書き⽅」だけ考えれば良い 従来のレイヤー化 拡張を介したレイヤー化
  43. 拡張の実例 2025/3/22 #phperkaigi #a 私の愛したLaravel 42 Øconfig() で拡張を設定する例 • 専⽤APIから拡張を登録する例

    • サービスコンテナ結合を上書きする例 • イベントやミドルウェアから介⼊する例 • インターフェースを利⽤する例
  44. 認証の拡張 2025/3/22 #phperkaigi #a 私の愛したLaravel 43 例えば次のような要件 1. 認証は外部の認証基盤が発⾏するJWTトークンを使う 2.

    JWTトークンの検証はアプリケーション側の要件 3. デコード結果には認証認可に必要な全ての情報が含まれる 4. ユーザー情報はJWTトークンのみから取得可能 • ⼆重管理を避けるためデータベースは使わない Eloquentにもデータベースにも依存しない場合 デフォルト状態のままでは Laravelの認証の仕組みを使えない
  45. 認証の拡張: 素朴な実装 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, ]); }
  46. 認証の拡張: 従来のレイヤー化 2025/3/22 #phperkaigi #a 私の愛したLaravel 45 本質と関係のない処理を別クラスに切り出す • クラスは単⼀責務であるべき

    • 「認証結果」を保持するDTOクラス • 「認証」を⾏うサービスクラス • 「認可」を⾏うサービスクラス • サービスクラスをサービスコンテナへ登録 • コントローラーへへサービスを注⼊
  47. 認証の拡張: 単⼀責務と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, ]); }
  48. 認証の拡張: 本来はこう書きたい 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() から取得可能
  49. 解法: 認証ガードのカスタマイズ 2025/3/22 #phperkaigi #a 私の愛したLaravel 48 • Auth::viaRequest()でリクエストによる 認証を実装

    • このクロージャでJWTトークンの検証やデ コードを⾏う • 実装した認証をconfig/auth.phpへ設定 Laravel⽇本語ドキュメント 認証 クロージャリクエストガード
  50. 解法: 認証結果の変更 2025/3/22 #phperkaigi #a 私の愛したLaravel 49 • 認証結果を⽰すクラスを実装 •

    class JwtUser implements Authenticatable {} • カスタムユーザープロバイダ や認証ガードからこれを返し 認証の動作を変更する: • Auth::user() • $request->user() • ここまでの拡張でLaravelの 認証認可を引き続き使える
  51. 理想的な例: Laravel Doctrine 2025/3/22 #phperkaigi #a 私の愛したLaravel 50 データベースやセッショ ンを使った完全な例は

    Laravel Doctrineのコード を参考 Laravel Doctrine - Authentication
  52. 拡張の実例 2025/3/22 #phperkaigi #a 私の愛したLaravel 51 • config() で拡張を設定する例 Ø専⽤APIから拡張を登録する例

    • サービスコンテナ結合を上書きする例 • イベントやミドルウェアから介⼊する例 • インターフェースを利⽤する例
  53. データベース機能の拡張 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実⾏計画の表⽰
  54. データベース機能の拡張 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実⾏計画を直接表⽰
  55. データベース機能の拡張 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() でこれを取得したい
  56. 解法: データベースドライバの上書き 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; } }
  57. データベースドライバの構成 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: クエリビルダ 拡張の対象に応じてカスタマイズするクラスが異なる
  58. 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)); } }
  59. 独⾃クエリビルダを参照する独⾃ドライバ 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() ); } }
  60. 独⾃ドライバをサービスプロバイダから登録 2025/3/22 #phperkaigi #a 私の愛したLaravel 59 // app/Providers/AppServiceProvider.php: register() Connection::resolverFor(

    'pgsql', // この例では `pgsql` ドライバを上書き(別名を指定し新規作成も可能) fn () => new ExtendedPostgresConnection(...func_get_args()) );
  61. 実⾏結果 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" => [ // 省略 ], ], ], ], }
  62. 完全な例: Laravel PostgreSQL Enhanced 2025/3/22 #phperkaigi #a 私の愛したLaravel 61 •マイグレーションの

    拡張 •migrateコマンドの拡 張 •クエリビルダの拡張 •Eloquentの拡張 •SQL⽂法の拡張 tpetry/laravel-postgresql-enhanced
  63. 拡張の実例 2025/3/22 #phperkaigi #a 私の愛したLaravel 62 • config() で拡張を設定する例 •

    専⽤APIから拡張を登録する例 Øサービスコンテナ結合を上書きする例 • イベントやミドルウェアから介⼊する例 • インターフェースを利⽤する例
  64. ビューの出⼒変更 2025/3/22 #phperkaigi #a 私の愛したLaravel 63 例: 次のような要件 • サーバ側でHTMLをレンダ

    • 「モバイル」「デスクトップ」「タブレット」それ ぞれ異なるHTMLを返す場合がある • テンプレートは必ず3種類⽤意されているとは限らない
  65. ビューの出⼒変更: 素朴な実装 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, ]), ); 画⾯毎のデバイス対応有無をコントローラーに個別に実装
  66. 解法: 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 デバイスの種別と 命名規則に応じたファイルの有無で 表⽰するテンプレートを決定
  67. ビューの出⼒変更: 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サービス: テンプレートファイルを探す処理
  68. ビューの出⼒変更: 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⾏(相当)のみ オリジナルの拡張⼦リストと デバイス毎拡張⼦をマージ
  69. ビューの出⼒変更: 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'], ); });
  70. 拡張すべきサービスを探す 2025/3/22 #phperkaigi #a 私の愛したLaravel 69 • 結合キーが指定されたファ サードは(理論的には)全て 機能を変更可能

    • 注意: 1つのファサードが複数 の結合キー(機能)を持つ ケース • 例: filesystemと filesystem.disk • __call()による処理の移譲 • 注意: 依存がLaravel内部で連 鎖しており表記以外の箇所の 拡張が必要になるケース • view.finderはこの表に載っ ていない
  71. 拡張すべきサービスを探す 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
  72. 拡張の実例 2025/3/22 #phperkaigi #a 私の愛したLaravel 71 • config() で拡張を設定する例 •

    専⽤APIから拡張を登録する例 • サービスコンテナ結合を上書きする例 Øイベントやミドルウェアから介⼊する例 • インターフェースを利⽤する例
  73. Laravel Debug Bar: バーの表⽰ 2025/3/22 #phperkaigi #a 私の愛したLaravel 72 インストールするだけで表⽰される

    • 割愛: Laravel⽇本語ドキュメント/ パッケージ開発/パッケージディスカ バリーを参照 全画⾯に⾃動的に表⽰される: • ⾃動的に表⽰されユーザー側のコード 変更は不要 • レスポンスは追加のHTML/JSを含む • 追加はHTMLレスポンスのみ(JSONレ スポンスを壊したりしない) どうやって表⽰している?
  74. 解法: ミドルウェアから介⼊ 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; }
  75. 解法: ミドルウェアから介⼊ 2025/3/22 #phperkaigi #a 私の愛したLaravel 74 • </body> の直前にHTMLを挿⼊

    • ⾒つからない場合は末尾に挿⼊ // src/LaravelDebugbar.php public function injectDebugbar(Response $response) { // 省略 $pos = strripos($content, '</body>'); if (false !== $pos) { $content = substr($content, 0, $pos) . $widget . substr($content, $pos); } else { $content = $content . $widget; } // 省略 }
  76. Laravel Debug Bar: メトリクスの収集 2025/3/22 #phperkaigi #a 私の愛したLaravel 75 ルーティング情報

    処理フェーズ毎の所要時間 レンダされたビュー 実⾏されたSQLクエリ
  77. 解法: イベントリスナから介⼊ 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); } );
  78. Laravel Nightwatch 2025Q1 リリース予定 2025/3/22 #phperkaigi #a 私の愛したLaravel 77 Laravel

    Nightwatch Maru, (2024) Laravel Nightwatch - Laravel専⽤監視サービス
  79. Laravel Nightwatch: メトリクス収集 2025/3/22 #phperkaigi #a 私の愛したLaravel 78 laravel/nightwatch/src/Hooks laravel/nightwatch/src/NightwatchServiceProvider.php

  80. 購読可能なイベントを探す 2025/3/22 #phperkaigi #a 私の愛したLaravel 79 クラスとして実装されたイベントの⼀覧 • ls src/**/Events/*.php

    • Laravel 12.2 の時点で111個のイベントが存在 ⽂字列をキーに発⽕するイベント • 検証⽬的であればEvent::listen('*')で収集可能 • 実⽤的にはEvent::listen('foo.*')で収集
  81. 拡張の実例 2025/3/22 #phperkaigi #a 私の愛したLaravel 80 • config() で拡張を設定する例 •

    専⽤APIから拡張を登録する例 • サービスコンテナ結合を上書きする例 • イベントやミドルウェアから介⼊する例 Øインターフェースを利⽤する例
  82. 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
  83. Inertia.jsの画⾯遷移 2025/3/22 #phperkaigi #a 私の愛したLaravel 82 トップページを表⽰ ログインページへ遷移

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

  85. Next.js型フルスタックアーキテクチャ 2025/3/22 #phperkaigi #a 私の愛したLaravel 84 1. 初回ロードはフルページ遷移(SSR) 2. 画⾯遷移時はSPA遷移(CSR)

    画⾯遷移の⽅法に応じてレスポンスが変わる: • SSR: propsでcomponentをレンダしたtext/html • CSR: 遷移先ルートとpropsを含むapplication/json
  86. 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'), ]); }
  87. そもそもコントローラーは何を返せるのか? 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(); }
  88. 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); }
  89. 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
  90. 解法: インターフェースを介し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';
  91. 解法: インターフェースを介しLaravelを制御 2025/3/22 #phperkaigi #a 私の愛したLaravel 90

  92. インターフェースによる依存逆転 2025/3/22 #phperkaigi #a 私の愛したLaravel 91 • ⼊⼒(例: DIによる注⼊)ではなく出⼒(例: return)の依存を逆転

    • ⾃分たちが依存先を作成するのではなく既存の依存先を利⽤する • 依存元の実装を「呼ぶ」のではなく「呼ばせる」 「典型的に紹介される例」との⽐較 • 典型的な例「コントローラーは表⽰に関知しない」 • 関知せずに済むための追加のコードがコントローラーに必要 • (結局、関知しているのでは?) • 今回の例「コントローラーは表⽰に関知しない」 • コントローラー内での追加のコードは不要 • (単純なコードの置き換えで完結)
  93. 最適なアプローチを選ぶ

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

  95. 偶有的な要件にどう対応するか? 2025/3/22 #phperkaigi #a 私の愛したLaravel 94 メリット リスク 素朴に実装 •

    難度が低い • 保守性 パターンの適⽤ • 保守性の向上 • FWロックインのリスク低減 • オンボーディングコスト • 品質が⼈に依存 • FWとの相性 FWの拡張 • 多くの箇所の難度が下がる • FWメリットを引き続き享受 • ⼀部の難度が極端に上がる • 属⼈化 • 魔改造
  96. リスクを最⼩化にするには 2025/3/22 #phperkaigi #a 私の愛したLaravel 95 • MUST: 「拡張」にはドメインロジックを決して持ち込まない •

    アプリケーション開発者ではなくライブラリ作者の気持ちで設計する • 実装すべき要件は他のアプリケーションでも再利⽤可能か? • 例えばパッケージとして公開できるか? • SHOULD: 標準状態のLaravelから開発者体験やインターフェースを変えない • シニア: 裏側で動いている動作の仕組みは他の開発者に意識させない」 • ビギナー: 仕組みは解らないが『いつものコード』で動く」 • laravel/*パッケージの仕様を参考にすると良い • SHOULD: アプリケーションから切り離した状態でテストできるようにする • Illuminate¥Foundation¥Testing¥TestCaseに依存しない • あるいは Testbenchを使う これらを満たせる場合「Laravelを拡張する」が有効
  97. ͋Δ΂͖΋ͷ͕͋Δ΂͖৔ॴʹೲ·ΓɺҰ੾ खΛՃ͑ͨΓɺ࡟ͬͨΓ͢Δ༨஍ͳͲͳ͘ɺ ੲ͔ΒͣͬͱมΘΒͣͦ͏Ͱ͔͋ͬͨͷΑ͏ ͳɺͦͯ͜͠Ε͔Β΋Ӭԕʹͦ͏Ͱ͋Γଓ͚ Δ֬৴ʹຬͪͨঢ়ଶɻ ⼩川 洋⼦ (2003), 新潮社 博⼠の愛した数式


[8]ページ先頭

©2009-2025 Movatter.jp