2018.10.07

サンプルコードで学ぶLaravelのメール送信機能


この記事では Laravel のメール送信機能について紹介します。単にコードの断片を載せるだけだとまぁマニュアル日本語)読めばいいって話になるので、簡単なサンプルコードを作成する流れで説明します。

Laravel のバージョンは 5.7 です。

サンプルの概要

今回はまず以下の通りユーザーがコメントを入力する機能を作成します。

Screen 1

そしてコメントを送信したタイミングでユーザー宛てにサンキューメールを送信する機能を実装します。

準備

コードの雛形生成

認証機能を自動生成。

$ php artisan make:auth

モデル、コントローラー、マイグレーションファイルを自動生成。

$ php artisan make:model Models/Comment -m -c

User.php を Models ディレクトリに移動

はじめ User.phpapp ディレクトリの直下に置かれていますが、他にもモデルが増えた際に居心地が悪いのでモデルを入れるディレクトリを追加します。

Models ディレクトリを新規作成して、User.php をその中に移動します。

$ mkdir app/Models
$ mv app/User.php app/Models/User.php

User.php は namespace を変更。

User.php
namespace App\Models;

次に config/auth.php は70行目あたりの providers の設定項目で、usersmodel の値を変更します。

auth.php
'providers' => [
    'users' => [
        'driver' => 'eloquent',
        'model' => App\Models\User::class,
    ],
],

最後に app/Http/Controllers/Auth/RegisterController.php を編集。冒頭の use 文を変更します。

RegisterController.php
use App\Models\User;

ルーティング

routes/web.php を編集します。以下を追記してください。

web.php
Route::group(['middleware' => 'auth'], function () {
    // 入力フォーム画面を返却するルート
    Route::get('/comment', 'CommentController@showForm')->name('comment');
    // 入力を受け付けるルート
    Route::post('/comment', 'CommentController@create');
    // 入力後にリダイレクトする完了画面のルート
    Route::get('/comment/thanks', 'CommentController@thanks')->name('comment.thanks');
});

マイグレーション

マイグレーションファイルは最初に実行した make:model コマンドで database/migrations ディレクトリに雛形が作成されているので以下の通り中身を記述します。

