dip Engineer Blog

Engineer Blog
ディップ株式会社のエンジニアによる技術ブログです。
弊社はバイトル・はたらこねっとなど様々なサービスを運営しています。

LaravelのMailableを使ってメール送信とテストコードを書いてみる

はじめに

こんにちは、PHPで求人系サービスの開発や社内向けツールの開発を行なっている @taku-0728 です。
今回はLaravelでメール送信が行えるMailableクラスを使ってメール送信機能の実装と、そのコードに対するテストコードの作成をやっていきます。
Mailableクラスを使ったメール送信機能の実装のやり方と、Mailableクラスに対するテストコードの書き方のそれぞれに関する記事はいくつか拝見しましたが、 その両方を1つの記事で解説している記事はあまりなかったので、実装もやりたいしテストコードも書きたい方の参考になればと思います。

開発環境

  • OS: macOS Catalina 10.15.6
  • Laravel: 6.18.25
  • PHP: 7.4.8

今回作るもの

(適当ですいません...)
今回は画像のような入力フォームにて名前、宛先、フリーメッセージを入力し、確認画面で確認後、メールを送信する。
というありがちな流れにそって作っていきます。

作り方

入力フォームの作成まで

まず下記コマンドでコントローラーを作成します。

php artisan make:controller Mail

作成したコントローラーを編集し、以下のようにします。

  /**
   * メール入力用フォームに遷移する
   */
  public function index(): object
  {
    return view('emails.index');
  }

viewファイルを用意します。

<!DOCTYPE html>
<html lang="ja">
  <head>
    <title>メール送信フォーム</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
  </head>
  <body>
    <div class="container">
      <h2 class="">メール送信フォーム</h2>
      <form id="form" method="post" action="/confirm">
        @csrf
        <div class="form-group">
          <label for="name">名前(必須)</label>
          <input type="text" class="form-control" id="name" name="name" placeholder="名前"  value="{{old('name')}}">
          @if($errors->has('name'))
            <p>{{ $errors->first('name') }}</p>
          @endif
        </div>
        <div class="form-group">
          <label for="email">宛先(必須)</label>
          <input type="text" class="form-control" id="email" name="email" placeholder="宛先"  value="{{old('email')}}">
          @if($errors->has('email'))
            <p>{{ $errors->first('email') }}</p>
          @endif
        </div>
        <div class="form-group">
          <label for="body">本文</label>
          <textarea class="form-control" id="body" name="body" placeholder="本文(100文字まで)をお書きください"  rows="3" >{{old('body')}}</textarea>
          @if($errors->has('body'))
            <p>{{ $errors->first('body') }}</p>
          @endif
        </div>
        <button name="submitted" type="submit" class="btn btn-primary">確認</button>
      </form>
    </div>
  </body>
</html>

アクセスすると、画像のような入力フォームが表示されるはずです。

確認画面の表示まで

先ほど作成したコントローラーに以下の処理を追記します。

  /**
   * メール確認
   *
   * @param  Request $request 入力値
   */
  public function confirm(Request $request)
  {
    $validator = Validator::make($request->all(), [
      'name' => 'required',
      'email' => 'required|email',
      'body' => 'max:100'
    ]);

    if ($validator->fails()) {
      return redirect('/index')->withErrors($validator)->withInput();
    }

    $result = [];

    if ($request->has(['name', 'email', 'body'])) {
      $result['name'] = $request->name;
      $result['email'] = $request->email;
      $result['body'] = $request->body;
    } else {
      return redirect('/index', $result);
    }

    return view('emails.confirm', $result);

  }

詳細は割愛しますが、入力フォームの入力値に対してバリデーション処理を行い、問題なければ確認画面に遷移させます。 確認画面用のviewファイルを追加します。

<!DOCTYPE html>
<html lang="ja">
  <head>
    <title>メール送信フォーム</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
  </head>
  <body>
    <div class="container">
      <h2 class="">メール送信フォーム</h2>
      <form id="form" method="post" action="/execute">
        @csrf
        <div class="form-group">
          <input type="hidden" class="form-control" id="name" name="name" value="{{ $name }}">
          <p>名前:<br>{{ $name }}</p>
        </div>
        <div class="form-group">
          <input type="hidden" class="form-control" id="email" name="email" value="{{ $email }}">
          <p>宛先:<br>{{ $email }}</p>
        </div>
        <div class="form-group">
          <input type="hidden" class="form-control" id="body" name="body" value="{{ $body }}">
          <p>本文:<br>{{ $body }}</p>
        </div>
        <button name="submitted" type="submit" class="btn btn-primary">送信</button>
      </form>
    </div>
  </body>
</html>

こんな感じで確認画面に遷移できていれば問題ありません。

メール送信処理の実装まで

いよいよメール送信部分の実装を行っていきます。
まず下記コマンドでメール送信用のMailableクラスを作成します。

php artisan make:mail MailSend

app/Mail 配下にMailSend.phpという下記のファイルが作成されているはずです。

<?php

namespace App\Mail;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;

