1. HOME
  2. ブログ
  3. IT技術
  4. LaravelのRoute::get()へ渡すactionが何者なのか探ってみる

LaravelのRoute::get()へ渡すactionが何者なのか探ってみる

概要

Laravelの Route::get()が受け取る第2引数actionがどんな仕組みで動いているのか、ソースコードから探っていきます。

ゴール

Route::get()へ渡す第2引数actionが何を指しており、どんな仕組みで動作するのか理解することを目指します。

環境

  • PHP8.1
  • Laravel10.0

TL;DR

配列形式・文字列形式いずれも最終的には、 Controller@Methodの形式の文字列となる。
LaravelのRouteオブジェクトは、文字列に含まれる @を境界にコントローラクラスのインスタンスを組み立て、メソッド名から具体的なメソッドを呼び出す。

用語

  • action: Route::get()の第2引数。文字列 Controller@methodや配列 ['Controler', 'method']のような形式がある

背景

Laravelでパスと対応するコントローラを紐づけるとき、以下のように書くことが多いでしょう。

ここで、第2引数( [UserController::class, 'index'], UserController@index)に注目してみます。
ドキュメントや書籍では当たり前のように書かれていますが、公式ドキュメントでも書式を定義しているところは見当たりませんでした。
APIドキュメントでも型にのみ言及しています。

> get(string $uri, array|string|callable $action = null)

参考

用例からなんとなく使い方は分かりますが、それでは中々頭に残りません。暗記するのではなく、仕組みを理解して使いこないしたいところです。
そこで、ドキュメントに書かれていないならソースコードを覗くしかないじゃない、ということで Route::get()の第2引数actionがどのように参照されているのか探ってみます。
更に、actionをもとにコントローラのメソッドがどのように呼び出されるのか流れも見ておきたいです。

仕組みと処理の流れが掴めれば、ルーティングに関する設定を書くときも自信をもって臨めるようになるはずです。

※ 今回はコントローラのメソッドが呼び出される仕組みをたどりたいので、Callableをactionとして渡すケースは対象外とします。

私的前置き