20xx_xx_xx_xxxxxx_create_comments_table.php
<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateCommentsTable extends Migration
{
    public function up()
    {
        Schema::create('comments', function (Blueprint $table) {
            $table->increments('id');
            $table->unsignedInteger('user_id');
            $table->text('body');
            $table->timestamps();
        });
    }

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

記述できたらマイグレーションを実行します。

$ php artisan migrate

コントローラー

コントローラーも make:model コマンドで app/Http/Controllers ディレクトリに雛形が作成されています。以下の通り記述してください。

CommentController.php
<?php

namespace App\Http\Controllers;

use App\Models\Comment;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class CommentController extends Controller
{
    // 入力フォーム画面
    public function showForm()
    {
        return view('comments.form');
    }

    // 入力を受け付ける
    public function create(Request $request)
    {
        $user = Auth::user();
        $comment = new Comment(['body' => $request->comment]);

        $user->comments()->save($comment);

        // TODO ここでメールを送る

        return redirect()->route('comment.thanks');
    }

    // 入力後にリダイレクトする完了画面
    public function thanks()
    {
        $comment = Auth::user()
            ->comments()
            ->orderBy('id', 'desc')
            ->first();

        return view('comments.thanks', compact('comment'));
    }
}

モデル

モデルも app/Models ディレクトリに雛形ができているので内容を記述します。

Comment.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Comment extends Model
{
    protected $fillable = [
        'body',
    ];
}

User.php にはリレーションを表現する以下のメソッドを追加します。

User.php
public function comments()
{
    return $this->hasMany('App\Models\Comment');
}

テンプレート

今回は2つの画面を追加します。

入力フォーム画面

form.blade.php
@extends('layouts.app')

@section('content')
  <div class="container">
    <div class="row justify-content-center">
      <div class="col-md-8">
        <div class="card">
          <div class="card-body">
            <form method="POST" action="{{ route('comment') }}">
              @csrf

              <div class="form-group">
                <label for="comment">コメントください</label>
                <textarea class="form-control" name="comment" id="comment" required></textarea>
              </div>

              <div class="text-right">
                <button type="submit" class="btn btn-primary">送信</button>
              </div>
            </form>
          </div>
        </div>
      </div>
    </div>
  </div>
@endsection

完了画面

thanks.blade.php
@extends('layouts.app')

@section('content')
  <div class="container">
    <div class="row justify-content-center">
      <div class="col-md-8">
        <div class="card">
          <div class="card-body">
            <p class="card-title">あなたのコメント</p>
            <p class="card-text">{{ $comment->body }}</p>
          </div>
        </div>
      </div>
    </div>
  </div>
@endsection

確認

ここまでできたらブラウザから確認しましょう。ユーザー登録して /comment に遷移してください。コメント欄が表示されたでしょうか?エラーなくコメントが送信できたらOKです。

長くなってしまいましたが、準備はここまでです。以下から、メールを送信する機能を追加していきます。

メールを送る

送信設定

メールを送信するためにはまず SMTP サーバーの設定を行います。しかし開発中など、本番用の SMTP サーバーを用意できないこともあるでしょう(お金もかかりますしね)。

そこで今回は、テスト用のメール送信サービス Mailtrap を使います。まずこちらから登録してください。Google もしくは Github アカウントでログイン可能です。ログインすると、以下のような画面が表示されるでしょう。「Demo Inbox」が作成されています。

Mailtrap 1

「Demo Inbox」をクリックすると SMTP の接続情報が確認できます(下の画像はユーザー名とパスワードをぼかしてあります)。

Mailtrap 2

.env に SMTP の接続情報を記載します。

.env
MAIL_DRIVER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=xxxxxxxxxxxxxx
MAIL_PASSWORD=xxxxxxxxxxxxxx
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS=sample@email.com
MAIL_FROM_NAME=サンプルアプリ

これで設定は完了です。メール関連の設定は config/mail.php に記述されていますが、設定値は .env を参照してるので編集するのは .env だけでOKです。

Mailable クラスを作成する

送信されるメールの内容(題名、From、本文、添付)は、Mailable クラスにまとめられます。

まずは以下の artisan コマンドで Mailable クラスを生成します。

$ php artisan make:mail CommentPosted

Mailable クラスの名前は、どんなときに送られるか?を表すものにするとよいようです。今回は「コメントが送られたとき」なので、CommentPosted にしました。

さて app/Mail ディレクトリに CommentPosted.php の雛形が作成されていますね。内容は以下の通り記述します。

CommentPosted.php
<?php

namespace App\Mail;

use App\Models\Comment;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Contracts\Queue\ShouldQueue;

class CommentPosted extends Mailable
{
    use Queueable, SerializesModels;

    public $user;
    public $comment;

    public function __construct(User $user, Comment $comment)
    {
        $this->user = $user;
        $this->comment = $comment;
    }

    public function build()
    {
        return $this
            ->subject('コメントありがとうございます')
            ->view('emails.comments.posted');
    }
}

Mailable クラスには基本的に2つのメソッドを実装します。コンストラクタと build メソッドです。

コンストラクタ

コンストラクタではメール本文中で表示したいデータを受け取って、プロパティに代入します。Mailable クラスの public プロパティは後に指定するテンプレート内で参照することができます。

build メソッド

ここでは Mailable クラスが持つメソッドを組み合わせてメールの内容を作成します。よく使うメソッドを以下に紹介します。

メソッド 用途
from 送信元のアドレスと送信名を指定する
subject 件名を指定する
view 本文のテンプレート名を指定する(HTML)
text 本文のテンプレート名を指定する(テキスト)

今回のようにfrom メソッドを省略した場合は config/mail.php に設定されたデフォルトの from 情報が使われます。

mail.php
'from' => [
    'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
    'name' => env('MAIL_FROM_NAME', 'Example'),
],

メール用のテンプレート

メールにも Blade テンプレートを用います。

resources/emails/comments/posts.php を作成してください。

posted.php
<p>
  {{ $user->name }} さん、<br>
  コメントありがとうございます!
</p>
<p>
  あなたのコメント:<br>
  『{{ $comment->body }}』
</p>

<p><br> タグがあることからも分かるように、HTML メールが送信されます。

コントローラー

メールの送信は、Mail ファサードが行います。

CommentController.php
// 追加
use Illuminate\Support\Facades\Mail;

class CommentController extends Controller
{
    // 中略

    public function create(Request $request)
    {
        $user = Auth::user();
        $comment = new Comment(['body' => $request->comment]);

        $user->comments()->save($comment);

        // 追加
        Mail::to($user)->send(new CommentPosted($user, $comment));

        return redirect()->route('comment.thanks');
    }

    // 中略
}

to メソッドに宛先を渡し、さらに send メソッドに Mailable クラスを渡すとメールが送信されます。「誰に to(user)」「何を send(Mailable)」送るかという送信処理そのものを受け持つのが Mail ファサードなのですね。

to メソッドに宛先として渡せる値は何種類かあります。

  • メールアドレス文字列
  • オブジェクト
  • オブジェクトのコレクション

オブジェクトまたはオブジェクトのコレクションが渡された場合は、email プロパティが存在するものとみなされ、その値が宛先として使用されます。

確認

ここまでできたら再度コメントを送信してみてください。Mailtrap のダッシュボードから以下のようにメールが送られていることが確認できます。ちなみに本来の宛先には送信されません。

Mailtrap 3

Mailtrap、実は今回初めて使ってみたのですが便利ですね!無料プランだと以下の制限がありますが、少なくとも個人で開発する分には手軽で 👍 です。

  • 受信ボックスは1つ
  • メールの保管は50通まで
  • 受信するのは1秒間に2通まで
  • メンバー追加不可

そして Laravel ではメールも非常に簡単なコードで送信できてしまいますね!

まとめ

メールを送るには、3つの登場人物が必要です。

登場人物 役割
Mailable クラス 送信物、題名、From、本文、添付など
テンプレート 具体的な本文の内容
Mail ファサード 送信処理、誰に何を送るか

実装の課題

簡単にメール送信は実装できたのですが、ここまでの実装方法には課題があります。

実際にコメント送信機能を動かしていただくと分かるかと思いますが、メール送信を追加する前と比べて明らかに体感できるほど動作が遅くなっています。コントローラーのメールを送る行をコメントアウトしたり戻したりしながら試してみるとよいです。メールの送信はアプリケーションにとって結構重たい処理なのですね。

ただこれには解決策があります。メール送信を非同期に処理すればよいのです。

上記のコントローラーのコードでは、時間のかかるメール送信が完了するまで待ってから画面を返していました(厳密にはリダイレクトですね)。「非同期に」というのは、メール送信が完了するのを待たずに画面を返してしまうということです。

ここからは、Laravel のキューの機能を使ってメールを非同期に送信する方法を紹介します。

メールを非同期に送信する

キューを使う

非同期処理を実現するには「キュー」という技術を使います。 以下は全体の流れを表した図です。

Figure 1

まずアプリケーションが非同期で実行したい処理の内容をキューに追加します。

この「処理の内容」をジョブと呼びます。具体的には、どのクラスのどのメソッドをどういう引数で実行するか、という情報が文字列にシリアライズされたデータです。

キューはジョブの入れ物です。リレーショナルデータベースや Redis などの NoSQL データストアが用いられます。

次に登場するのがワーカーです。ワーカーはキューを監視するプログラムです。一定間隔で常にキューに問い合わせをし続け、ジョブがあれば取り出して処理を実行します。

説明すると複雑な感じですが、Laravel ではフレームワークがほとんど面倒を見てくれているので、少ない記述でキューを使ったメールの非同期送信が実現できます。

データベース

前述の通り、キューはジョブが格納できれば RDB でも NoSQL でもいいのですが、今回は Laravel で手軽に導入できる RDB を使用する方法を紹介します。

まずは .env に、キューとしてデータベースを使う設定を記述します。

.env
QUEUE_CONNECTION=database

次にキュー用のテーブルを作成します。queue:table コマンドでマイグレーションファイルを作成し、migrate でデータベースに適用します。

$ php artisan queue:table
$ php artisan migrate

マイグレーションができたらデータベースを確認しましょう。jobs というテーブルが作成されているはずです。

コントローラー

そして送信する箇所のメソッドを send から queue に変更します。

CommentController.php
Mail::to($user)->queue(new CommentPosted($user, $comment));

実装はたったこれだけです!

ワーカーを起動する

監視用のワーカープロセスを起動しましょう。

$ php artisan queue:work

コンソールはカーソルが戻ってこない状態になり、ワーカーが処理を開始したときと完了したときにログが表示されます。ワーカーのプロセスを中断するには ctrl+C を入力してください。

確認

では再び画面から動作を確認しましょう。

いかがでしょうか?画面の応答が早くなったのではないでしょうか。Mailtrap を見るとメールが送られていることも分かりますね。

Supervisor

先ほど queue:work コマンドでワーカーを起動しましたが、本番環境では Supervisor などのプロセス管理システムを利用してワーカーを制御します。Supervisor を使うと下記のメリットがあります。

  • ワーカーをバックグラウンドで起動できる。
  • ワーカーが処理に失敗したときに自動で再起動してくれる。

Supervisor の設定方法は OS によって異なるので、本記事ではオマケとして Mac でのインストール〜設定〜起動方法を紹介します。以降の手順を試す場合は、queue:work コマンドは中断させておいてください。

まずは Homebrew で Supervisor をインストールします。

$ brew install supervisor

設定ファイルを作成します。任意の場所でいいのですが、今回は /usr/local/etc/supervisor を設定ファイルの格納ディレクトリとします。conf.d ディレクトリには Supervisor で管理するプログラムごとの設定ファイルを格納します。

$ mkdir -p /usr/local/etc/supervisor/conf.d

echo_supervisord_conf コマンドでデフォルト状態の設定を作成します。

$ echo_supervisord_conf > /usr/local/etc/supervisor/supervisord.conf
$ vi /usr/local/etc/supervisor/supervisord.conf

最後の行のみ編集します。各プログラムの設定ファイルの場所ですね。

supervisord.conf
[include]
files = /usr/local/etc/supervisor/conf.d/*.conf

次にLaravel のワーカー用の設定ファイルを作成します。

$ vi /usr/local/etc/supervisor/conf.d/laravel-worker.conf

以下の内容を記述しましょう。

laravel-worker.conf
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /プロジェクトまでのパス/artisan queue:work
autostart=true
autorestart=true
user=実行ユーザー名(ログインユーザーで良いかと)
numprocs=3
redirect_stderr=true
stdout_logfile=/プロジェクトまでのパス/worker.log

Supervisor を起動して…

$ supervisord -c /usr/local/etc/supervisor/supervisord.conf
$ supervisorctl start laravel-worker:*

起動状態を確認すると、numprocs で設定した通り3つプロセスが立ち上がっています。

$ supervisorctl status all
laravel-worker:laravel-worker_00   RUNNING   pid 80202, uptime 0:04:08
laravel-worker:laravel-worker_01   RUNNING   pid 80203, uptime 0:04:08
laravel-worker:laravel-worker_02   RUNNING   pid 80204, uptime 0:04:08

ここまでで Supervisor の設定は完了です。前段と同様に動作することを確認しましょう。

ファイルを添付する

最後に、メールにファイルを添付する方法を紹介します。ファイル添付も複雑なコードは必要ありません。

ワーカーの再起動

ここで一点注意です。キューでメールを処理している場合は、ファイルに編集を加えたあとにワーカーを再起動させましょう。

// artisan コマンドを使っている場合
$ php artisan queue:restart
// Supervisor を使っている場合
$ supervisorctl restart laravel-worker:*

ワーカーはジョブを処理すると説明しました。そしてジョブの中身はどのクラスのどのメソッドをどういう引数で呼び出すかという情報でした。つまりクラスを呼び出すためにワーカーはアプリケーションのコピーを持っていると考えてください。

ワーカーは起動しっぱなしのプログラムなので、起動した時点の状態のアプリケーションのコピーを持っていて、コードの変更を反映させるためには再起動が必要なのです。

添付ファイル

さて、コメントをくれたユーザーには猫の GIF をプレゼントしましょう!今回はこちらの GIF (via GIPHY)を使います。まぁどれでもお気に入りのやつでいいんですけどね。

Cat GIF

右クリックでダウンロードしたら、storage ディレクトリに cat.gif という名前で移動してください。

Mailable クラスを編集します。

CommentPosted.php
public function build()
{
    return $this
        ->subject('コメントありがとうございます')
        ->view('emails.comments.posted')
        ->attach(storage_path('cat.gif'));
}

attach メソッドを追加します。

これだけです 😃✨

本文中に貼り付ける

次に、画像を本文中に貼り付ける方法を紹介します。

posted.blade.php
<p>
  {{ $user->name }} さん、<br>
  コメントありがとうございます!
</p>
<p>
  あなたのコメント:<br>
  『{{ $comment->body }}』
</p>
<p>
  <img src="{{ $message->embed(storage_path('cat.gif')) }}">
</p>

メールの本文となるテンプレートでは、自動的に $message という変数が使えるようになります。$message から embed メソッドを呼び出してください。引数はファイルのパスです。

画面から動作を確認しましょう。メール本文に GIF が表示されているでしょうか。


以上、Laravel のメール送信機能について紹介しました。パターンさえ分かれば簡単な記述でだいたいのことが実現できてしまう Laravel の魅力が伝われば幸いです 🤗