4
5

More than 1 year has passed since last update.

LaravelのMail送信処理を深堀りする!

Last updated at Posted at 2022-01-25

ある日Laravelでメール送信の実装をしようと思った時のこと、Laravelの公式ドキュメントを見て簡単に実装できることを知りました(以下のような書き方で実装可能です)。

Mail::send(/*引数省略*/); // Mailファサードのみで送信
Mail::to($user)->send($mailable); // Mailableクラスを使用してメールを送信

「すごい便利!」と思ったのですが、簡単すぎてなんかいまいち理解しきれていない。まあライブラリなので完全に理解する必要はないのですが、ちょっと気になるなともやもやが起きました笑

ということで、そんなもやもやを解消するために、Mailファサードについて深堀りをしようと思います。
同じもやもやを抱えてしまった方、一緒にもやもやを解消していきましょう!(「おっと、この部分は違うぞ!」と思った方はコメントで教えてください)

著者の環境

  • Laravel 8.78.1
  • PHP 8.0.8
  • MailerはAmazon SESを使用

Mailファサードについて

まずはMailファサードについてみていきます。
Mailファサードのファイルは以下のようになっています。

Mail.php
<?php

namespace Illuminate\Support\Facades;

use Illuminate\Support\Testing\Fakes\MailFake;

/**
 * @method static \Illuminate\Mail\PendingMail to($users)
 * @method static \Illuminate\Mail\PendingMail bcc($users)
 * @method static void raw(string $text, $callback)
 * @method static void send(\Illuminate\Contracts\Mail\Mailable|string|array $view, array $data = [], \Closure|string $callback = null)
   // 中略
 *
 * @see \Illuminate\Mail\Mailer
 * @see \Illuminate\Support\Testing\Fakes\MailFake
 */
class Mail extends Facade
{
    // 中略
    /**
     * Get the registered name of the component.
     *
     * @return string
     */ 
    protected static function getFacadeAccessor()
    {
        return 'mailer';
    }
}

アノテーション(@method)がたくさん書いてありますね。
これらのメソッドはMailファサードが静的メソッドっぽく使うことができるものです。
例えばMail::send();のような形で使うことができます。

facadeクラスを継承しているので、クラスには実装されていないメソッドが静的メソッドように使用することができるんですね。
実際には\Illuminate\Mail\Mailerがnewされており、そのインスタンスのメソッドが実行されています。
この辺りの依存解決はIlluminate\Mail\MailServiceProviderでなされています。

参考

ファサードについて詳しく知りたいとはという方は【Laravel】ファサードとは?何が便利か?どういう仕組みか?がかなりわかりやすいのでおすすめです。

ということで、Mailファサードのメソッドは実質Mailerクラスの動的メソッドであるため、ここからは\Illuminate\Mail\Mailerを見ていきます。
※ Mail::fake()とするとMailFakeクラスが使えるのですが、このクラスはテストの時に使用するものなので、一旦今回は割愛します。

Mailerクラスについて

ファイルの内容については、かなり多いのでMail::send(/*引数省略*/);に出てくるsendからみていこうと思います。
ちなみにMail::send(/*引数省略*/);について、引数を省略しないと以下のような実装になります。

Mail::send('emails.test_content', [
            "test" => "テストメッセージ"
        ], function($message) {
            $message
                ->to('test@example.com')
                ->subject("メールのタイトル");
        });

Mailerクラスの送信処理

それでは、Mailerクラスのsendメソッドを見ていきましょう。
処理全体はこんな感じです。
Mailableクラスを使った送信処理と、送信以外の処理はコメントアウトで何をしているか書いてみました。

Mailer.php
<?php

namespace Illuminate\Mail;

