前回の記事、ログイン後のリダイレクト先、その2では未ログインの状態でログインが必要なページにアクセスした場合、ログインページへ飛ばされ、ログインすると元々アクセスしたかったページへリダイレクトされる挙動について確認し、sessionのurl.intendedにセットされた値によってログイン後のリダイレクト先が決定する事が理解できました。しかし、url.intendedは一体いつどこでsessionにセットされたのでしょうか?気になったので処理を追ってみました。

注意

今回の記事に掲載しているソースコードはLaravel8から引用しました。Laravel9ではPHP8.0の記法が導入され一部異なりますが行っている処理は同じなのでそちらを使う場合でも参考になるかと思います。

Illuminate\Auth\Middleware\Authenticate.php

ルートのmiddlewareにauthを指定すると、そのページにリクエストが投げられた際にこのクラスのauthenticate()が実行されます。まずはここから見ていきましょう。このメソッドでは$this->auth->guard($guard)->check()にてユーザがログイン済みか否かを確認し、未ログインであればその後の$this->unauthenticated($request, $guards);が実行されます。unauthenticatedAuthenticationExceptionをスローします。

    /**
     * Determine if the user is logged in to any of the given guards.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  array  $guards
     * @return void
     *
     * @throws \Illuminate\Auth\AuthenticationException
     */
    protected function authenticate($request, array $guards)
    {
        if (empty($guards)) {
            $guards = [null];
        }

        foreach ($guards as $guard) {
            if ($this->auth->guard($guard)->check()) {    // ログイン済みかチェック
                return $this->auth->shouldUse($guard);
            }
        }
        $this->unauthenticated($request, $guards);    // 未ログインならこちらを実行
    }

    /**
     * Handle an unauthenticated user.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  array  $guards
     * @return void
     *
     * @throws \Illuminate\Auth\AuthenticationException
     */
    protected function unauthenticated($request, array $guards)
    {
        throw new AuthenticationException(    // 専用の例外をスロー
            'Unauthenticated.', $guards, $this->redirectTo($request)
        );
    }

Illuminate/Foundation/Exceptions/Handler.php

Laravelの全ての例外はApp\Exceptions\Handlerクラスにて処理されます。実際に実行される処理は親クラスである、Illuminate\Foundation\Exceptions\Handlerクラスに定義されています。例外をHTTPレスポンスに変換するrender()を見てみると、AuthenticationExceptionが投げられた場合の処理が記述されています。

    /**
     * Render an exception into an HTTP response.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Throwable  $e
     * @return \Symfony\Component\HttpFoundation\Response
     *
     * @throws \Throwable
     */
    public function render($request, Throwable $e)
    {
        ...

        if ($e instanceof HttpResponseException) {
            return $e->getResponse();
        } elseif ($e instanceof AuthenticationException) {
            return $this->unauthenticated($request, $e);    // ←AuthenticationExceptionが投げられた場合の処理
        } elseif ($e instanceof ValidationException) {
            return $this->convertValidationExceptionToResponse($e, $request);
        }

        ...
    }

そして、同じクラス内のunauthenticated()を呼び出しています。

    ...

    /**
     * Convert an authentication exception into a response.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Illuminate\Auth\AuthenticationException  $exception
     * @return \Symfony\Component\HttpFoundation\Response
     */
    protected function unauthenticated($request, AuthenticationException $exception)
    {
        return $this->shouldReturnJson($request, $exception)
                    ? response()->json(['message' => $exception->getMessage()], 401)
                    : redirect()->guest($exception->redirectTo() ?? route('login'));
    }

    ...

ajaxリクエストの場合はjsonレスポンスを返し、そうでない場合は、redirect()->guest(...)が実行されます。

Illuminate\Routing\Redirector

guest()はコメントにある通り、カレントのURLをセッションにセットしリダイレクトレスポンスを作成します。このカレントのURLというのがアクセスしようとしていたURLの事であり、setIntendedUrl()によってsessionのurl.intendedにセットされるのです。

    /**
     * Create a new redirect response, while putting the current URL in the session.
     *
     * @param  string  $path
     * @param  int  $status
     * @param  array  $headers
     * @param  bool|null  $secure
     * @return \Illuminate\Http\RedirectResponse
     */
    public function guest($path, $status = 302, $headers = [], $secure = null)
    {
        $request = $this->generator->getRequest();

        $intended = $request->method() === 'GET' && $request->route() && ! $request->expectsJson()    // ←カレントURLを取得
                        ? $this->generator->full()
                        : $this->generator->previous();

        if ($intended) {
            $this->setIntendedUrl($intended);
        }

        return $this->to($path, $status, $headers, $secure);
    }

    ...
    /**
     * Set the intended url.
     *
     * @param  string  $url
     * @return void
     */
    public function setIntendedUrl($url)
    {
        $this->session->put('url.intended', $url);    // sessionのurl.intendedにセット
    }

url.intendedは色々なクラスの色々なメソッドを介して最終的にセットされているため、少し複雑でしたが処理の流れを把握できてスッキリしました。お疲れさまでした。

By hikaru