當(dāng)我使用 Laravel 的郵件發(fā)送功能時(shí),腦子里浮現(xiàn)出這么幾個(gè)問題:
Laravel 集成了 SMTP 、Mailgun 、SparkPost 、 Amazon SES 等驅(qū)動(dòng),是怎么做到的?
Laravel 提供全文本格式、網(wǎng)頁格式和 Markdown 格式,是怎么實(shí)現(xiàn)的?
整個(gè)郵件發(fā)送流程是什么樣的?
下面就讓我們開始徒手扒一扒「郵件發(fā)送功能」的實(shí)現(xiàn)原理。
寫個(gè) demo
我們使用阿里云提供的免費(fèi)郵,和采用「smtp」驅(qū)動(dòng),作為測試,參考 .env 配置:
MAIL_DRIVER=smtp
MAIL_HOST=smtp.mxhichina.com
MAIL_PORT=25
MAIL_USERNAME=***@coding01.cn
MAIL_PASSWORD=****
MAIL_ENCRYPTION=tls
MAIL_FROM=***@coding01.cn
MAIL_NAME=coding01
寫個(gè)測試流程,還是挺簡單的,具體如下:
// 1. 創(chuàng)建測試類
php artisan make:mail TestEmail
// 2. 在 TestEmail 類,載入視圖
public function build()
{
return $this->view('mail.test');
}
// 3. 輸出 hello coding01
<p>hello coding01</p>
最后寫個(gè)命令函數(shù):
Artisan::command('test', function () {
Mail::to('yemeishu@126.com')->send(new \App\Mail\TestEmail());
});
執(zhí)行 php artisan test 看測試是否發(fā)送成功:

解析 MailServiceProvider
寫了不少 Laravel 代碼,看
Mail::to('yemeishu@126.com')->send(new \App\Mail\TestEmail());
自然而然的想到是不是有一個(gè) MailServiceProvider,果不其然,在 config/app.php 的數(shù)組 providers 就包含了該 ServiceProvider

