[Laravel]仮会員登録時のメール認証をusersテーブルとは別で実装する

Laravel5.5を使用してメール認証を使った会員登録を実装した時に、[こちらの記事](https://qiita.com/nitaking/items/588a645968efcac6d37a)を参考にましたが、メール認証と仮会員登録を別々のtableで管理したかったのでその時の備忘録もかねて紹介したいと思います。

##前提
* 参考記事で一通りのユーザー登録画面が作成してある

##流れ
1.仮会員のメールアドレスを登録
2.登録したアドレスにメールを送る
3.メールにあるURLをクリック
4.本会員登録用の入力フォームを登録する
5.本会員登録完了

###メール認証用のtableを作成する
今回は`email_verifications`という名前で、メール認証用のテーブルを作成します。

```sh
php artisan make:migration create_email_verification_table
```
作成したmigrationファイルにメール認証に必要なカラムを記載していきます。

- email:メールアドレス
- token:メールアドレス確認用のトークン
- status:仮会員登録のステータス
- expiration_datetime:有効期限

を追加したいと思います。

```php:create_email_verification_table.php
class CreateEmailVerification extends Migration
{
    public function up()
    {
        Schema::create('email_verifications', function (Blueprint $table) {
            $table->increments('id');
            $table->string('email', 255)->comment('メールアドレス');
            $table->string('token', 250)->comment('確認トークン');
            $table->tinyInteger('status')->comment('ステータス');
            $table->dateTime('expiration_datetime')->comment('有効期限');
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('email_verifications');
    }
}
```

###仮会員のメールアドレス登録画面を作る
仮会員登録時は「メールアドレス」だけ登録してもらうようにします。
register.blade.phpを編集して「メールアドレス」以外の項目を削除します。


###仮会員登録処理を実装する
routeを`web.php`に追加します。

```php:web.php
Route::post('/register', 'Auth\RegisterController@emailVerification');
```
> routeの設定に間違いがあったので訂正しましたmm

####Mode\EmailVerificationを作成する
`email_verifications`に登録するModelを作成します。

```sh
php artisan make:model EmailVerification
```
```php:EmailVerification.php
class EmailVerification extends Model
{
    const SEND_MAIL = 0;   // 仮会員登録のメール送信
    const MAIL_VERIFY = 1; //メールアドレス認証
    const REGISTER = 2;    // 本会員登録完了

    protected $fillable = [
        'email',
        'token',
        'status',
        'expiration_datetime',
    ];

    public function __construct(array $attributes = [])
    {
        parent::__construct($attributes);
    }

    public static function build($email, $hours = 1)
    {
        $emailVerification = new self([
            'email' => $email,
            'token' => str_random(250),
            'status' => self::SEND_MAIL,
            'expiration_datetime' => Carbon::now()->addHours($hours),
        ]);
        return $emailVerification;
    }
}
```
`status`は仮会員ということで「0」を登録しておきます。
`expiration_datetime`は仮会員の有効期限なので1時間くらいに設定しておきます。

####RegisterControllerの更新
**validator()の更新**

メールアドレスのチェックだけなので`email_validator()`を作成しました。

```php:Auth/RegisterController.php
    protected function email_validator(array $data)
    {
        return Validator::make($data, [
            'email' => 'required|string|email||unique:users,email',
        ]);
    }
```
今回は、`Users`テーブルとは別テーブルに仮会員登録をしたいのですが、メールアドレスは重複させたくなかったので、`email_validator()`には`unique:user,email`を追加しました。
**これで本登録した時に、`Users`テーブルで重複したメールアドレスが無くせるかと思います。**

**emailVerification()を追記**
仮会員用のメールアドレスを、作成した新しいテーブル`email_verifications`に登録する処理を追記します。

```php:Auth/RegisterController.php
    public function emailVerification(Request $request)
    {
        $validator = $this->email_validator($request->all());
        if ($validator->fails()) {
            return view('auth.register')
                ->with([
                    'email' => $request->email,
                ])
                ->withErrors($validator);
        } else {
            $emailVerification = EmailVerification::build($request->email);
            DB::beginTransaction();
            try {
                $emailVerification->saveOrFail();
                DB::commit();
            } catch (\Throwable $e) {
                DB::rollBack();
                Log::warning("メールアドレス変更処理に失敗しました。 {$e->getMessage()}", $e->getTrace());
                return view('auth.register')
                    ->with([
                        'email' => $request->email
                    ])->withErrors(['error' => 'メールアドレスの登録に失敗しました。']);
            }
            Mail::to($request->email)->send(new \App\Mail\EmailVerification($emailVerification));

            return view('auth.email_verify');
        }
    }
```
>メールを送る処理は参考にした記事のように実装します。

これで、`email_verifications`に仮会員登録を行い、登録したメールアドレスに認証用のメールが送られてきます。

###本会員登録処理を実装する
仮会員登録をした時に送られてきたメール認証用のURLをクリックして認証を行い本登録用にパスワードの設定を行います。

####メールアドレスの認証を行う
認証用のrouteを設定する

```php:web.php
Route::get('/verify/{token}', [
    'as' => 'verify',
    'uses' => 'Auth\RegisterController@emailVerifyComplete',
])->where('token','[A-Za-z0-9]+');
```

RegisterController.phpにメールアドレス認証完了用の処理を追加する

```php:Auth/RegisterController.php
    public function emailVerifyComplete($token)
    {
        // 有効なtokenか確認する
        $emailVerification = EmailVerification::findByToken($token);
        if (empty($emailVerification) || $emailVerification->isRegister()) {
            return view('errors.email_verify');
        }

        // ステータスをメール認証済みに変更する
        $emailVerification->mailVerify();

        DB::beginTransaction();
        try {
            // DB更新
            $emailVerification->update();
            DB::commit();
        } catch (\Throwable $e) {
            DB::rollBack();
            Log::warning("メールアドレスの認証に失敗しました: email: {$emailVerification->email}", $e->getTrace());
            return redirect(route('/'))
                ->with(['message' => 'メールアドレスの認証に失敗しました。管理者にお問い合わせください。']);
        }
        return view('auth.pre_register')
            ->with(['token' => $emailVerification->token]);
    }
```

EmailVerification.phpにも処理を追加しました。

```php:EmailVerification.php
    public static function findByToken($token)
    {
        return self::where('token', '=', $token)->first();
    }

    public function mailVerify()
    {
        $this->status = self::MAIL_VERIFY;
    }

    public function isRegister()
    {
        return $this->status === self::REGISTER;
    }

    public function register()
    {
        $this->status = self::REGISTER;
    }
```

これで仮会員登録まで完了しました。

####本会員登録用のviewを作る
仮会員登録完了した後に`return view('auth.pre_register')`としたので、viewファイルを作成します。


```php:auth/pre_register.blade.php
    <div class="main_inner form">
        <h1 class="alignCenter">OK</h1>
        <p class="alignCenter">
            メールアドレスの認証が完了しました。
            <br>
            名前とパスワードを設定してください。
        </p>
        {!! Form::open(['route' => ['registered', $token], 'method' => 'post']) !!}

        {{ csrf_field() }}

        @if(session('message') OR isset($message))
            <div class="success m-b2">
                {!! session('message') ? session('message') : $message !!}
            </div>
        @endif

        @if(session('error'))
            <div class="end m-b2">
                {!! session('error') !!}
            </div>
        @endif

        @if (count($errors) > 0)
            <ul class="end">
                @foreach ($errors->all() as $error)
                    <li>{{ $error }}</li>
                @endforeach
            </ul>
        @endif

        <p class="bold m-t3">名前</p>
        <p>{{ Form::text('name', null, ['placeholder' => '名前', 'required']) }}</p>

        <p class="bold m-t3">パスワード</p>
        <p>{{ Form::password('password', ['placeholder' => 'パスワード', 'required']) }}</p>

        <p class="bold m-t3">確認用パスワード</p>
        <p>{{ Form::password('password_confirmation', ['placeholder' => '確認用パスワード', 'required']) }}</p>

        <div class="m-t3">{{ Form::submit('登録する', ['class' => 'btn']) }}</div>

        {!! Form::close() !!}
    </div>
```
>auth/register.blade.phpのformの部分だけ書き換えて作成しました。

これで本会員登録用の入力フォームが完成しました。

####登録処理を実装する
次に、登録ボタンを押した時の遷移先を`web.php`に追加します。
入力フォームのpost先は
`['route' => ['registered', $token], 'method' => 'post']`
にしたのでこれに対応するrouteを追記します。

```php:web.php
Route::post('/verify/{token}',[
    'as' => 'registered',
    'uses' => 'Auth\RegisterController@create',
])->where('token','[A-Za-z0-9]+');
```
routeに追加したので次は、Controllerに本会員登録の処理を追加していきます。

```php:Auth/RegisterController.php
    protected function create_validator(array $data)
    {
        return Validator::make($data, [
            'name' => 'required|string|max:255',
            'password' => 'required|string|min:6|confirmed',
        ]);
    }

    protected function create(Request $request, $token)
    {
        $validator = $this->create_validator($request->all());
        if ($validator->fails()) {
            return view('auth.pre_register')
                ->with([
                    'token' => $request->token,
                    'name' => $request->name,
                ])
                ->withErrors($validator);
        } else {
            $emailVerification = EmailVerification::findByToken($token);
            $user = User::create([
                'name' => $request->name,
                'email' => $emailVerification->email,
                'password' => Hash::make($request->password),
            ]);
            $emailVerification->register();
            $emailVerification->update();
            Auth::login($user);

            return redirect()->route('home');
        }
    }
```
> 本会員登録時のvalidationでエラーが発生した場合は、パスワード以外のパラメータを返しておきます。

本会員用のvalidation処理と登録処理を追加しました。
カラムも`make:auth`で実行した時に作成される分しか使用しなかったので特に追加する必要はありません。

###実装完了です
今回、本会員登録後のviewは作成しなかったので作成されている`home`を表示するようにしました。

Laravelを触ったのが今回初めてだったので、分からないことがたくさんあり調べつつ実装していきました。
せっかく新しいFWを書いたので備忘録も兼ねて初めてまとめてみました。
もっと実務でも書けるように勉強していきたいと思います！

ありがとうございました。

###参考
今回の実装をするに当たって以下のqiita記事を参考にして作成しました。
[[Laravel]メール認証を使った会員登録](https://qiita.com/nitaking/items/588a645968efcac6d37a)