栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > PHP

说一说 Laravel 邮件发送流程

PHP 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

说一说 Laravel 邮件发送流程

当我使用 Laravel 的邮件发送功能时,脑子里浮现出这么几个问题:

  1. Laravel 集成了 SMTP 、Mailgun 、SparkPost 、 Amazon SES 等驱动,是怎么做到的?

  2. Laravel 提供全文本格式、网页格式和 Markdown 格式,是怎么实现的?

  3. 整个邮件发送流程是什么样的?

下面就让我们开始徒手扒一扒「邮件发送功能」的实现原理。

写个 demo

我们使用阿里云提供的免费邮,和采用「smtp」驱动,作为测试,参考 .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

写个测试流程,还是挺简单的,具体如下:

// 1. 创建测试类
php artisan make:mail TestEmail

// 2. 在 TestEmail 类,载入视图

public function build()
{
    return $this->view('mail.test');
}

// 3. 输出 hello coding01

hello coding01

最后写个命令函数:

Artisan::command('test', function () {
    Mail::to('yemeishu@126.com')->send(new AppMailTestEmail());
});

执行 php artisan test 看测试是否发送成功:

解析 MailServiceProvider

写了不少 Laravel 代码,看

 Mail::to('yemeishu@126.com')->send(new AppMailTestEmail());

自然而然的想到是不是有一个 MailServiceProvider,果不其然,在 config/app.php 的数组 providers 就包含了该 ServiceProvider

所以我们就开始围绕这个 MailServiceProvider 来解析了


public function register()
{
    $this->registerSwiftMailer();

    $this->registerIlluminateMailer();

    $this->registerMarkdownRenderer();
}

看 register 函数,一目了然,我们将重点看看这三个方法都是干嘛用的。

registerSwiftMailer

看代码:


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 实例。在创建实例之前,执行 $this->registerSwiftTransport();方法:


protected function registerSwiftTransport()
{
    $this->app->singleton('swift.transport', function ($app) {
 return new TransportManager($app);
    });
}

看看这个 TransportManager 类是干嘛用的:

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;
    }

    
    protected function createSendmailDriver()
    {
 return new SendmailTransport($this->app['config']['mail']['sendmail']);
    }

    
    protected function createSesDriver()
    {
 $config = array_merge($this->app['config']->get('services.ses', []), [
     'version' => 'latest', 'service' => 'email',
 ]);

 return new SesTransport(new SesClient(
     $this->addSesCredentials($config)
 ));
    }

    
    protected function addSesCredentials(array $config)
    {
 if ($config['key'] && $config['secret']) {
     $config['credentials'] = Arr::only($config, ['key', 'secret']);
 }

 return $config;
    }

    
    protected function createMailDriver()
    {
 return new MailTransport;
    }

    
    protected function createMailgunDriver()
    {
 $config = $this->app['config']->get('services.mailgun', []);

 return new MailgunTransport(
     $this->guzzle($config),
     $config['secret'], $config['domain']
 );
    }

    
    protected function createMandrillDriver()
    {
 $config = $this->app['config']->get('services.mandrill', []);

 return new MandrillTransport(
     $this->guzzle($config), $config['secret']
 );
    }

    
    protected function createSparkPostDriver()
    {
 $config = $this->app['config']->get('services.sparkpost', []);

 return new SparkPostTransport(
     $this->guzzle($config), $config['secret'], $config['options'] ?? []
 );
    }

    
    protected function createLogDriver()
    {
 return new LogTransport($this->app->make(LoggerInterface::class));
    }

    
    protected function createArrayDriver()
    {
 return new ArrayTransport;
    }

    
    protected function guzzle($config)
    {
 return new HttpClient(Arr::add(
     $config['guzzle'] ?? [], 'connect_timeout', 60
 ));
    }

    
    public function getDefaultDriver()
    {
 return $this->app['config']['mail.driver'];
    }

    
    public function setDefaultDriver($name)
    {
 $this->app['config']['mail.driver'] = $name;
    }
}

通过观察,可以看出,TransportManager 主要是为了创建各种驱动:

  • Smtp —— 创建 Swift_SmtpTransport 实例对象,主要使用的参数为:host、port、encryption、username、password、stream;

  • Sendmail、Mail —— 创建 Swift_SendmailTransport 实例对象,使用的参数为:sendmail;

  • Ses —— 创建 SesTransport 实例对象,使用的参数为 config/services 下对应的值:
'ses' => [
'key' => env('SES_KEY'),
'secret' => env('SES_SECRET'),
'region' => 'us-east-1',
],
  • Mailgun —— 创建 MailgunTransport 实例对象,使用的参数为 config/services 下对应的值:
'mailgun' => [
'domain' => env('MAILGUN_DOMAIN'),
'secret' => env('MAILGUN_SECRET'),
],
  • Mandrill —— 创建 MandrillTransport 实例对象,使用的参数为 config/services 下对应的值:「暂无」,可以自行添加

  • SparkPost —— 创建 SparkPostTransport 实例对象,使用的参数为 config/services 下对应的值:
'sparkpost' => [
'secret' => env('SPARKPOST_SECRET'),
],

此外,就是创建 Log 驱动,和设置默认的驱动,由 app['config']['mail.driver'] 决定的。

通过上文,我们还可以看出在使用 Mailgun、Mandrill 或者 SparkPost 都需要使用插件 guzzle,这也是为什么官网提示要安装 guzzle 插件的原因了:

同时,这些驱动类都是 extends IlluminateMailTransport,而且抽象类 Transport 是实现 Swift_Transport 接口:


我们利用 PhpStorm 查看有多少类实现该接口:

好了,有了创建驱动的实例,接下来就是创建 Swift_Mailer 对象实例了:

$this->app->singleton('swift.mailer', function ($app) {

    ...

    return new Swift_Mailer($app['swift.transport']->driver());
});

下面借助 $app['swift.transport']->driver() 函数来说一说怎么拿到我们指定的驱动。

从 TransportManager 的父类 Manager 抽象类找到driver() 函数:


abstract public function getDefaultDriver();


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];
}

主要的使用各个继承类 (TransportManager) 实现的 $this->getDefaultDriver()


public function getDefaultDriver()
{
    return $this->app['config']['mail.driver'];
}

这就好理解了,指定的驱动是由 config 自主指定的;当拿到驱动名称后,我们回到 driver() 函数,继续往下看到代码:

if (! isset($this->drivers[$driver])) {
    $this->drivers[$driver] = $this->createDriver($driver);
}

// 注:$this->createDriver($driver) 这才是真正创建指定驱动的方法


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.");
}

当然我们的目标就定在这里:

$method = 'create'.Str::studly($driver).'Driver';

if (method_exists($this, $method)) {
    return $this->$method();
}

通过拿到的「驱动名称」,拼接成函数名,假如我们的驱动名称为:mailgun,则函数名:createMailgunDriver,然后就可以直接执行该方法,拿到对应的驱动对象实例了。

注:推荐看看这个 Str::studly($driver) 函数源码

到此,我们知道了如何利用 config 配置文件,来创建指定的驱动器,最后创建 Swift_Mailer 对象,以供之后执行使用。

registerIlluminateMailer

看代码:


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;
    });
}

光看这个,比较简单,就是传入 view、第一步创建好的邮件发送器Swift_Mailer 对象,和 events 事件分发器,如果有队列,传入队列,创建 Illuminate mailer 对象,供我们真正场景使用;最后就是配置全局参数了。

registerMarkdownRenderer

Laravel 能够捕获很多开发者的,还有一个核心的地方在于:知道开发者想要什么。其中 Markdown 基本就是开发者的必备。用 Markdown 写邮件,是一个不错的方案,下面看看怎么做到的?

为了扒 Markdown 代码,先写个 demo 看怎么使用。

使用命令,带上 --markdown 选项:

php artisan make:mail TestMdEmail --markdown=mail.testmd

这样就可以为我们创建了 TestMdEmail 类

markdown('mail.testmd');
    }
}

和视图 testmd.blade.php,默认视图内容:

@component('mail::message')
# Introduction

The body of your message.

@component('mail::button', ['url' => ''])
Button Text
@endcomponent

Thanks,
{{ config('app.name') }} @endcomponent

写个测试,发送看看运行效果:

Artisan::command('testmd', function () {
    Mail::to('yemeishu@126.com')->send(new AppMailTestMdEmail());
});

一切使用默认的,就可以很轻易的创建 markdown 格式的邮件内容,并发送。

我们可以看看源码了:


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', []),
 ]);
    });
}

目标很简单,就是利用配置信息,创建 Markdown 对象,为后续服务。

我们先看默认的 mail config:



'markdown' => [
    'theme' => 'default',
    'paths' => [
 resource_path('views/vendor/mail'),
    ],
],