所以我們就開始圍繞這個(gè) MailServiceProvider 來解析了
/**
* Register the service provider.
*
* @return void
*/
public function register()
{
$this->registerSwiftMailer();
$this->registerIlluminateMailer();
$this->registerMarkdownRenderer();
}
看 register 函數(shù),一目了然,我們將重點(diǎn)看看這三個(gè)方法都是干嘛用的。
registerSwiftMailer
看代碼:
/**
* Register the Swift Mailer instance.
*
* @return void
*/
public function registerSwiftMailer()
{
$this->registerSwiftTransport();
// Once we have the transporter registered, we will register the actual Swift
// mailer instance, passing in the transport instances, which allows us to
// override this transporter instances during app start-up if necessary.
$this->app->singleton('swift.mailer', function ($app) {
if ($domain = $app->make('config')->get('mail.domain')) {
Swift_DependencyContainer::getInstance()
->register('mime.idgenerator.idright')
->asValue($domain);
}
return new Swift_Mailer($app['swift.transport']->driver());
});
}
很好理解,就是注冊 Swift Mailer 實(shí)例。在創(chuàng)建實(shí)例之前,執(zhí)行 $this->registerSwiftTransport();方法:
/**
* Register the Swift Transport instance.
*
* @return void
*/
protected function registerSwiftTransport()
{
$this->app->singleton('swift.transport', function ($app) {
return new TransportManager($app);
});
}
看看這個(gè) TransportManager 類是干嘛用的:
<?php
namespace Illuminate\Mail;
use Aws\Ses\SesClient;
use Illuminate\Support\Arr;
use Psr\Log\LoggerInterface;
use Illuminate\Support\Manager;
use GuzzleHttp\Client as HttpClient;
use Swift_SmtpTransport as SmtpTransport;
use Illuminate\Mail\Transport\LogTransport;
use Illuminate\Mail\Transport\SesTransport;
use Illuminate\Mail\Transport\ArrayTransport;
use Swift_SendmailTransport as MailTransport;
use Illuminate\Mail\Transport\MailgunTransport;
use Illuminate\Mail\Transport\MandrillTransport;
use Illuminate\Mail\Transport\SparkPostTransport;
use Swift_SendmailTransport as SendmailTransport;
class TransportManager extends Manager
{
/**
* Create an instance of the SMTP Swift Transport driver.
*
* @return \Swift_SmtpTransport
*/
protected function createSmtpDriver()
{
$config = $this->app->make('config')->get('mail');
// The Swift SMTP transport instance will allow us to use any SMTP backend
// for delivering mail such as Sendgrid, Amazon SES, or a custom server
// a developer has available. We will just pass this configured host.
$transport = new SmtpTransport($config['host'], $config['port']);
if (isset($config['encryption'])) {
$transport->setEncryption($config['encryption']);
}
// Once we have the transport we will check for the presence of a username
// and password. If we have it we will set the credentials on the Swift
// transporter instance so that we'll properly authenticate delivery.
if (isset($config['username'])) {
$transport->setUsername($config['username']);
$transport->setPassword($config['password']);
}
// Next we will set any stream context options specified for the transport
// and then return it. The option is not required any may not be inside
// the configuration array at all so we'll verify that before adding.
if (isset($config['stream'])) {
$transport->setStreamOptions($config['stream']);
}
return $transport;
}
/**
* Create an instance of the Sendmail Swift Transport driver.
*
* @return \Swift_SendmailTransport
*/
protected function createSendmailDriver()
{
return new SendmailTransport($this->app['config']['mail']['sendmail']);
}
/**
* Create an instance of the Amazon SES Swift Transport driver.
*
* @return \Illuminate\Mail\Transport\SesTransport
*/
protected function createSesDriver()
{
$config = array_merge($this->app['config']->get('services.ses', []), [
'version' => 'latest', 'service' => 'email',
]);
return new SesTransport(new SesClient(
$this->addSesCredentials($config)
));
}
/**
* Add the SES credentials to the configuration array.
*
* @param array $config
* @return array
*/
protected function addSesCredentials(array $config)
{
if ($config['key'] && $config['secret']) {
$config['credentials'] = Arr::only($config, ['key', 'secret']);
}
return $config;
}
/**
* Create an instance of the Mail Swift Transport driver.
*
* @return \Swift_SendmailTransport
*/
protected function createMailDriver()
{
return new MailTransport;
}
/**
* Create an instance of the Mailgun Swift Transport driver.
*
* @return \Illuminate\Mail\Transport\MailgunTransport
*/
protected function createMailgunDriver()
{
$config = $this->app['config']->get('services.mailgun', []);
return new MailgunTransport(
$this->guzzle($config),
$config['secret'], $config['domain']
);
}
/**
* Create an instance of the Mandrill Swift Transport driver.
*
* @return \Illuminate\Mail\Transport\MandrillTransport
*/
protected function createMandrillDriver()
{
$config = $this->app['config']->get('services.mandrill', []);
return new MandrillTransport(
$this->guzzle($config), $config['secret']
);
}
/**
* Create an instance of the SparkPost Swift Transport driver.
*
* @return \Illuminate\Mail\Transport\SparkPostTransport
*/
protected function createSparkPostDriver()
{
$config = $this->app['config']->get('services.sparkpost', []);
return new SparkPostTransport(
$this->guzzle($config), $config['secret'], $config['options'] ?? []
);
}
/**
* Create an instance of the Log Swift Transport driver.
*
* @return \Illuminate\Mail\Transport\LogTransport
*/
protected function createLogDriver()
{
return new LogTransport($this->app->make(LoggerInterface::class));
}
/**
* Create an instance of the Array Swift Transport Driver.
*
* @return \Illuminate\Mail\Transport\ArrayTransport
*/
protected function createArrayDriver()
{
return new ArrayTransport;
}
/**
* Get a fresh Guzzle HTTP client instance.
*
* @param array $config
* @return \GuzzleHttp\Client
*/
protected function guzzle($config)
{
return new HttpClient(Arr::add(
$config['guzzle'] ?? [], 'connect_timeout', 60
));
}
/**
* Get the default mail driver name.
*
* @return string
*/
public function getDefaultDriver()
{
return $this->app['config']['mail.driver'];
}
/**
* Set the default mail driver name.
*
* @param string $name
* @return void
*/
public function setDefaultDriver($name)
{
$this->app['config']['mail.driver'] = $name;
}
}
通過觀察,可以看出,TransportManager 主要是為了創(chuàng)建各種驅(qū)動(dòng):
Smtp—— 創(chuàng)建Swift_SmtpTransport實(shí)例對象,主要使用的參數(shù)為:host、port、encryption、username、password、stream;
Sendmail、Swift_SendmailTransport實(shí)例對象,使用的參數(shù)為:sendmail;
Ses—— 創(chuàng)建SesTransport實(shí)例對象,使用的參數(shù)為config/services下對應(yīng)的值:'ses' => [
'key' => env('SES_KEY'),
'secret' => env('SES_SECRET'),
'region' => 'us-east-1',
],
- `Mailgun` —— 創(chuàng)建 `MailgunTransport` 實(shí)例對象,使用的參數(shù)為 `config/services` 下對應(yīng)的值:'mailgun' => [
'domain' => env('MAILGUN_DOMAIN'),
'secret' => env('MAILGUN_SECRET'),
],- `Mandrill` —— 創(chuàng)建 `MandrillTransport` 實(shí)例對象,使用的參數(shù)為 `config/services` 下對應(yīng)的值:「暫無」,可以自行添加 - `SparkPost` —— 創(chuàng)建 `SparkPostTransport` 實(shí)例對象,使用的參數(shù)為 `config/services` 下對應(yīng)的值:'sparkpost' => [
'secret' => env('SPARKPOST_SECRET'),
],此外,就是創(chuàng)建 `Log` 驅(qū)動(dòng),和設(shè)置默認(rèn)的驅(qū)動(dòng),由 `app['config']['mail.driver']` 決定的。
通過上文,我們還可以看出在使用 Mailgun、Mandrill 或者 SparkPost 都需要使用插件 guzzle,這也是為什么官網(wǎng)提示要安裝 guzzle 插件的原因了:

同時(shí),這些驅(qū)動(dòng)類都是 extends Illuminate\Mail\Transport,而且抽象類 Transport 是實(shí)現(xiàn) Swift_Transport 接口:
<?php
/*
* This file is part of SwiftMailer.
* (c) 2004-2009 Chris Corbyn
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/**
* Sends Messages via an abstract Transport subsystem.
*
* @author Chris Corbyn
*/
interface Swift_Transport
{
/**
* Test if this Transport mechanism has started.
*
* @return bool
*/
public function isStarted();
/**
* Start this Transport mechanism.
*/
public function start();
/**
* Stop this Transport mechanism.
*/
public function stop();
/**
* Check if this Transport mechanism is alive.
*
* If a Transport mechanism session is no longer functional, the method
* returns FALSE. It is the responsibility of the developer to handle this
* case and restart the Transport mechanism manually.
*
* @example
*
* if (!$transport->ping()) {
* $transport->stop();
* $transport->start();
* }
*
* The Transport mechanism will be started, if it is not already.
*
* It is undefined if the Transport mechanism attempts to restart as long as
* the return value reflects whether the mechanism is now functional.
*
* @return bool TRUE if the transport is alive
*/
public function ping();
/**
* Send the given Message.
*
* Recipient/sender data will be retrieved from the Message API.
* The return value is the number of recipients who were accepted for delivery.
*
* @param Swift_Mime_SimpleMessage $message
* @param string[] $failedRecipients An array of failures by-reference
*
* @return int
*/
public function send(Swift_Mime_SimpleMessage $message, &$failedRecipients = null);
/**
* Register a plugin in the Transport.
*
* @param Swift_Events_EventListener $plugin
*/
public function registerPlugin(Swift_Events_EventListener $plugin);
}
我們利用 PhpStorm 查看有多少類實(shí)現(xiàn)該接口:

好了,有了創(chuàng)建驅(qū)動(dòng)的實(shí)例,接下來就是創(chuàng)建 Swift_Mailer 對象實(shí)例了:
$this->app->singleton('swift.mailer', function ($app) {
...
return new Swift_Mailer($app['swift.transport']->driver());
});
下面借助 $app['swift.transport']->driver() 函數(shù)來說一說怎么拿到我們指定的驅(qū)動(dòng)。
從 TransportManager 的父類 Manager 抽象類找到driver() 函數(shù):
/**
* Get the default driver name.
*
* @return string
*/
abstract public function getDefaultDriver();
/**
* Get a driver instance.
*
* @param string $driver
* @return mixed
*/
public function driver($driver = null)
{
$driver = $driver ?: $this->getDefaultDriver();
if (is_null($driver)) {
throw new InvalidArgumentException(sprintf(
'Unable to resolve NULL driver for [%s].', static::class
));
}
// If the given driver has not been created before, we will create the instances
// here and cache it so we can return it next time very quickly. If there is
// already a driver created by this name, we'll just return that instance.
if (! isset($this->drivers[$driver])) {
$this->drivers[$driver] = $this->createDriver($driver);
}
return $this->drivers[$driver];
}
主要的使用各個(gè)繼承類 (TransportManager) 實(shí)現(xiàn)的 $this->getDefaultDriver()
/**
* Get the default mail driver name.
*
* @return string
*/
public function getDefaultDriver()
{
return $this->app['config']['mail.driver'];
}
這就好理解了,指定的驅(qū)動(dòng)是由 config 自主指定的;當(dāng)拿到驅(qū)動(dòng)名稱后,我們回到 driver() 函數(shù),繼續(xù)往下看到代碼:
if (! isset($this->drivers[$driver])) {
$this->drivers[$driver] = $this->createDriver($driver);
}
// 注:$this->createDriver($driver) 這才是真正創(chuàng)建指定驅(qū)動(dòng)的方法
/**
* Create a new driver instance.
*
* @param string $driver
* @return mixed
*
* @throws \InvalidArgumentException
*/
protected function createDriver($driver)
{
// We'll check to see if a creator method exists for the given driver. If not we
// will check for a custom driver creator, which allows developers to create
// drivers using their own customized driver creator Closure to create it.
if (isset($this->customCreators[$driver])) {
return $this->callCustomCreator($driver);
} else {
$method = 'create'.Str::studly($driver).'Driver';
if (method_exists($this, $method)) {
return $this->$method();
}
}
throw new InvalidArgumentException("Driver [$driver] not supported.");
}
當(dāng)然我們的目標(biāo)就定在這里:
$method = 'create'.Str::studly($driver).'Driver';
if (method_exists($this, $method)) {
return $this->$method();
}
通過拿到的「驅(qū)動(dòng)名稱」,拼接成函數(shù)名,假如我們的驅(qū)動(dòng)名稱為:mailgun,則函數(shù)名:createMailgunDriver,然后就可以直接執(zhí)行該方法,拿到對應(yīng)的驅(qū)動(dòng)對象實(shí)例了。
注:推薦看看這個(gè)
Str::studly($driver)函數(shù)源碼
到此,我們知道了如何利用 config 配置文件,來創(chuàng)建指定的驅(qū)動(dòng)器,最后創(chuàng)建 Swift_Mailer 對象,以供之后執(zhí)行使用。
registerIlluminateMailer
看代碼:
/**
* Register the Illuminate mailer instance.
*
* @return void
*/
protected function registerIlluminateMailer()
{
$this->app->singleton('mailer', function ($app) {
$config = $app->make('config')->get('mail');
// Once we have create the mailer instance, we will set a container instance
// on the mailer. This allows us to resolve mailer classes via containers
// for maximum testability on said classes instead of passing Closures.
$mailer = new Mailer(
$app['view'], $app['swift.mailer'], $app['events']
);
if ($app->bound('queue')) {
$mailer->setQueue($app['queue']);
}
// Next we will set all of the global addresses on this mailer, which allows
// for easy unification of all "from" addresses as well as easy debugging
// of sent messages since they get be sent into a single email address.
foreach (['from', 'reply_to', 'to'] as $type) {
$this->setGlobalAddress($mailer, $config, $type);
}
return $mailer;
});
}
光看這個(gè),比較簡單,就是傳入 view、第一步創(chuàng)建好的郵件發(fā)送器Swift_Mailer 對象,和 events 事件分發(fā)器,如果有隊(duì)列,傳入隊(duì)列,創(chuàng)建 Illuminate mailer 對象,供我們真正場景使用;最后就是配置全局參數(shù)了。
registerMarkdownRenderer
Laravel 能夠捕獲很多開發(fā)者的??,還有一個(gè)核心的地方在于:知道開發(fā)者想要什么。其中 Markdown 基本就是開發(fā)者的必備。用 Markdown 寫郵件,是一個(gè)不錯(cuò)的方案,下面看看怎么做到的?
為了扒 Markdown 代碼,先寫個(gè) demo 看怎么使用。
使用命令,帶上 --markdown 選項(xiàng):
php artisan make:mail TestMdEmail --markdown=mail.testmd
這樣就可以為我們創(chuàng)建了 TestMdEmail 類
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Contracts\Queue\ShouldQueue;
class TestMdEmail 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->markdown('mail.testmd');
}
}
和視圖 testmd.blade.php,默認(rèn)視圖內(nèi)容:
@component('mail::message')
# Introduction
The body of your message.
@component('mail::button', ['url' => ''])
Button Text
@endcomponent
Thanks,<br>
{{ config('app.name') }}
@endcomponent
寫個(gè)測試,發(fā)送看看運(yùn)行效果:
Artisan::command('testmd', function () {
Mail::to('yemeishu@126.com')->send(new \App\Mail\TestMdEmail());
});