class MailSend extends Mailable
{
use Queueable, SerializesModels;

  /**
   * Create a new message instance.
   *
   * @return void
   */
  public function __construct()
  {
    //
  }

  /**
   * Build the message.
   *
   * @return $this
   */
  public function build()
  {
    return $this->view('view.name');
  }
}

次にコントローラーに作成したSendEmailクラスを呼ぶ処理を加えます。
下記のメソッドを追加してください。

  /**
   * メール送信
   *
   * @param  Request $request 入力値
   */
  public function execute(Request $request)
  {

    if ($request->has(['name', 'email', 'body'])) {
      $name = $request->name;
      $email = $request->email;
      $body = $request->body;
    }

    Mail::to($email)->send(new MailSend($name, $body));

    return view('emails.result');
  }

確認画面から受け取ったパラメータを確認し、問題なければMailファサードのtoメソッドに送信先のアドレスを、sendメソッドの引数にMailSendクラスのインスタンスを渡しています。 コントローラーの処理の追記が完了したので、改めてMailSendクラスにも追記します。

  use Queueable, SerializesModels;

  protected string $name;
  protected string $body;

  /**
  * Create a new message instance.
  *
  * @return void
  */
  public function __construct(string $name, string $body)
  {
    $this->name = $name;
    $this->body = $body;
  }

  /**
  * Build the message.
  *
  * @return $this
  */
  public function build()
  {
    return $this->view('emails.body')
                ->from('sample@example.com')
                ->subject('テストメールです')
                ->with([
                  'name' => $this->name,
                  'body' => $this->body
                ]);
  }

コンストラクタで名前と本文を受け取り、buildメソッド内で実際に送信するメール本文を作成しています。 resources/views/emails配下にbody.blade.phpを用意します。
中身はこちら

{{ $name }}さん

このメッセージが読めているということは、メール送信が正常に行われたということです。
あなたが入力した本文は以下に記載されているはずです。確認してみてください。

{{ $body }}

MailSendクラスのwithを使って変数を渡しているため、展開できるはずです。
本当に変数が展開できているか、内容をログファイルに書き出してみます。

まず送信完了後の画面を用意します。

<!DOCTYPE html>
<html lang="ja">
  <head>
    <title>送信完了</title>
  </head>
  <body>
    <p>送信完了</p>
  </body>
</html>

次に、メールの内容をログファイルに書き出すように変更を加えます。 config/mail.php を確認してください。

'driver' => env('MAIL_DRIVER', 'smtp'),

これを

'driver' => env('MAIL_DRIVER', 'log'),

こう書き換えます。もちろん .env 内で MAIL_DRIVER を定義し、そちらで変更しても問題ありません。
この状態で送信処理を行うと、ログファイルにメール本文が書き込まれているはずです。
変数が展開されているか確認してみてください。

テストコードを書いてみる

メール送信までの一連の処理が書けたので、テストコードを書いていきます。 ここではMailableクラスに対するテストコードのみ記載します。 まず、テストクラスを作成します。

php artisan make:test MailSendTest

tests/Feature配下にMailSendTest.php が作成されているはずです。 作成されたMailSendTest.phpを以下のようにかきかえます。

<?php

namespace Tests\Feature;

use Tests\TestCase;
use App\Mail\MailSend;
use Illuminate\Support\Facades\Mail;

class MailSendTest extends TestCase
{
  /**
   * A basic unit test example.
   *
   * @return void
   */
  public function test_メールが送信されること()
  {
    // 実際にはメールを送らないように設定
    Mail::fake();

    // メールが送られていないことを確認
    Mail::assertNothingSent();

    $name = 'テスト太郎';
    $email = 'test@example.com';
    $body = 'これはテストメッセージです';

    // メール送信処理を実施
    $response = $this->post('/execute', ['name' => $name, 'email' => $email, 'body' => $body]);

    // メッセージが指定したユーザーに届いたことをアサート
    Mail::assertSent(MailSend::class, function ($mail) use ($email) {
        return $mail->hasTo($email);
    });

    // メールが1回送信されたことをアサート
    Mail::assertSent(MailSend::class, 1);

  }
}

細かいところはコメントに記載したので詳細は割愛します。 この状態でテストを実行し、テストが通過できればOKです。

$ ./vendor/bin/phpunit tests/Feature/MailSendTest.php

PHPUnit 8.5.8 by Sebastian Bergmann and contributors.
.                                                                   1 / 1 (100%)
Time: 695 ms, Memory: 16.00 MB
OK (1 test, 3 assertions)

まとめ

今回はLaravelのMailableクラスを使ってメール送信処理の実装と、それに対するテストコードの作成について書いてみました。 実際にメール送信処理を実装する場合にはもう少し細かい部分で考慮するべきポイントはあると思いますが、実装方法の参考になれば幸いです。 コードの誤りやよりよい方法の提案などあれば、是非コメントいただければと思います。 ありがとうございました。

参考

筆者

https://dippeople.dip-net.jp/6818/dippeople.dip-net.jp (写真右)