class Mailer implements MailerContract, MailQueueContract
{
     * Send a new message using a view.
     *
     * @param  \Illuminate\Contracts\Mail\Mailable|string|array  $view
     * @param  array  $data
     * @param  \Closure|string|null  $callback
     * @return void
     */
    public function send($view, array $data = [], $callback = null)
    {
        // Mailableクラスを用いた時の送信処理
        if ($view instanceof MailableContract) {
            return $this->sendMailable($view);
        }

        // $viewの中身がstringかarrayか、arrayであったら連想配列かどうかによって、$view, $plain, $rawに格納する値を分けている。
        [$view, $plain, $raw] = $this->parseView($view);

        // Messageクラスのインスタンスを作成し、各変数に格納している。
        $data['message'] = $message = $this->createMessage();

        // 第3引数のコールバック関数を実行
        $callback($message);

        // メールのbody等を作成
        $this->addContent($message, $view, $plain, $raw, $data);

        // $messageにToのアドレスをセット
        if (isset($this->to['address'])) {
            $this->setGlobalToAndRemoveCcAndBcc($message);
        }

        // Swift_Messageのインスタンスをゲッターで取得
        $swiftMessage = $message->getSwiftMessage();

        if ($this->shouldSendMessage($swiftMessage, $data)) {
            $this->sendSwiftMessage($swiftMessage);

            $this->dispatchSentEvent($message, $data);
        }
    }

ということで、if文を含めた最後の3行で送信が行われています。
この部分を深堀りしてみます。

if文の条件であるshouldSendMessage($swiftMessage, $data)についてですが、今回はtrueになります。
 (詳細は割愛します)

それではif文の中身を見ていきます。
メソッドは以下の2つですね。

Mailer.php
$this->sendSwiftMessage($swiftMessage);
$this->dispatchSentEvent($message, $data);

今回のメール送信では、上のsendSwiftMessageにてメールが送信されるます。
dispatchSentEventについては、queueを使用したとき処理されるため、今回は特に何も起きません。
そのため今回はsendSwiftMessageのみ扱います。

Mailer.phpのメール送信処理の詳細

sendSwiftMessageMailerクラスのメソッドで詳細はこのようになっています。

Mailer.php
protected function sendSwiftMessage($message)
{
    $this->failedRecipients = [];

    try {
        return $this->swift->send($message, $this->failedRecipients);
    } finally {
        $this->forceReconnection();
    }
}

この時$this->swiftにはSwift_Mailerクラスのインスタンスが入っています。
Swift_Mailerのsendメソッドでメールが送信されているんですね。
このSwift_Mailerのsendメソッドの内容は以下の通りです。

Swift_Mailer.php
public function send(Swift_Mime_SimpleMessage $message, &$failedRecipients = null)
{

    $sent = 0;

    try {
        $sent = $this->transport->send($message, $failedRecipients);
    } catch (Swift_RfcComplianceException $e) {
        foreach ($message->getTo() as $address => $name) {
            $failedRecipients[] = $address;
        }
    }

    return $sent;
}

sendメソッドの中にさらにsendメソッドがありました。
今回はMailerにAmazon SESを用いているので、$this->transportにはSesTransportのインスタンスが格納されています。
それでは、SesTransportのsendメソッドを見ていきましょう。

SesTransport.php
public function send(Swift_Mime_SimpleMessage $message, &$failedRecipients = null)
{
    $this->beforeSendPerformed($message);

    $result = $this->ses->sendRawEmail(
        array_merge(
            $this->options, [
                'Source' => key($message->getSender() ?: $message->getFrom()),
                'RawMessage' => [
                    'Data' => $message->toString(),
                ],
            ]
        )
    );

    // 後略
}

ようやく、送信処理の最深部にたどり着きました!
このsendRawEmailメソッドによって、Amazon SESにメールの送信処理がリクエストされています($this->sesにはSesClientのインスタンスが入っています)。
※実際には、さらにSesClientのexecuteメソッド(AwsClientTraitにて実装)が処理されたりするのですが、キリがないので一旦ここを最深部とさせてください笑

ということで、Mailを送信する処理はかなり階層が深くなっていました。

次にMailableを使用する場合について、書いてきます。

Mailableを使用する場合について

Mailableを使用する場合についても、実はMailerクラスのsenderメソッドが使用されます。
それでは見ていきましょう。

冒頭でも記載しましたが、以下のようなコードでメールを送信することができます。

// $mailableはMailableクラスを継承したクラスのインスタンスです
// $userにはメールアドレスやメールアドレスが格納されている配列が入ります。
Mail::to($user)->send($mailable);

まずはMail::to($user)の部分から見ていきます。

Mailer.php
public function to($users)
{
    return (new PendingMail($this))->to($users);
}

PendingMailのインスタンスを生成し、PendingMailのtoメソッドの返り値を返しています。

ちなみにPendingMailのtoメソッドは以下の通りで、$userをメンバー変数に格納してPendingMailのインスタンスを返しているだけです。

PendingMail.php
public function to($users)
{
    $this->to = $users;

    if (! $this->locale && $users instanceof HasLocalePreference) {
        $this->locale($users->preferredLocale());
    }
    return $this;
}

ということで、Mail::to($user)->send($mailable);はPendingMailのsendメソッドが実行されているのでした。
PendingMailのsendメソッドは以下のようにMailerのsendメソッドが使用されています。
$this->fill($mailable)Mailableのインスタンスを返しています。

PendingMail.php
public function send(MailableContract $mailable)
{
    $this->mailer->send($this->fill($mailable));
}

Mailer.phpのsendの処理の冒頭3行でMailableを使用したときの送信処理が書かれていました。

Mailer.php
if ($view instanceof MailableContract) {
    return $this->sendMailable($view);
}

sendMailableメソッドは次のように実装されています。

Mailer.php
protected function sendMailable(MailableContract $mailable)
{
    return $mailable instanceof ShouldQueue
                    ? $mailable->mailer($this->name)->queue($this->queue)
                    : $mailable->mailer($this->name)->send($this);
}

今回、MailableShouldQueueではないので$mailable->mailer($this->name)->send($this);が実行されます。
これをたどると、最終的に、Mailerクラスのsendメソッドに行き着きます!(ここに来て省略で申し訳ないですが笑)

おわり

ということで深堀りはこれで以上です。
ライブラリは中身を知らなくても利用できてしまいますが、深堀りをするとより使いこなせますし、何よりも勉強になりました。
あまりライブラリやフレームワークの中身を読んだことない方はぜひ読んでみてください!

最後まで読んでくださりありがとうございました!!

4
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
5