一切使用默認(rèn)的,就可以很輕易的創(chuàng)建 markdown 格式的郵件內(nèi)容,并發(fā)送。
我們可以看看源碼了:
/**
* Register the Markdown renderer instance.
*
* @return void
*/
protected function registerMarkdownRenderer()
{
if ($this->app->runningInConsole()) {
$this->publishes([
__DIR__.'/resources/views' => $this->app->resourcePath('views/vendor/mail'),
], 'laravel-mail');
}
$this->app->singleton(Markdown::class, function ($app) {
$config = $app->make('config');
return new Markdown($app->make('view'), [
'theme' => $config->get('mail.markdown.theme', 'default'),
'paths' => $config->get('mail.markdown.paths', []),
]);
});
}
目標(biāo)很簡單,就是利用配置信息,創(chuàng)建 Markdown 對象,為后續(xù)服務(wù)。
我們先看默認(rèn)的 mail config:
/*
|--------------------------------------------------------------------------
| Markdown Mail Settings
|--------------------------------------------------------------------------
|
| If you are using Markdown based email rendering, you may configure your
| theme and component paths here, allowing you to customize the design
| of the emails. Or, you may simply stick with the Laravel defaults!
|
*/
'markdown' => [
'theme' => 'default',
'paths' => [
resource_path('views/vendor/mail'),
],
],
默認(rèn)的 markdown 配置信息都存在 views/vendor/mail 文件夾下,我們可以通過命令:
$ php artisan vendor:publish --tag=laravel-mail
Copied Directory [/vendor/laravel/framework/src/Illuminate/Mail/resources/views] To [/resources/views/vendor/mail]
Publishing complete.
所有的默認(rèn)組件都存在這個(gè)文件夾下,還有頁面的視圖樣式主題等:

注:我們可以自定組件和增加發(fā)布郵箱的 css 樣式
看 Maikdown 構(gòu)造函數(shù):
/**
* Create a new Markdown renderer instance.
*
* @param \Illuminate\Contracts\View\Factory $view
* @param array $options
* @return void
*/
public function __construct(ViewFactory $view, array $options = [])
{
$this->view = $view;
$this->theme = $options['theme'] ?? 'default';
$this->loadComponentsFrom($options['paths'] ?? []);
}
主要是傳入 View 視圖構(gòu)造器和主題樣式,以及各個(gè) markdown 組件。
郵件發(fā)送流程
下面我們結(jié)合上面的 demo 看看如何構(gòu)造郵件內(nèi)容,和發(fā)送郵件的,我們看代碼:
Mail::to('yemeishu@126.com')->send(new \App\Mail\TestMdEmail());
這里的 Mail 就是上面 registerIlluminateMailer 注冊的 Illuminate\Mail\Mailer 對象。
我們且看它的 send() 方法:
/**
* Send a new message using a view.
*
* @param string|array|MailableContract $view
* @param array $data
* @param \Closure|string $callback
* @return void
*/
public function send($view, array $data = [], $callback = null)
{
if ($view instanceof MailableContract) {
return $this->sendMailable($view);
}
// First we need to parse the view, which could either be a string or an array
// containing both an HTML and plain text versions of the view which should
// be used when sending an e-mail. We will extract both of them out here.
list($view, $plain, $raw) = $this->parseView($view);
$data['message'] = $message = $this->createMessage();
// Once we have retrieved the view content for the e-mail we will set the body
// of this message using the HTML type, which will provide a simple wrapper
// to creating view based emails that are able to receive arrays of data.
call_user_func($callback, $message);
$this->addContent($message, $view, $plain, $raw, $data);
// If a global "to" address has been set, we will set that address on the mail
// message. This is primarily useful during local development in which each
// message should be delivered into a single mail address for inspection.
if (isset($this->to['address'])) {
$this->setGlobalTo($message);
}
// Next we will determine if the message should be sent. We give the developer
// one final chance to stop this message and then we will send it to all of
// its recipients. We will then fire the sent event for the sent message.
$swiftMessage = $message->getSwiftMessage();
if ($this->shouldSendMessage($swiftMessage, $data)) {
$this->sendSwiftMessage($swiftMessage);
$this->dispatchSentEvent($message, $data);
}
}
我們看第一步:
if ($view instanceof MailableContract) {
return $this->sendMailable($view);
}
執(zhí)行的 $this->sendMailable($view):
/**
* Send the message using the given mailer.
*
* @param \Illuminate\Contracts\Mail\Mailer $mailer
* @return void
*/
public function send(MailerContract $mailer)
{
$translator = Container::getInstance()->make(Translator::class);
$this->withLocale($this->locale, $translator, function () use ($mailer) {
Container::getInstance()->call([$this, 'build']);
$mailer->send($this->buildView(), $this->buildViewData(), function ($message) {
$this->buildFrom($message)
->buildRecipients($message)
->buildSubject($message)
->runCallbacks($message)
->buildAttachments($message);
});
});
}
核心的在于先執(zhí)行我們默認(rèn) build 方法:
/**
* Build the message.
*
* @return $this
*/
public function build()
{
return $this->markdown('mail.testmd');
}
這就是為什么在命令創(chuàng)建發(fā)送郵件模板類時(shí),都會(huì)默認(rèn)創(chuàng)建該 build 方法了,然后在該方法里,載入我們的構(gòu)建內(nèi)容和邏輯;在 markdown 視圖中,默認(rèn)的是運(yùn)行 $this->markdown('mail.testmd'):
/**
* Set the Markdown template for the message.
*
* @param string $view
* @param array $data
* @return $this
*/
public function markdown($view, array $data = [])
{
$this->markdown = $view;
$this->viewData = array_merge($this->viewData, $data);
return $this;
}
將視圖和視圖內(nèi)容載入對象中。
然后我們繼續(xù)回到上個(gè) send 方法中:
$mailer->send($this->buildView(), $this->buildViewData(), function ($message) {
$this->buildFrom($message)
->buildRecipients($message)
->buildSubject($message)
->runCallbacks($message)
->buildAttachments($message);
});
我們一個(gè)個(gè)方法來解析:
$this->buildView()
/**
* Build the view for the message.
*
* @return array|string
*/
protected function buildView()
{
if (isset($this->html)) {
return array_filter([
'html' => new HtmlString($this->html),
'text' => isset($this->textView) ? $this->textView : null,
]);
}
if (isset($this->markdown)) {
return $this->buildMarkdownView();
}
if (isset($this->view, $this->textView)) {
return [$this->view, $this->textView];
} elseif (isset($this->textView)) {
return ['text' => $this->textView];
}
return $this->view;
}
很顯然,執(zhí)行 $this->buildMarkdownView()
/**
* Build the Markdown view for the message.
*
* @return array
*/
protected function buildMarkdownView()
{
$markdown = Container::getInstance()->make(Markdown::class);
if (isset($this->theme)) {
$markdown->theme($this->theme);
}
$data = $this->buildViewData();
return [
'html' => $markdown->render($this->markdown, $data),
'text' => $this->buildMarkdownText($markdown, $data),
];
}
這時(shí)候,Markdown 對象就派上用場了,目標(biāo)該放在這兩個(gè)方法上了:
return [
'html' => $markdown->render($this->markdown, $data),
'text' => $this->buildMarkdownText($markdown, $data),
];
看 $markdown->render() 方法:
/**
* Render the Markdown template into HTML.
*
* @param string $view
* @param array $data
* @param \TijsVerkoyen\CssToInlineStyles\CssToInlineStyles|null $inliner
* @return \Illuminate\Support\HtmlString
*/
public function render($view, array $data = [], $inliner = null)
{
$this->view->flushFinderCache();
$contents = $this->view->replaceNamespace(
'mail', $this->htmlComponentPaths()
)->make($view, $data)->render();
return new HtmlString(($inliner ?: new CssToInlineStyles)->convert(
$contents, $this->view->make('mail::themes.'.$this->theme)->render()
));
}
和 $markdown->renderText() 方法:
/**
* Render the Markdown template into HTML.
*
* @param string $view
* @param array $data
* @return \Illuminate\Support\HtmlString
*/
public function renderText($view, array $data = [])
{
$this->view->flushFinderCache();
$contents = $this->view->replaceNamespace(
'mail', $this->markdownComponentPaths()
)->make($view, $data)->render();
return new HtmlString(
html_entity_decode(preg_replace("/[\r\n]{2,}/", "\n\n", $contents), ENT_QUOTES, 'UTF-8')
);
}
主要的邏輯,就是將 markdown 格式轉(zhuǎn)變成 html 格式,以及構(gòu)成數(shù)組 ['html', 'data'] 輸出,最后再次執(zhí)行 send 方法,并傳入閉包函數(shù),供構(gòu)建 message 服務(wù):
$mailer->send($this->buildView(), $this->buildViewData(), function ($message) {
$this->buildFrom($message)
->buildRecipients($message)
->buildSubject($message)
->runCallbacks($message)
->buildAttachments($message);
});
我們回頭再看 send 方法,未解析的代碼:
// First we need to parse the view, which could either be a string or an array
// containing both an HTML and plain text versions of the view which should
// be used when sending an e-mail. We will extract both of them out here.
list($view, $plain, $raw) = $this->parseView($view);
$data['message'] = $message = $this->createMessage();
// Once we have retrieved the view content for the e-mail we will set the body
// of this message using the HTML type, which will provide a simple wrapper
// to creating view based emails that are able to receive arrays of data.
call_user_func($callback, $message);
$this->addContent($message, $view, $plain, $raw, $data);
// If a global "to" address has been set, we will set that address on the mail
// message. This is primarily useful during local development in which each
// message should be delivered into a single mail address for inspection.
if (isset($this->to['address'])) {
$this->setGlobalTo($message);
}
// Next we will determine if the message should be sent. We give the developer
// one final chance to stop this message and then we will send it to all of
// its recipients. We will then fire the sent event for the sent message.
$swiftMessage = $message->getSwiftMessage();
if ($this->shouldSendMessage($swiftMessage, $data)) {
$this->sendSwiftMessage($swiftMessage);
$this->dispatchSentEvent($message, $data);
}
第一步無非就是將上面的數(shù)組遍歷出來,然后再創(chuàng)建 Message 對象:
$data['message'] = $message = $this->createMessage();
/**
* Create a new message instance.
*
* @return \Illuminate\Mail\Message
*/
protected function createMessage()
{
$message = new Message($this->swift->createMessage('message'));
// If a global from address has been specified we will set it on every message
// instance so the developer does not have to repeat themselves every time
// they create a new message. We'll just go ahead and push this address.
if (! empty($this->from['address'])) {
$message->from($this->from['address'], $this->from['name']);
}
// When a global reply address was specified we will set this on every message
// instance so the developer does not have to repeat themselves every time
// they create a new message. We will just go ahead and push this address.
if (! empty($this->replyTo['address'])) {
$message->replyTo($this->replyTo['address'], $this->replyTo['name']);
}
return $message;
}
這個(gè) Message 構(gòu)造函數(shù)傳入的 swift 服務(wù)對象,以后通過 message 傳入的數(shù)據(jù),都是傳給 swift 服務(wù)對象。
$message = new Message($this->swift->createMessage('message'));
...
/**
* Create a new class instance of one of the message services.
*
* For example 'mimepart' would create a 'message.mimepart' instance
*
* @param string $service
*
* @return object
*/
public function createMessage($service = 'message')
{
return Swift_DependencyContainer::getInstance()
->lookup('message.'.$service);
}
如:
/**
* Add a "from" address to the message.
*
* @param string|array $address
* @param string|null $name
* @return $this
*/
public function from($address, $name = null)
{
$this->swift->setFrom($address, $name);
return $this;
}
/**
* Set the "sender" of the message.
*
* @param string|array $address
* @param string|null $name
* @return $this
*/
public function sender($address, $name = null)
{
$this->swift->setSender($address, $name);
return $this;
}
這樣,我們就開始使用 MailServiceProvider 中創(chuàng)建的 Swift_Mailer 對象了。
好了,終于到最后一個(gè)步驟了:
// Next we will determine if the message should be sent. We give the developer
// one final chance to stop this message and then we will send it to all of
// its recipients. We will then fire the sent event for the sent message.
$swiftMessage = $message->getSwiftMessage();
if ($this->shouldSendMessage($swiftMessage, $data)) {
$this->sendSwiftMessage($swiftMessage);
$this->dispatchSentEvent($message, $data);
}
獲取 swift 服務(wù)對象,然后開始執(zhí)行發(fā)送邏輯,和分發(fā)發(fā)送郵件事件了。
/**
* Send a Swift Message instance.
*
* @param \Swift_Message $message
* @return void
*/
protected function sendSwiftMessage($message)
{
try {
return $this->swift->send($message, $this->failedRecipients);
} finally {
$this->forceReconnection();
}
}
...
/**
* Dispatch the message sent event.
*
* @param \Illuminate\Mail\Message $message
* @param array $data
* @return void
*/
protected function dispatchSentEvent($message, $data = [])
{
if ($this->events) {
$this->events->dispatch(
new Events\MessageSent($message->getSwiftMessage(), $data)
);
}
}
繼續(xù)看如何利用 swift 對象發(fā)送郵件。
/**
* Send a Swift Message instance.
*
* @param \Swift_Message $message
* @return void
*/
protected function sendSwiftMessage($message)
{
try {
return $this->swift->send($message, $this->failedRecipients);
} finally {
$this->forceReconnection();
}
}
看 $this->swift->send() 方法:
/**
* Send the given Message like it would be sent in a mail client.
*
* All recipients (with the exception of Bcc) will be able to see the other
* recipients this message was sent to.
*
* Recipient/sender data will be retrieved from the Message object.
*
* The return value is the number of recipients who were accepted for
* delivery.
*
* @param Swift_Mime_SimpleMessage $message
* @param array $failedRecipients An array of failures by-reference
*
* @return int The number of successful recipients. Can be 0 which indicates failure
*/
public function send(Swift_Mime_SimpleMessage $message, &$failedRecipients = null)
{
$failedRecipients = (array) $failedRecipients;
if (!$this->transport->isStarted()) {
$this->transport->start();
}
$sent = 0;
try {
$sent = $this->transport->send($message, $failedRecipients);
} catch (Swift_RfcComplianceException $e) {
foreach ($message->getTo() as $address => $name) {
$failedRecipients[] = $address;
}
}
return $sent;
}
還記得一開始對每個(gè)發(fā)送驅(qū)動(dòng)做封裝了吧,send 動(dòng)作,最終還是交給我們的郵件發(fā)送驅(qū)動(dòng)去執(zhí)行,默認(rèn)的是使用 SmtpTransport,即 Swift_SmtpTransport 發(fā)送。
$sent = $this->transport->send($message, $failedRecipients);
總結(jié)
過了一遍代碼,粗略了解下怎么封裝各個(gè)驅(qū)動(dòng)器,將 markdown 格式轉(zhuǎn)成 html 格式,然后再封裝成 Message 對象,交給驅(qū)動(dòng)器去發(fā)送郵件。
下一步說一說 Swift_SmtpTransport 實(shí)現(xiàn)原理,和我們自己怎么制作一個(gè)驅(qū)動(dòng)器,最后再說一說這過程用到了哪些設(shè)計(jì)模式?
未完待續(xù)