默认的 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.

所有的默认组件都存在这个文件夹下,还有页面的视图样式主题等:

注:我们可以自定组件和增加发布邮箱的 css 样式

看 Maikdown 构造函数:


public function __construct(ViewFactory $view, array $options = [])
{
    $this->view = $view;
    $this->theme = $options['theme'] ?? 'default';
    $this->loadComponentsFrom($options['paths'] ?? []);
}

主要是传入 View 视图构造器和主题样式,以及各个 markdown 组件。

邮件发送流程

下面我们结合上面的 demo 看看如何构造邮件内容,和发送邮件的,我们看代码:

Mail::to('yemeishu@126.com')->send(new AppMailTestMdEmail());

这里的 Mail 就是上面 registerIlluminateMailer 注册的 IlluminateMailMailer 对象。

我们且看它的 send() 方法:


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);
}

执行的 $this->sendMailable($view):


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);
 });
    });
}

核心的在于先执行我们默认 build 方法:


public function build()
{
    return $this->markdown('mail.testmd');
}

这就是为什么在命令创建发送邮件模板类时,都会默认创建该 build 方法了,然后在该方法里,载入我们的构建内容和逻辑;在 markdown 视图中,默认的是运行 $this->markdown('mail.testmd'):


public function markdown($view, array $data = [])
{
    $this->markdown = $view;
    $this->viewData = array_merge($this->viewData, $data);

    return $this;
}

将视图和视图内容载入对象中。

然后我们继续回到上个 send 方法中:

$mailer->send($this->buildView(), $this->buildViewData(), function ($message) {
    $this->buildFrom($message)
  ->buildRecipients($message)
  ->buildSubject($message)
  ->runCallbacks($message)
  ->buildAttachments($message);
});

我们一个个方法来解析:

$this->buildView()


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;
}

很显然,执行 $this->buildMarkdownView()


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),
    ];
}

这时候,Markdown 对象就派上用场了,目标该放在这两个方法上了:

return [
    'html' => $markdown->render($this->markdown, $data),
    'text' => $this->buildMarkdownText($markdown, $data),
];

看 $markdown->render() 方法:


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() 方法:



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("/[rn]{2,}/", "nn", $contents), ENT_QUOTES, 'UTF-8')
    );
}

主要的逻辑,就是将 markdown 格式转变成 html 格式,以及构成数组 ['html', 'data'] 输出,最后再次执行 send 方法,并传入闭包函数,供构建 message 服务:

$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);
    }

第一步无非就是将上面的数组遍历出来,然后再创建 Message 对象:

$data['message'] = $message = $this->createMessage();


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;
}

这个 Message 构造函数传入的 swift 服务对象,以后通过 message 传入的数据,都是传给 swift 服务对象。

$message = new Message($this->swift->createMessage('message'));

...


public function createMessage($service = 'message')
{
    return Swift_DependencyContainer::getInstance()
 ->lookup('message.'.$service);
}

如:


public function from($address, $name = null)
{
    $this->swift->setFrom($address, $name);

    return $this;
}


public function sender($address, $name = null)
{
    $this->swift->setSender($address, $name);

    return $this;
}

这样,我们就开始使用 MailServiceProvider 中创建的 Swift_Mailer 对象了。

好了,终于到最后一个步骤了:

// 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 服务对象,然后开始执行发送逻辑,和分发发送邮件事件了。


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

...


protected function dispatchSentEvent($message, $data = [])
{
    if ($this->events) {
 $this->events->dispatch(
     new EventsMessageSent($message->getSwiftMessage(), $data)
 );
    }
}

继续看如何利用 swift 对象发送邮件。


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

看 $this->swift->send() 方法:


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;
}

还记得一开始对每个发送驱动做封装了吧,send 动作,最终还是交给我们的邮件发送驱动去执行,默认的是使用 SmtpTransport,即 Swift_SmtpTransport 发送。

$sent = $this->transport->send($message, $failedRecipients);
总结

过了一遍代码,粗略了解下怎么封装各个驱动器,将 markdown 格式转成 html 格式,然后再封装成 Message 对象,交给驱动器去发送邮件。

下一步说一说 Swift_SmtpTransport 实现原理,和我们自己怎么制作一个驱动器,最后再说一说这过程用到了哪些设计模式?

未完待续

转载请注明:文章转载自 www.mshxw.com
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号