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

## 環境
- Laravel 8.6
- PHP 8.0
- Bootstrap 5.1

## 11.1 AccountActivationsリソース

### 11.1.1 AccountActivationsコントローラ

```php:/routes/web.php
Route::get('account_activations/{token}/edit', [AccountActivationsController::class, 'edit'])->name('activation');
```

### 11.1.2 AccountActivationのデータモデル

```php:/database/migrations/[timestamp]_add_activation_to_users.php
class AddActivationToUsers extends Migration
{
    public function up()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->string('activation_digest')->nullable();
            $table->boolean('activated')->default(false);
            $table->dateTime('activated_at')->nullable();
        });
    }

    public function down()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->dropColumn('activation_digest');
            $table->dropColumn('activated');
            $table->dropColumn('activated_at');
        });
    }
}
```
```php:/app/Http/Controllers/UsersController.php
    public function store(Request $request)
    {
        $request->email = Str::lower($request->email);
        $request->validate([
            "name"                  => "required|max:50",
            "email"                 => "required|max:255|email|unique:users",
            "password"              => "required|confirmed|min:6",
            "password_confirmation" => "required|min:6",
        ]);
        $user = new User;
        $user->name = $request->name;
        $user->email = $request->email;
        $user->password = bcrypt($request->password);
        $activation_token = Str::random(22);
        $user->activation_digest = bcrypt($activation_token);
        $user->save();
        Auth::login($user);
        session()->flash("message", ['success' => 'Welcome to the Sample App!']);
        return redirect()->route("users.show", $user);
    }
```
```php:/database/factories/UserFactory.php
$factory->define(User::class, function (Faker $faker) {
    return [
            'name'         => $this->faker->name(),
            'email'        => "example-{$this->faker->unique()->randomNumber}@railstutorial.org",
            'password'     => bcrypt("password"),
            'activated'    => true,
            'activated_at' => Carbon::now(),
    ];
});
```
```php:/database/seeds/DatabaseSeeder.php
        DB::table('users')->insert([
            "name"         => "Example User",
            "email"        => "example@railstutorial.org",
            "password"     => bcrypt("foobar"),
            "admin"        => true,
            "activated"    => true,
            "activated_at" => Carbon::now(),
        ]);
```
```php:/database/seeds/test/TestSeeder.php
        User::create(["name" => "Michael Example", "email" => "michael@example.com", "password" => bcrypt("password"), "admin" => true, "activated" => true, "activated_at" => Carbon::now()]);
        User::create(["name" => "Sterling Archer", "email" => "duchess@example.gov", "password" => bcrypt("password"), "activated" => true, "activated_at" => Carbon::now()]);
        User::create(["name" => "Lana Kane", "email" => "hands@example.gov", "password" => bcrypt("password"), "activated" => true, "activated_at" => Carbon::now()]);
        User::create(["name" => "Malory Archer", "email" => "boss@example.gov", "password" => bcrypt("password"), "activated" => true, "activated_at" => Carbon::now()]);
```

## 11.2 アカウント有効化のメール送信

### 11.2.1 送信メールのテンプレート

```bash
sail artisan make:mail AccountActivation
sail artisan make:mail PasswordReset
```
```html:/resources/views/emails/account_activation.blade.php
<h1>Sample App</h1>

<p>Hi {{ $user->name }},</p>

<p>
Welcome to the Sample App! Click on the link below to activate your account:
</p>

{{ link_to_route("activation", "Activate", ["token" => $activation_token, "email" => $user->email]) }}
```
```php:/app/Mail/AccountActivation.php
class AccountActivation extends Mailable
{
    use Queueable, SerializesModels;

    public $user;
    public $activation_token;

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

    public function build()
    {
        return $this->from("noreply@example.com")
                    ->subject("Account activation")
                    ->view('emails.account_activation');
    }
}
```
```php:/app/Http/Controllers/UsersController.php
        Mail::to($user)->send(new AccountActivation($user, $activation_token));
```

### 11.2.2 送信メールのプレビュー

mailHogを使用する
https://readouble.com/laravel/8.x/ja/sail.html

### 11.2.3 送信メールのテスト

```php:/tests/Unit/MailerTest.php
    protected function setUp(): void
    {
        parent::setUp();
        Artisan::call('migrate:fresh');
        $this->seed('TestSeeder');
    }

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

        $user = User::find(1);
        $activation_token = Str::random(22);
        Mail::to($user)->send(new AccountActivation($user, $activation_token));
        Mail::assertSent(AccountActivation::class, function ($mail) use ($user, $activation_token) {
            $mail->build();
            $this->assertEquals("Account activation", $mail->subject);
            $this->assertTrue($mail->hasTo($user->email));
            $this->assertTrue($mail->hasFrom("noreply@example.com"));
            $this->assertEquals($user->name, $mail->user->name);
            $this->assertEquals($activation_token, $mail->activation_token);
            $this->assertEquals($user->email, $mail->user->email);
            return true;
        });
    }
```

### 11.2.4 ユーザーのcreateアクションを更新

