赞
踩
相关:使用Redis实现队列
使用容器提供者实现发送消息的优势
前几篇大致重新梳理了一下优雅的完成队列发送消息的逻辑,既然前奏已经吹响,那么接下来就是要发送消息的实现了,按照我的逻辑,我下面是使用的发送消息是邮件触达用户,那就直接步入正题吧
laravle自带的有消息通知的功能
其实在项目开发中,发送邮件,你会直接使用
Mail::send() 或者 Mail::to()->send()
但是在使用的时候,你会不会有以下疑问:
- Laravel 集成了 SMTP 、Mailgun 、SparkPost 、 Amazon SES 等驱动,是怎么做到的?
- Laravel 提供全文本格式、网页格式和 Markdown 格式,是怎么实现的?
- 整个邮件发送流程是什么样的?
根据文章服务容器提供者我们可以了解到,使用Mail调用时,是使用到了laravel的Facade,对应的只返回了一个字符串‘mail.manager’;那么有facade就有对应的MailServiceProvider,并且肯定在app.php中能找到对应的配置
这里就可以看到当你调用Mail::to等时,就是通过键‘Mail’找到了对应的facade类‘Illuminate\Support\Facades\Mail::class’;那么接下来我们就去找找这个facade返回的‘mail.manager’绑定了哪个服务提供者
根据上篇了解到的,项目启动时,会通过index.php文件加载autoload.php和bootstrap/app.php文件,在app.php文件你会看到我们通常使用的app(‘key’)调用某实例中的app对象是Illuminate\Foundation\Application的实例,那么我们就可以打开该文件,搜索registerCoreContainerAliases方法,这里是整个项目提供的出来的,这里可以看到
'mail.manager' => [\Illuminate\Mail\MailManager::class, \Illuminate\Contracts\Mail\Factory::class],
这里就是你使用Mail时最终指向的类是new MailManager;
同样地,想通过app(‘key’)的形式调用,就需要有对应的服务提供者来绑定需要实例的类(MailServiceProvider),打开同样是有register等固定方法;
class MailServiceProvider extends ServiceProvider implements DeferrableProvider { /** * Register the service provider. * * @return void */ public function register() { $this->registerIlluminateMailer(); $this->registerMarkdownRenderer(); } /** * Register the Illuminate mailer instance. * * @return void */ protected function registerIlluminateMailer() { $this->app->singleton('mail.manager', function ($app) { return new MailManager($app); }); $this->app->bind('mailer', function ($app) { return $app->make('mail.manager')->mailer(); }); } /** * 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', []), ]); }); } /** * Get the services provided by the provider. * * @return array */ public function provides() { return [ 'mail.manager', 'mailer', Markdown::class, ]; } }
这里就可以一眼看出来,mail.manager是实例化了一个MailManager类,并且执行mail.manager对应类中的mailer方法;
接下来就来看看MailServiceProvider实例化MailManager时,发生了什么??
new一个MailManager对象,并调用了mailer方法;
public function mailer($name = null)
{
$name = $name ?: $this->getDefaultDriver(); //smtp
return $this->mailers[$name] = $this->get($name);//返回了一个驱动为name的Swift Mailer实例,并在创建前执行了createSwiftMailer方法,创建对应的驱动
}
$name很好理解,使用getDefaultDriver方法去配置文件中获取配置的邮件驱动名字
public function getDefaultDriver()
{
// Here we will check if the "driver" key exists and if it does we will use
// that as the default driver in order to provide support for old styles
// of the Laravel mail configuration file for backwards compatibility.
return $this->app['config']['mail.driver'] ??
$this->app['config']['mail.default'];
}
下一步就是调用的get()方法,并返回数据mailer的$name键对应的值(以下 $name默认为smtp)
/** * Attempt to get the mailer from the local cache. * * @param string $name * @return \Illuminate\Mail\Mailer */ protected function get($name) { return $this->mailers[$name] ?? $this->resolve($name); } /** * Resolve the given mailer. * * @param string $name * @return \Illuminate\Mail\Mailer * * @throws \InvalidArgumentException */ protected function resolve($name) { $config = $this->getConfig($name); //smtp if (is_null($config)) { throw new InvalidArgumentException("Mailer [{$name}] is not defined."); } // Once we have created 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( $name, $this->app['view'], $this->createSwiftMailer($config), $this->app['events'] ); if ($this->app->bound('queue')) { $mailer->setQueue($this->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 these will be sent to a single email address. foreach (['from', 'reply_to', 'to', 'return_path'] as $type) { $this->setGlobalAddress($mailer, $config, $type); } return $mailer; } protected function getConfig(string $name) { // Here we will check if the "driver" key exists and if it does we will use // the entire mail configuration file as the "driver" config in order to // provide "BC" for any Laravel <= 6.x style mail configuration files. return $this->app['config']['mail.driver'] ? $this->app['config']['mail'] : $this->app['config']["mail.mailers.{$name}"]; }
这里的主要看resolve方法,第一步是根据name去配置文件获取对应的配置信息,此处返回是smtp对应的数据数据,包含如下:
'smtp' => [
'transport' => 'smtp',
'host' => env('MAIL_HOST', 'smtp.mailgun.org'),
'port' => env('MAIL_PORT', 587),
'encryption' => env('MAIL_ENCRYPTION', 'tls'),
'username' => env('MAIL_USERNAME'),
'password' => env('MAIL_PASSWORD'),
'timeout' => null,
'auth_mode' => null,
]
第二步就是new Mailer;这里重点就来了,
1.以前老是听说发邮件时,驱动实例化并不是在放入队列的时候,而是真实发消息的才实例,现在看到源码可是明白了;
2.官网说为啥使用一些驱动时,为啥一定要有Guzzle HTTP库
这里马上也就可以知晓了
new Mailer时,总共有四个参数,着重来看以下第三个,也就是使用配置文件对应的配置实例化一个Swift_Mailer类,
/** * Create the SwiftMailer instance for the given configuration. * * @param array $config * @return \Swift_Mailer */ protected function createSwiftMailer(array $config) { if ($config['domain'] ?? false) { Swift_DependencyContainer::getInstance() ->register('mime.idgenerator.idright') ->asValue($config['domain']); } return new Swift_Mailer($this->createTransport($config));//创建了Swift_Mailer实例 } /** * Create a new transport instance. * * @param array $config * @return \Swift_Transport */ public function createTransport(array $config) { // Here we will check if the "transport" key exists and if it doesn't we will // assume an application is still using the legacy mail configuration file // format and use the "mail.driver" configuration option instead for BC. $transport = $config['transport'] ?? $this->app['config']['mail.driver']; if (isset($this->customCreators[$transport])) { return call_user_func($this->customCreators[$transport], $config); } if (trim($transport) === '' || ! method_exists($this, $method = 'create'.ucfirst($transport).'Transport')) { //根据获取的驱动名字,去调用对应的创建驱动的方法,比如为smtp时,调用createSmtpTransport方法 throw new InvalidArgumentException("Unsupported mail transport [{$transport}]."); } return $this->{$method}($config); }
这里着重看一下驱动的创建历程;
这里根据前面已经拿到的配置信息,经过一系列判断,最终返回
$this->{$method}($config); //method=根据驱动名字对应的createTransport方法
这里你可以看到接下来就是根据项目配置的驱动来具体调用对应的方法实现创建各种驱动,总结如下:
- smtp : 创建 Swift_SmtpTransport 实例对象,主要使用的参数为:host、port、encryption、username、password、stream;
- Sendmail 和Mail : 创建 Swift_SendmailTransport 实例对象,使用的参数为:sendmail;
- Ses : 创建 SesTransport 实例对象,使用的参数为 config/services 下对应的值:
- Mailgun : 创建 Swift_SmtpTransport 实例对象,主要使用的参数为:domain,secret, endpoint;
- Postmark 、Log、 Array: 等;可以具体看源码
在看到创建Mailgun 驱动时候,你会发现它调用的有Guzzle方法,这里就解释了文档上说的在使用基于API的驱动时,一定要安装Guzzl HTTP库,真相了----
同时,你会发现这些驱动类都是Illuminate\Mail\Transport,而且抽象类Transport是实现Swift_Transport 接口的;
以上就是如何利用config配置文件,来创建指定驱动器,最后创建了Swift Mailer对象,以供后续使用
laravel这点很招揽人心,就是它知道开发者想要什么。其中 Markdown 基本就是开发者的必备。用 Markdown 写邮件,是一个不错的方案,具体的我等下一篇再分享记录吧,我赶着实现队列发送邮件的流程
在调用send或者to等方法时,你会发现最底层实际是contract,是个接口;这个接口中定义了一些常用的方法:to、cc、bcc、raw、send等方法,使用接口可以给开发者提供很多便利,谁想自己重新来过,只需要实现MailableContract这个接口中定义的通用方法就好,当然也可以自我扩展
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) { 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. [$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. $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->setGlobalToAndRemoveCcAndBcc($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); } } } /** * Add the content to a given message. * * @param \Illuminate\Mail\Message $message * @param string $view * @param string $plain * @param string $raw * @param array $data * @return void */ protected function addContent($message, $view, $plain, $raw, $data) { if (isset($view)) { $message->setBody($this->renderView($view, $data), 'text/html'); } if (isset($plain)) { $method = isset($view) ? 'addPart' : 'setBody'; $message->$method($this->renderView($plain, $data) ?: ' ', 'text/plain'); } if (isset($raw)) { $method = (isset($view) || isset($plain)) ? 'addPart' : 'setBody'; $message->$method($raw, 'text/plain'); } }
这里就是具体的实现接口的send方法,这有三个简单的参数,一个是自定义的邮件模板视图,一个是可以渲染试图的变量,再者就是一个回调;下面是我的示例:
public function send($toEmail, $subject = "test主题")
{
info('收件者的地址为:'.$toEmail);
Mail::send('mail', ['name'=>'XXX'], function ($message) use ($toEmail, $subject){
$to =$toEmail;
$message->to($to)->subject($subject);
});
}
这样我就完成了邮件发送,想必这里也是好奇,邮件发送是需要发件人和发件地址的,这里怎么实现呐,那么下一篇我就来分享一下怎么设置邮件客户端,点击查看:配置私人邮箱为发件客户端
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。