Laravel/Socialiteが何をしているか調べてみた　～①リダイレクト編～

# 目標
Laravelで「Googleでログイン」を実装する際に導入する"Socialite"がどんなことをやっているのか調査してみた。
①リダイレクト編の目標は、Controllerに記述することになる下記コードが最終的なアウトプットは何か、どのように実装されているのかを明確にする。

以下Controllerに記述することになるコード

```php5:SocialOAuthController.php

/**
 * Googleアカウントの認証ページへユーザーをリダイレクト
 * ①リダイレクト編で解説するコード
 */
public function redirectToGoogle()
{
    return Socialite::driver('google')->redirect();
}

/**
 * Googleアカウントからユーザー情報を取得
 * ②コールバック編で解説予定
 */
public function handleGoogleCallback()
{
    $user = Socialite::driver('google')->stateless()->user();
}

```

# 先に結論（最終的なアウトプット）
認可URLにリダイレクトするRedirectResponseインスタンスを生成します。
リダイレクト先のURLは下記の通りです。

```php
RequestURL = https://accounts.google.com/o/oauth2/auth?
client_id=$client_id
&redirect_url=$redirect_url
&scope=$scope1+$scope2+$scope3
&response_type=code
&state=$state
```

# ＜環境＞

```
Laravel Framework 8.17.2
laravel/socialite v5.1.2
```


# ＜解説＞

## Socialite

SocialiteのファサードはSocialiteServiceProviderで下記のように定義されています。

```php5:SocialiteServiceProvider.php
namespace Laravel\Socialite;

class SocialiteServiceProvider extends ServiceProvider
{
    /**
     * Register the service provider.
     *
     * @return void
     */
    public function register()
    {
        $this->app->singleton(Factory::class, function ($app) {
            return new SocialiteManager($app);
        });
    }
}

```

## Socialite::driver('google')

driverメソッドに文字列でドライバを指定しています。
Socialiteクラスの基底クラスであるManagerクラスにdriverメソッドで引数に渡した文字列に該当するドライバが可変関数として呼び出されます。

```php5:Manager

namespace Illuminate\Support;

abstract class Manager
{
    /**
    * Get a driver instance.
    *
    * @param  string|null  $driver
    * @return mixed
    *
    * @throws \InvalidArgumentException
    */
    public function driver($driver = null)
    {
        $driver = $driver ?: $this->getDefaultDriver();

        if (is_null($driver)) {
            throw new InvalidArgumentException(sprintf(
                'Unable to resolve NULL driver for [%s].', static::class
            ));
        }

        // ドライバのインスタンスはキャッシュして再利用される。
        if (! isset($this->drivers[$driver])) {
            $this->drivers[$driver] = $this->createDriver($driver);
        }

        return $this->drivers[$driver];
    }

    /**
    * Create a new driver instance.
    *
    * @param  string  $driver
    * @return mixed
    *
    * @throws \InvalidArgumentException
    */
    protected function createDriver($driver)
    {
        if (isset($this->customCreators[$driver])) {
            return $this->callCustomCreator($driver);
        } else {
            $method = 'create'.Str::studly($driver).'Driver';

            if (method_exists($this, $method)) {
                // ここでcreateGoogleDriver()が呼び出される
                return $this->$method();
            }
        }

        throw new InvalidArgumentException("Driver [$driver] not supported.");
    }
}

```

### createGoogleDriver()

createGoogleDriverメソッドではconfigファイル（services.google）に設定した情報を取得し、GoogleProviderクラスのインスタンスを生成しています。

```php5:SocialiteManager.php

namespace Laravel\Socialite;

class SocialiteManager extends Manager implements Contracts\Factory
{
    /**
    * Create an instance of the specified driver.
    *
    * @return \Laravel\Socialite\Two\AbstractProvider
    */
    protected function createGoogleDriver()
    {
        $config = $this->config->get('services.google');

        return $this->buildProvider(
        GoogleProvider::class, $config
        );
    }
}

```


AbstractProviderを継承するGoogleProviderインスタンスを生成するまでがSocialite::driver('google')の動きです。

## Socialite::driver('google')->redirect()

redirectメソッドでは生成されたリダイレクトURLにリダイレクトするようにRedirectResponseクラスのインスタンスを生成している。

