LoginSignup
0
0

More than 3 years have passed since last update.

Laravel のルート定義でコントローラーのクラス名とメソッド名を別々に書きたい

Posted at

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 オブジェクトを書き換える

ルートがマッチした後、ディスパッチされる前に RouterRouteMatched イベントが発生するので、そのイベントで 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 というものがあって、これはそれと同じものを使えるようにするもののようです。

ただコントローラーのメソッドが暗黙にルートになるもののようで、期待しているものとはちょっと違いそうでした(ルート自体は明示的に書きたい)。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0