LTS の 5.5 を使ってます。
つまり、こういうことがやりたいのです。
Route::controller(UserController::class)->group(function () {
Route::get('/users', '@index');
Route::get('/users/create', '@create');
Route::post('/users', '@store');
Route::get('/users/{user}', '@show');
Route::get('/users/{user}/edit', '@edit');
Route::put('/users/{user}', '@update');
Route::delete('/users/{user}', '@destroy');
});
↑のまんまだと Route::resource()
だけで十分ですが、その他のアクションも微妙にほしいとき、↑のような書き方をしたいことがあります。
試行錯誤
Route::get()
などの第2引数の action は Route::namespace()
と結合されるので次のようにできるかと思いましたが、
Route::namespace(UserController::class)->group(function () {
Route::get('/users', '@index');
Route::get('/users/create', '@create');
Route::post('/users', '@store');
Route::get('/users/{user}', '@show');
Route::get('/users/{user}/edit', '@edit');
Route::put('/users/{user}', '@update');
Route::delete('/users/{user}', '@destroy');
});
これだと App\Controllers\UserController\@index
などとなって App\Controllers\UserController\
なんてクラスは無いと怒られます、末尾の \
が余分です。
Illuminate\Routing\Router::prependGroupNamespace()
で namespace と action をつなげるときに \
が付け足されています。
protected function prependGroupNamespace($class)
{
$group = end($this->groupStack);
return isset($group['namespace']) && strpos($class, '\\') !== 0
? $group['namespace'].'\\'.$class : $class;
}
action(↑のコードでは $class
)の先頭が @
のときは \
を付け足さない、とかやってくれれば良かったのですが。。。ので、Router
を継承して prependGroupNamespace()
の動きを変えます。
class AppRouter extends Router
{
protected function prependGroupNamespace($class)
{
$group = end($this->groupStack);
if (isset($group['namespace']) && strpos($class, '\\') !== 0) {
if ($class[0] === '@') {
$class = $group['namespace'] . $class;
} else {
$class = $group['namespace'] . '\\' . $class;
}
}
return $class;
}
}
そしてサービスプロバイダで router
を差し替えます。
public function register()
{
$this->app->singleton('router', function ($app) {
return new AppRouter($app['events'], $app);
});
}
しかしこれはうまくいきません。それどころか全てのリクエストが 404 になります。
詳しく追ってみたところ、↑のサービスプロバイダの定義よりも先に Router
がインスタンス化されていました。\Illuminate\Foundation\Http\Kernel
のコンストラクタ引数に Router
があるので、初っ端の
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
の時点で Router
がインスタンス化され、リクエストのディスパッチにはこれが使用されます。
アプリケーションのサービスプロバイダが実行されるのはもっと後なので、Router
をアプリケーションで差し替えると、ルート定義される Router
と、リクエストのディスパッチに使用される Router
が別のインスタンスになってしまい、ルート定義が空でディスパッチされてしまいます。
方法その1:RouteMatched
イベントで Route
オブジェクトを書き換える
ルートがマッチした後、ディスパッチされる前に Router
の RouteMatched
イベントが発生するので、そのイベントで Route
オブジェクトを書き換えます。
Route::matched(function (RouteMatched $event) {
if (isset($event->route->action['uses']) && is_string($event->route->action['uses'])) {
$event->route->action['uses'] = preg_replace('/\\\\@/', '@', $event->route->action['uses']);
}
});
一応これで一応動きますが、この方法だと artisan route:list
でエラーになります・・
方法その2:Kernel
より先に router
のサービス定義する
Kernel
がインスタンス化されるより先に router
のサービスを定義すれば良いので、bootstrap/app.php
にサービスの定義を追加します。
$app->singleton('router', function ($app) {
return new AppRouter($app['events'], $app);
});
artisan route:list
も動くので「方法その1:RouteMatched
イベントで Route
オブジェクトを書き換える」よりよいと思います。
ただし、以下のようにルートを定義するとやっぱり駄目です。
Route::namespace(UserController::class)->group(function () {
Route::get('/users')->uses('@index');
});
この場合は Router
ではなく Route
クラスの中のメソッドで \
が補完されてます。そして Route
を差し替えるのは簡単ではなさそうです・・・
さいごに
うーん、どっちの方法も無理矢理感があるので微妙かな・・・もっとうまい方法無かろうか。
下記を見るに、かつては Route::controller
というものがあって、これはそれと同じものを使えるようにするもののようです。
ただコントローラーのメソッドが暗黙にルートになるもののようで、期待しているものとはちょっと違いそうでした(ルート自体は明示的に書きたい)。