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

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

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

PHPerKaigi 2025
#phperkaigi #a

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

https://fortee.jp/phperkaigi-2025/proposal/3ff0f775-9601-4bf6-a1cf-9911b11787b3

武田 憲太郎

March 21, 2025
Tweet

More Decks by 武田 憲太郎

Other Decks in Programming

Transcript

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

    • 拡張の実例 • config() で拡張を設定する例 • 専⽤APIから拡張を登録する例 • サービスコンテナ結合を上書きする例 • イベントやミドルウェアから介⼊する例 • インターフェースを利⽤する例 • 最適なアプローチを選ぶ
  3. リソース志向フレームワーク 2025/3/22 #phperkaigi #a 私の愛したLaravel 12 武⽥ 憲太郎 (2023) Laravelへの異常な愛情

    または私は如何にして⼼配するのを⽌めてEloquentを愛するようになったか PHPerKaigi 2023
  4. リソース志向 + 簡潔なコード 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
  5. リソース志向 + 簡潔なコード 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} 削除
  6. リソース志向 + 簡潔なコード 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(); // 削除 } }
  7. リソース志向 + 簡潔なコード 2025/3/22 #phperkaigi #a 私の愛したLaravel 18 「簡潔なコード」の条件 •

    要件をCRUD操作として表現できること • 操作対象リソースがEloquentモデルであること 破綻への道 • CRUD操作として表現できない要件 • 横断的関⼼事や対向要件 • 他の設計パターンを中途半端に導⼊
  8. アクティブレコードパターン 2025/3/22 #phperkaigi #a 私の愛したLaravel 21 アクティブレコード データベーステーブルまたはビューの⾏をラップし、データベースアク セスをカプセル化してデータにドメインロジックを追加するオブジェク ト。

    (中略) 動作⽅法 アクティブレコードの本質はドメインモデルであり、アクティブレコー ド内のクラスは、基盤となるデータベースレコード構造とほぼ⼀致して いる。それぞれのアクティブレコードはデータベースへの保存や読み込 みを⾏い、またデータに適⽤されるドメインロジックとしての役割も果 たす。 マーチン ファウラー (著), テクノロジックアート (翻訳), 翔泳社 エンタープライズアプリケーションアーキテクチャパターン
  9. アクティブレコードパターン 2025/3/22 #phperkaigi #a 私の愛したLaravel 23 チーズがトマトを!トマトがチーズをひき⽴てる! 「ハーモニー」っつーんですかあ〜 「味の調和」っつーんですかあ〜っ たとえるならサイモンとガーファンクルのデュエット!

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

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

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

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

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

    ンテしづらく、読み⼿にも負担になる」という⽅針 を持っています。 その⽅針を基準に現在の情報量と なっています。それを尊重しています。 (中略) ⽇本語訳に⾜りない部分は、原⽂に⾜りない部分で す。公式ドキュメントに⾜りない部分を追加するPR を⾏うか、もしくはブログ記事などを書き、内容を 補ってください。 Laravel公式ドキュメント 翻訳リポジトリ メンテナンス⽅針
  17. 破綻の理由を分析する 2025/3/22 #phperkaigi #a 私の愛したLaravel 32 傾向 • ドキュメントの情報不⾜もあり、 •

    Laravel対応外に⾒える要件や将来の⼤規模化への対応のため、 • 他の設計パターンを中途半端に導⼊しながら、 • コードを不必要に難解にしてしまう。 対策 • 対策1: 責務を分離した疎結合で⾼凝集な設計(本トークのテーマ外) • 対策2: Laravelを拡張する(本トークのテーマ) • ドキュメントの情報不⾜のためある程度のコードリーディングが必要
  18. 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
  19. フレームワークの設計を知る 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(); });
  20. Laravelを簡単に拡張して良いのか? 2025/3/22 #phperkaigi #a 私の愛したLaravel 40 補⾜: • 「ブレーキングチェンジ」にはクラスやインター フェースのシグネチャ変更も含まれる

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

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

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

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

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

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

    class JwtUser implements Authenticatable {} • カスタムユーザープロバイダ や認証ガードからこれを返し 認証の動作を変更する: • Auth::user() • $request->user() • ここまでの拡張でLaravelの 認証認可を引き続き使える
  30. 拡張の実例 2025/3/22 #phperkaigi #a 私の愛したLaravel 51 • config() で拡張を設定する例 Ø専⽤APIから拡張を登録する例

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

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

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

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

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

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

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

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

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

    Nightwatch Maru, (2024) Laravel Nightwatch - Laravel専⽤監視サービス
  57. 購読可能なイベントを探す 2025/3/22 #phperkaigi #a 私の愛したLaravel 79 クラスとして実装されたイベントの⼀覧 • ls src/**/Events/*.php

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

    専⽤APIから拡張を登録する例 • サービスコンテナ結合を上書きする例 • イベントやミドルウェアから介⼊する例 Øインターフェースを利⽤する例
  59. 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
  60. Next.js型フルスタックアーキテクチャ 2025/3/22 #phperkaigi #a 私の愛したLaravel 84 1. 初回ロードはフルページ遷移(SSR) 2. 画⾯遷移時はSPA遷移(CSR)

    画⾯遷移の⽅法に応じてレスポンスが変わる: • SSR: propsでcomponentをレンダしたtext/html • CSR: 遷移先ルートとpropsを含むapplication/json
  61. 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'), ]); }
  62. そもそもコントローラーは何を返せるのか? 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(); }
  63. 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); }
  64. 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
  65. 解法: インターフェースを介し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';
  66. インターフェースによる依存逆転 2025/3/22 #phperkaigi #a 私の愛したLaravel 91 • ⼊⼒(例: DIによる注⼊)ではなく出⼒(例: return)の依存を逆転

    • ⾃分たちが依存先を作成するのではなく既存の依存先を利⽤する • 依存元の実装を「呼ぶ」のではなく「呼ばせる」 「典型的に紹介される例」との⽐較 • 典型的な例「コントローラーは表⽰に関知しない」 • 関知せずに済むための追加のコードがコントローラーに必要 • (結局、関知しているのでは?) • 今回の例「コントローラーは表⽰に関知しない」 • コントローラー内での追加のコードは不要 • (単純なコードの置き換えで完結)
  67. 偶有的な要件にどう対応するか? 2025/3/22 #phperkaigi #a 私の愛したLaravel 94 メリット リスク 素朴に実装 •

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

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