```php5:AbstractProvider.php

namespace Laravel\Socialite\Two;

abstract class AbstractProvider implements ProviderContract
{
    /**
     * Redirect the user of the application to the provider's authentication screen.
     *
     * @return \Illuminate\Http\RedirectResponse
     */
    public function redirect()
    {
        $state = null;

        if ($this->usesState()) {
            $this->request->session()->put('state', $state = $this->getState());
        }

        // ここでRedirectResponseクラスのインスタンスを生成
        return new RedirectResponse($this->getAuthUrl($state));
    }



}

```

### getAuthUrl()
getAuthUrlメソッドはGoogleProviderで定義されている

```php5:GoogleProvider.php
namespace Laravel\Socialite\Two;

use Illuminate\Support\Arr;

class GoogleProvider extends AbstractProvider implements ProviderInterface
{
     /**
     * {@inheritdoc}
     */
    protected function getAuthUrl($state)
    {
        return $this->buildAuthUrlFromBase('https://accounts.google.com/o/oauth2/auth', $state);
    }
}

```

AbstractProviderに戻ってURLがどのように生成されるか見ていく。
GoogleのベースURLにクエリパラメータとして認可に必要な各項目をセットしている。


```php5:AbstractProvider.php

    /**
     * Build the authentication URL for the provider from the given base URL.
     *
     * @param  string  $url
     * @param  string  $state
     * @return string
     */
    protected function buildAuthUrlFromBase($url, $state)
    {
        // $this->encodingTypeはPHP_QUERY_RFC1738が定義されている
        return $url.'?'.http_build_query($this->getCodeFields($state), '', '&', $this->encodingType);
    }

    /**
     * Get the GET parameters for the code request.
     *
     * @param  string|null  $state
     * @return array
     */
    protected function getCodeFields($state = null)
    {
        $fields = [
            'client_id' => $this->clientId,
            'redirect_uri' => $this->redirectUrl,
            'scope' => $this->formatScopes($this->getScopes(), $this->scopeSeparator),
            'response_type' => 'code',
        ];

        if ($this->usesState()) {
            $fields['state'] = $state;
        }

        return array_merge($fields, $this->parameters);
    }

    /**
     * Format the given scopes.
     *
     * @param  array  $scopes
     * @param  string  $scopeSeparator
     * @return string
     */
    protected function formatScopes(array $scopes, $scopeSeparator)
    {
        return implode($scopeSeparator, $scopes);
    }
}


```


# ＜おまけ＞認可リクエストのカスタマイズ

Laravel公式にもあるようにパラメータや、スコープの追加をすることができる。

## パラメータの追加

パラメータを追加するにはwithメソッドを使用します。
$parametersの規定値は空の配列[]です。

```php5:AbstractProvider.php

    /**
     * Set the custom parameters of the request.
     *
     * @param  array  $parameters
     * @return $this
     */
    public function with(array $parameters)
    {
        $this->parameters = $parameters;
        return $this;
    }

```


## scopeの追加

scopeに規定値は['openid','profile','email']が定義されている。
追加するには下記のようにscopesメソッドを呼び出すか、setScopeメソッドで上書きするとよい。

```php5:SocialOAuthController.php

/**
 * Googleアカウントの認証ページへユーザーをリダイレクト
 */
public function redirectToGoogle()
{
    return Socialite::driver('google')->scopes(['read:user', 'public_repo'])->redirect();
}

```

メソッドの中身は下記のように配列がマージされる。
array_uniqueメソッドでは、重複した要素が存在する場合、はじめの要素のキーと値が保持されます。
https://www.php.net/manual/ja/function.array-unique.php

```php5:AbstractProvider.php

    /**
     * Merge the scopes of the requested access.
     *
     * @param  array|string  $scopes
     * @return $this
     */
    public function scopes($scopes)
    {
        // 
        $this->scopes = array_unique(array_merge($this->scopes, (array) $scopes));
        return $this;
    }

    /**
     * Set the scopes of the requested access.
     *
     * @param  array|string  $scopes
     * @return $this
     */
    public function setScopes($scopes)
    {
        $this->scopes = array_unique((array) $scopes);
        return $this;
    }

```

## リダイレクトURLの変更

リダイレクトURLが複数存在し、configファイルではなく動的に値を変更したい場合はredirectURLメソッドを使用します。

```php5:SocialOAuthController.php

/**
 * Googleアカウントの認証ページへユーザーをリダイレクト
 */
public function redirectToGoogle()
{
    return Socialite::driver('google')->redirectURL($redirectUrl)->redirect();
}

```

```php5:AbstractProvider.php

    /**
     * Set the redirect URL.
     *
     * @param  string  $url
     * @return $this
     */
    public function redirectUrl($url)
    {
        $this->redirectUrl = $url;

        return $this;
    }

```