```php:/app/Http/Controllers/UsersController.php
        $user->save();
        Mail::to($user)->send(new AccountActivation($user, $activation_token));
        session()->flash('message', ['info' => 'Please check your email to activate your account.']);
        return redirect("/");
```
```php:/tests/Feature/UsersSignupTest.php
        // $response->assertViewIs("users.show");
        // $this->assertTrue(Auth::check());
```

## 11.3 アカウントを有効化する

### 11.3.1 authenticated?メソッドの抽象化

### 11.3.2 editアクションで有効化

```php:/app/Http/Controllers/AccountActivationsController.php
class AccountActivationsController extends Controller
{
    public function edit(Request $request, $token)
    {
        $user = User::where('email', $request->email)->first();
        if ($user && !$user->activated && Hash::check($token, $user->activation_digest)) {
            $user->activated = true;
            $user->activated_at = Carbon::now();
            $user->save();
            Auth::login($user);
            session()->flash('message', ['success' => 'Account activated!']);
            return redirect()->route("users.show", $user->id);
        } else {
            session()->flash('message', ['danger' => 'Invalid activation link']);
            return redirect("/");
        }
    }
}
```
```php:/app/Http/Controllers/SessionController.php
    public function store(Request $request)
    {
        $user = User::where('email', $request->email)->first();
        if ($user && Hash::check($request->password, $user->password)) {
            if ($user->activated === true) {
                Auth::login($user, $request->remember_me === "1");
                return redirect()->intended("/users/" . Auth::id());
            } else {
                session()->flash('message', ['warning' => 'Account not activated. Check your email for the activation link.']);
                return redirect("/");
            }
        } else {
            session()->flash('message', ['danger' => 'Invalid email/password combination']);
            return back()->withInput();
        }
    }
```

### 11.3.3 有効化のテストとリファクタリング

```php:/tests/Feature/UsersSignupTest.php
class UsersSignupTest extends TestCase
{
    use RefreshDatabase;

    public function testInvalidSignup()
    {
        $this->get("signup");
        $count = User::all()->count();
        $response = $this->followingRedirects()
                        ->post(route("signup"), [
                            "name" => "",
                            "email" => "user@invalid",
                            "password" => "foo",
                            "password_confirmation" => "bar"
                        ]);
        $this->assertSame($count, User::all()->count());
        $response->assertViewIs("users.create");
        $dom = $this->dom($response->content());
        $this->assertSame(1, $dom->filter('div#error_explanation')->count());
        $this->assertSame(1, $dom->filter('div.alert-danger')->count());
    }

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

        $this->get(route('signup'));
        $count = User::all()->count();
        $response = $this->followingRedirects()
            ->post(route('signup'), [
                'name'                  => "Example User",
                'email'                 => "user@example.com",
                'password'              => "password",
                'password_confirmation' => "password",
            ]);
        $this->assertSame($count + 1, User::all()->count());
        Mail::assertSent(AccountActivation::class, 1);
        $user = User::where("email", "user@example.com")->first();
        $activation_token = Str::random(22);
        $user->update(["activation_digest" => bcrypt($activation_token)]);
        $this->assertFalse($user->activated);

        $this->post("login", ["email" => $user->email, "password" => "password"]);
        $this->assertFalse(Auth::check());
        $this->get(route("activation", ["token" => "incalid token", "email" => $user->email]));
        $this->assertFalse(Auth::check());
        $this->get(route("activation", ["token" => $activation_token, "email" => "wrong"]));
        $this->assertFalse(Auth::check());
        $response = $this->get(route("activation", ["token" => $activation_token, "email" => $user->email]));
        $this->assertTrue(User::find($user->id)->activated);
        $response->assertRedirect(route("users.show", $user->id));
        $this->assertTrue(Auth::check());
    }
}
```
```php:/app/Http/Models/User.php
    protected $casts = [
        'email_verified_at' => 'datetime',
        'admin'             => 'boolean',
        'activated'         => 'boolean',
    ];

    public function activate()
    {
        $this->activated = true;
        $this->activated_at = Carbon::now();
        $this->save();
    }

    public function sendActivateEmail($token)
    {
        Mail::to($this)->send(new AccountActivation($this, $token));
    }
```

#### 課題

```php:/app/Http/Controllers/UsersController.php
    public function index()
    {
        $users = User::where("activated", true)->paginate(30);
        return view('users.index')->with('users', $users);
    }
...
    public function show(User $user)
    {
        if ($user->activated) {
            return view('users.show')->with('user', $user);
        } else {
            return redirect("/");
        }
    }
```

## 11.4 本番環境でのメール送信

sendgridを使用

```
heroku addons:create sendgrid:starter
```

```
heroku config:set MAIL_HOST=smtp.sendgrid.net
heroku config:set MAIL_USERNAME={heroku config:get SENDGRID_USERNAME}
heroku config:set MAIL_PASSWORD={heroku config:get SENDGRID_PASSWORD}
```

# 11.5 最後に

https://windii.jp/backend/laravel/api-register
https://www.ritolab.com/entry/38
https://sendgrid.com/docs/for-developers/sending-email/laravel/