Laravel で Ruby on Rails チュートリアル【12章】

## 環境
- Laravel 5.5
- PHP 7.2

# この章でやること
- [12.1 PasswordResetsリソース](#12-1-)
 - [12.1.1 PasswordResetsコントローラー](#12-1-1-)
 - [12.1.2 新しいパスワードの設定](#12-1-2-)
 - [12.1.3 createアクションでパスワード再設定](#12-1-3-)
- [12.2 パスワード再設定のメール送信](#12-2-)
 - [12.2.1 パスワード再設定のメールとテンプレート](#12-2-1-)
 - [12.2.2 送信メールのテスト](#12-2-2-)
- [12.3 パスワードを再設定する](#12-3-)
 - [12.3.1 editアクションで再設定](#12-3-1-)
 - [12.3.2 パスワードを更新する](#12-3-2-)
 - [12.3.3 パスワードの再設定をテストする](#12-3-3-)
- [12.4 本番環境でのメール送信(再掲)](#12-4-)
- [12.5 最後に](#12-5-)
- [12.6 照明期限切れの比較](#12-6-)

# 12.1 PasswordResetsリソース

# 12.1.1 PasswordResetsコントローラー

ルート追加(/routes/web.php)
```php
Route::get('password_resets/create', "PasswordResetsController@create")->name("resets.create");
Route::post('password_resets', "PasswordResetsController@store")->name("resets.store");
Route::get('password_resets/{token}/edit', "PasswordResetsController@edit")->name("resets.edit");
Route::patch('password_resets/{token}', "PasswordResetsController@update")->name("resets.update");
```
レイアウト(/resources/views/sessions/create.blade.php)
```ppp
{{ Html::linkRoute("resets.create", "(forgot password)") }}
```

# 12.1.2 新しいパスワードの設定

ミグレーション(/dataase/migrations/[timestamp]_add_reset_to_users.php)
```php
    public function up()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->string('reset_digest')->nullable();
            $table->dateTime('reset_sent_at')->nullable();
        });
    }

    public function down()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->dropColumn('reset_digest');
            $table->dropColumn('reset_sent_at');
        });
    }
```
(/resources/views/password_resets/create.blade.php)
```html
@extends('layouts.application')

@section('title', 'Forgot password')

@section('content')
<h1>Forgot password</h1>

<div class="row">
    <div class="col-md-6 col-md-offset-3">
        {{ Form::open(['route' => 'resets.store']) }}

        {{ Form::label('email') }}
        {{ Form::text('email', "", ["class" => "form-control"]) }}

        {{ Form::submit("Submit", ["class" => "btn btn-primary"]) }}
        {{ Form::close() }}
    </div>
</div>
@endsection
```
(/app/Http/Controllers/PasswordResetsController.php)
```php
    public function create()
    {
        return view("password_resets.create");
    }
```

# 12.1.3 createアクションでパスワード再設定

(/app/Http/Conttollers/PasswordResetsController.php)
```php
    public function store(Request $request)
    {
        $user = User::where("email", $request->email)->first();
        if (!$user) {
            session()->flash('message', ['danger' => 'Email address not found']);
            return redirect()->back();
        }
        $reset_token = str_random(22);
        $user->reset_digest = bcrypt($reset_token);
        $user->reset_sent_at = Carbon::now();
        $user->save();
        $user->sendPasswordResetMail($reset_token);
        session()->flash('message', ['info' => 'Email sent with password reset instructions']);
        return redirect("/");
    }
```
(/app/User.php)
```php
    public function sendPasswordResetMail($token)
    {
        Mail::to($this)->send(new PasswordReset($this, $token));
    }
```

# 12.2 パスワード再設定のメール送信

# 12.2.1 パスワード再設定のメールとテンプレート

(/app/Mail/PasswordReset.php)
```php
class PasswordReset extends Mailable
{
    use Queueable, SerializesModels;

    public $user;
    public $reset_token;

    public function __construct($user, $reset_token)
    {
        $this->user = $user;
        $this->reset_token = $reset_token;
    }

    public function build()
    {
        return $this->from("noreply@example.com")
                    ->subject("Password reset")
                    ->view('emails.password_reset');
    }
}
```
(/resources/views/emails/password_reset.blade.php)
```html
<h1>Password reset</h1>

<p>To reset your password click the link below:</p>

{{ Html::linkRoute("resets.edit", "Reset password", ["token" => $reset_token, "email" => $user->email]) }}

<p>This link will expire in two hours.</p>

<p>
If you did not request your password to be reset, please ignore this email and
your password will stay as it is.
</p>
```

# 12.2.2 送信メールのテスト

(/tests/Unit/MailerTest.php)
```php
    public function testPasswordReset()
    {
        Mail::fake();
        
        $user = User::find(1);
        $reset_token = str_random(22);
        Mail::to($user)->send(new PasswordReset($user, $reset_token));
        Mail::assertSent(PasswordReset::class, function ($mail) use ($user, $reset_token) {
            $mail->build();
            $this->assertEquals("Password reset", $mail->subject);
            $this->assertTrue($mail->hasTo($user->email));
            $this->assertTrue($mail->hasFrom("noreply@example.com"));
            $this->assertEquals($user->name, $mail->user->name);
            $this->assertEquals($reset_token, $mail->reset_token);
            $this->assertEquals($user->email, $mail->user->email);
            return true;
        });
    }
```

# 12.3 パスワードを再設定する

# 12.3.1 editアクションで再設定

(/resources/views/password_resets/edit.blade.php)
```html
@extends('layouts.application')

@section('title', 'Reset password')

@section('content')
<h1>Reset password</h1>

<div class="row">
    <div class="col-md-6 col-md-offset-3">
        {{ Form::model($user, ["route" => ["resets.update", $token], "method" => "patch"]) }}
        @include('shared.error_messages')
        {{ Form::hidden('email', $user->email) }}
        {{ Form::label('password') }}
        {{ Form::password('password', ["class" => "form-control"]) }}
        {{ Form::label('password_confirmation') }}
        {{ Form::password('password_confirmation', ["class" => "form-control"]) }}
        
        {{ Form::submit("Update password", ["class" => "btn btn-primary"]) }}
        {{ Form::close() }}
    </div>
</div>
@endsection
```
(/app/Http/Controllers/PasswordResetController.php)
```php
    public function __construct()
    {
        $this->middleware(function ($request, $next) {
            $user = User::where("email", $request->email)->first();
            if (!$user || !$user->activated || !Hash::check($request->token, $user->reset_digest)) {
                return redirect('/');
            }
            return $next($request);
        })->only(["edit", "update"]);
    }
...
    public function edit(Request $request)
    {
        $user = User::where("email", $request->email)->first();
        return view("password_resets.edit")->with(["user" => $user, "token" => $request->token]);
    }
```

# 12.3.2 パスワードを更新する

(/app/Http/Controllers/PasswordResetsController.php)
```php
    public function __construct()
    {
        $this->middleware(function ($request, $next) {
            $user = User::where("email", $request->email)->first();
            if ($user->checkExpiration()) {
                session()->flash('message', ['danger' => 'Password reset has expired']);
                return redirect()->back();
            }
            return $next($request);
        })->only(["edit", "update"]);
    }

    public function update(Request $request)
    {
        $request->validate([
            'password' => 'required|min:6|confirmed',
            'password_confirmation' => 'required|min:6',
        ]);

        $user = User::where("email", $request->email)->first();
        $user->password = bcrypt($request->password);
        $user->reset_digest = null;
        $user->save();
        Auth::login($user);
        session()->flash('message', ['success' => 'Password has been reset.']);
        return redirect()->route("users.show", $user->id);
    }
```
(/app/User.php)
```php
    public function checkExpiration()
    {
        return $this->reset_sent_at < Carbon::now()->subHours(2);
    }
```

# 12.3.3 パスワードの再設定をテストする

(/tests/Feature/PasswordResetsTest)
```php
class PasswordResetsTest extends TestCase
{
    private $user;

    protected function setUp()
    {
        parent::setUp();
        Artisan::call('migrate:fresh');
        $this->seed('TestSeeder');
        $this->user = User::find(1);
    }

    public function testPasswordResets()
    {
        Mail::fake();

        $response = $this->get(route("resets.create"));
        $response->assertViewIs("password_resets.create");
        $response = $this->post(route("resets.store"), ["email" => ""]);
        $response->assertSessionHas("message");
        $response->assertRedirect(route("resets.create"));
        $response = $this->post(route("resets.store"), ["email" => $this->user->email]);
        $this->assertNotEquals($this->user->reset_digest, User::find(1)->reset_digest);
        Mail::assertSent(PasswordReset::class, 1);
        $response->assertSessionHas("message");
        $response->assertRedirect("/");

        $reset_token = str_random(22);
        $this->user->update(["reset_digest" => bcrypt($reset_token)]);
        $response = $this->get(route("resets.edit", ["token" => $reset_token, "email" => ""]));
        $response->assertRedirect("/");
        $this->user->update(["activated" => false]);
        $response = $this->get(route("resets.edit", ["token" => $reset_token, "email" => $this->user->email]));
        $response->assertRedirect("/");
        $this->user->update(["activated" => true]);
        $response = $this->get(route("resets.edit", ["token" => "wrong token", "email" => $this->user->email]));
        $response->assertRedirect("/");
        $response = $this->followingRedirects()
                        ->get(route("resets.edit", ["token" => $reset_token, "email" => $this->user->email]));
        $response->assertViewIs("password_resets.edit");
        $dom = $this->dom($response->content());
        $this->assertSame(1, $dom->filter("input[name=email][type=hidden][value=\"{$this->user->email}\"]")->count());
        $response = $this->followingRedirects()
                        ->patch(route("resets.update", $reset_token), [
                            "email" => $this->user->email,
                            "password" => "foobaz",
                            "password_confirmation" => "barquux"
                        ]);
        $this->assertSame(1, $this->dom($response->content())->filter("div#error_explanation")->count());
        $response = $this->followingRedirects()
                        ->patch(route("resets.update", $reset_token), [
                            "email" => $this->user->email,
                            "password" => "",
                            "password_confirmation" => ""
                        ]);
        $this->assertSame(1, $this->dom($response->content())->filter("div#error_explanation")->count());
        $response = $this->followingRedirects()
                        ->patch(route("resets.update", $reset_token), [
                            "email" => $this->user->email,
                            "password" => "foobaz",
                            "password_confirmation" => "foobaz"
                        ]);
        $this->assertTrue(Auth::check());
        $response->assertSeeText("Password has been reset.");
        $response->assertViewIs("users.show");
    }
}
```

# 12.4 本番環境でのメール送信(再掲)

# 12.5 最後に

# 12.6 照明期限切れの比較