色々書きましたが実際はLaravelのソースコードも読んでみたいな〜と思っていたので、好奇心が理由の大半だったりします(:

大方針

Laravelのソースコードを覗いてみることにしました。
ですが、コントローラのメソッドが呼ばれる仕組み1つをたどるにしても、すべてを読み解こうとすると膨大な量のコードが襲いかかってきます。ただ闇雲に読んでいくだけでは心が折れてしまいます。

そこで、目的を見据え、どうやって読んでいくのか簡単な地図をつくっておきます。
目的・方針が定まっていれば、広大なLaravelのコードの中から何を読むべきか取捨選択でき、迷うことなく目的地へといたれるはずです。

目的

なぜ Route::get()に文字列や配列が指定できるのか明らかにしたいです。
おそらくこれらをまとめて扱えるよう共通の形式に変換しているような処理があるはずなので、actionを加工しているっぽい処理を中心に探っていきます。

更に、actionをもとにどうやってコントローラのメソッドが呼ばれるのかおおまかな流れを見ておきたいです。
なんとなく、リフレクション的な仕組みでインスタンスをつくって動的にメソッドを呼び出しているんじゃないかなとは思いますが、正確な理解を得たいところです。

方針

Route::get()の引数がどんな形となるのかは、じっくり見ていきます。
actionを加工しているような処理をざっくりと探していき、該当しそうなところを見つけたら掘り下げていきます。

そして、コントローラのメソッドが実際に呼ばれる処理は、概要だけを掴むにとどめておきます。
リフレクションのような仕組みはコードが自ずと複雑になるので、すべてを読み解こうとすると長い道のりになってしまいます。
ですので、細部には入りこまず、何をしているのか流れを理解するところまでを目標とします。


ゴールと、ゴールにいたるまでの道のりが見えてきたので、早速Laravelのソースコードを探検していきます。

入り口

まずは Route::get()メソッドが定義されているところを探します。Routeクラスの定義へ飛んでみましょう。

getFacadeAccessor()なるメソッドは定義されていましたが、肝心の Route::get()が見当たりませんでした。
実はLaravelでは、Facadeと呼ばれるデザインパターンに従い、メソッド名(get)やインスタンス名( getFacadeAccessor()の戻り値 router)をもとに特定のクラスのメソッドを呼び出せる仕組みがあります。

参考

これを実現するためには裏で色々なモジュールが動いているのですが、本筋から外れてしまうので割愛します。
結論だけ書くと、 Route::get()という命令で実際に呼ばれているのは、Routerクラスのgetメソッドです。
Routerクラスを確認してみましょう。

actionを参照しているところにたどり着きました。
ようやく入り口に足を踏み入れることができたので、actionが処理される仕組みを追っていきます。

action

ここからは、メソッドの中身に潜りこんでactionがどのように参照されるのか見ていきます。

Router -actionをもとにRouteオブジェクトをつくって登録

Router::get()で呼ばれていた Router::addRoute()から始めます。

更に Router::createRoute()へと進みます。

必要な処理は、actionを受け取っているところだけなので、見るところを前半に絞っておきます。

action変換処理

actionを加工していそうな処理が見つかりました。
コメントをざっと読んでみると、コントローラを対象としたactionを共通の形式の配列に加工している予感がします。中身を詳しく見ていきます。

色々と条件が書かれているので、actionが文字列の場合、配列の場合で分けて考えてみます。
まず、どちらもClosureではないので、if文の中のreturn文が評価されます。

actionが文字列のときは is_string()からtrueが返ります。
一方、actionが配列のときは、 usesキーを持たないのでfalseが返ります。

思っていた挙動ではなさそうですが、続けて convertToControllerAction()へ進みます。

どうやらこのメソッドでは、文字列形式のactionを以下のようなaction arrayと呼ばれる形式に変換しているようです。
コントローラ名を残しておくことで必要なときに参照するための処理であるようですが、今回の目的とは関係なさそうでした。

ということで、元々見ていた Router::createRoute()に戻ります。

今度は Router::newRouteへと進みます。

Routeオブジェクトをつくっているようです。
actionを参照している処理はここで最後のようなので、必ず配列や文字列を共通の形式へと加工している処理が潜んでいるはずです。
じっくりと読み解いていきましょう。


Route-ルート情報を表現するオブジェクト

Routeクラスのインスタンスがつくられていたので、コンストラクタから読み始めます。

ここで重要そうなのは、 Route::parseAction()です。早速中身を覗いてみます。

いかにもな名前のクラスのメソッドが呼ばれています。 RouteAction::parse()へと移ります。

action加工処理

長いメソッドなので、抜粋して探っていきます。
ぱっと見では配列はCallableじゃなさそうだし...と素通りしそうなところですが、念には念を入れて呼び出し先を確認しておきます。

if文の条件に注目します。配列形式のactionはコントローラ名・メソッド名をそれぞれ文字列で渡しています。要素はコード内の $var[0]$var[1]と対応します。
よって、 ['Controller', 'method']のような配列は、 Reflector::isCallable()にてtrueと評価されます。

やや直感と反しますが、これはPHPの is_callableの仕様を踏襲しているようです。
参考


配列がreturn文を評価する対象であることが明らかになりました。
ですので、もう一度return文を見直しておきます。

配列から要素を取り出し、 @で連結しています。いかにもな処理ですね。

これはつまり、 ['Controller' 'method']のような配列から、 Controller@methodといった形式の文字列が組み立てられていることを表します。
まとめると、actionは文字列・配列で渡されても最終的には以下のような形になることが分かりました。


最初の目的-actionがどのように参照されるか

actionを加工している処理を探すことで、なぜ文字列や配列をactionとして渡すことができるのか解き明かせました。
改めて言葉にすると、文字列も配列も、指定したコントローラのメソッドを呼び出せるよう、 Controller@メソッド名の形式の文字列となることが分かりました。

これで Route::get()へ配列を渡すときも書式に迷わず書けそうです。

コントローラのメソッドはいかにして呼び出されるか

actionがどのような形となるかは理解できました。
更に理解を深めるために、actionを表す文字列からどのようにしてコントローラのメソッドが呼ばれるのか、流れを押さえておきます。

エントリーポイント

リクエストがどのように処理されるか、から始めると長くなってしまうので、ルーティング関連の処理の入り口までショートカットします。
リクエストから対応するコントローラを呼び出す処理は、 Router::dispatch()を起点とします。

Router::dispatchToRoute()へと進みます。

Router::findRoute()は、リクエストのパス情報をもとに、対応するRouteオブジェクトを探し出す処理です。
Routeオブジェクトは、先ほど見た通り、パスやactionを持っています。

例えば、 Route::get('/action/', ['Controller', 'method'])のような命令を書くとします。
すると、これをもとにuriプロパティが /action/・actionプロパティが Controller@methodのRouteオブジェクトがつくられます。
Router::findRoute()は、リクエストのパスが http://localhost/action/のような形式だったとき、前述のRouteオブジェクトを返します。

ひとまずここでは、 Route::get()でつくられたオブジェクトが取り出されたんだな〜、ということを理解しておきましょう。


コントローラのメソッドを呼び出していそうな Router::runRoute()を見てみます。

色々と複雑そうな処理が書かれているので、流れだけ押さえておきます。 Route::runRouteWithinStack()を契機に、 Route::run()が呼び出されます。

tryブロックに興味深そうな処理が書かれています。
Route::isControllerAction()は、actionに文字列や配列を渡していた場合、trueと評価されます。

コントローラのメソッドを呼び出していそうな Route::runController()へ進みます。

こういう処理は評価される順から見ていくのが定石です。
Route::getController()Route::getControllerMethod()はいずれも似たような処理なので、あわせて見ていきます。

Route::parseControllerCallback()にて、 Controller@method形式の文字列からコントローラ名やメソッド名が抽出されていそうな予感がします。
中身を見てみましょう。

これは、 Controller@methodのような文字列を ['Controller', 'method']形式の配列へ変換します。
よって、コントローラ・メソッドを得る処理は、コントローラ名・メソッド名それぞれをactionから取り出すことができます。

これを受けて、 Route::getController()をもう一度見てみます。

Illuminate\Container::make()はコントローラのクラス名を表現する文字列から、クラスのインスタンスを出力してくれます。
内部では色々と複雑な処理が書かれているので、ここでは振る舞いを押さえておくにとどめます。


ということで、コントローラクラスのインスタンス・呼び出したいメソッド名がそろいました。
あとは実際にメソッドを呼び出しているところを確認できれば、今回の目標は達成です。
既にたくさんの処理を追ってきましたが、ゴールも見えてきたので、もうひと頑張りしてみます。

コントローラクラスのインスタンス・メソッド名を受け取る Route::runController()へ戻ります。

ここで参照しているdispatcherは、 Illuminate\Routing\ControllerDispatcherです。
実際に呼ばれているのは、 ControllerDispatcher::dispatch()なので、どうやってコントローラのメソッドを呼び出すかだけ見ておきます。

return文に欲しかった答えが書かれています。
確かに、 Controller@method形式の文字列と対応するコントローラクラスのメソッドが呼ばれていることが確認できました。


流れを振り返る

コントローラのメソッドが呼ばれるまでの概要をたどるだけでも、長い道のりでした。
理解したことを整理するために、ここで流れを復習しておきましょう。

  • RouterがリクエストされたURL(パス)と対応するRouteオブジェクトを探索
  • Routeオブジェクトのrunメソッドが呼ばれる
  • Routeオブジェクトは、自身のaction( Controller@method形式の文字列)から、コントローラクラスのインスタンスとメソッド名を得る
  • 得られたものをもとに、ControllerDispatcherを介してコントローラのメソッドを呼び出し
余談: フレームワークのコードを追う意義

振り返ってみると、概ね予測したものと合致していました。なんとなく推測できるようなものを時間をかけて読む意義はあったのでしょうか。
※ 以降に書いているのは、個人の主観です。

これは、コードを読むこと自体によきことがあると思っています。
例えば、コードを実際に眺めることで、URLとコントローラを対応づける処理はRouterが指令塔になっていて、処理自体はRouteで完結させているんだなぁ、ということが分かります。
構造が見えてくれば、LaravelはWebアプリケーションのよくある処理をどんな考え方で実現しているのか、思考がうっすら見えてきます。
そして何より、コードを読むのは楽しいことです。

楽しみながらLaravelの理解を深められるとあれば、やってみない手はないでしょう。

まとめ

Laravelの Route::get()を入り口に、actionが動作する仕組みを見てきました。
分かってしまえば単純な話でしたが、コードを読む中でLaravelの理解が少しでも進めば幸いです。

関連記事

採用情報

\ あの有名サービスに参画!? /

バックエンドエンジニア

\ クリエイティブの最前線 /

フロントエンドエンジニア

\ 世界を変える…! /

Androidエンジニア

\ みんなが使うアプリを創る /

iOSエンジニア