(已解决) 怎么让 Laravel 5.3 支持多个 mail driver,并且能在运行中切换?
遇到个特殊的场景估计Laravel开发团队未曾想到,就是QQ邮箱会拒收mailgun, mandrill等国外邮件服务发出的邮件,而QQ邮箱的用户比重较大,这导致必须对QQ邮箱启用国内的如sendcloud, aliyun direct mail等邮件服务,这意味着需要在系统中同时提供国外、国外各一套邮件服务,在系统运行中,当遇到qq.com等国内邮箱时,使用国内邮件服务,其它的则使用国外的服务。现在Laravel 5.3只支持config中定义的一个mail driver,如何可以最优雅地提供两套服务,同时支持运行时切换呢?谢谢大侠!
本帖已被设为精华帖!
本帖由 Summer
于 8年前 加精
高认可度评论:
@kvz 嗯,我说的就是这个意思,从你代码来看方向已经对了。但是跟我说的「最优雅」方案还差一小丢丢,我下面详细列一下,以供参考。
Mailer
,Mailable
,MailableMailer
,Notification MailChannel
,每种方式都有其对应的send
和queue
方法。但是所有的send
和queue
都最终调用了Mailer::send
来执行发送。你可以跟踪下它们的源代码,最终就跑到同一个地方了。所以我们只需要重写Mailer::send
这一处就可以扩展(自定义)整个 Laravel Mailer 系统的发送功能了。Mail::sendSwiftMessage
这个方法,把$this->swift
这一处改成我们的,比如$this->getSwiftMailerForMessage($message)->send(....);
其他地方都不用动。getSwiftMailerForMessage
方法可以写到 Mailer 类中,就像你代码中那样。我在上面的回复里提到了『建议缓存 SwiftMailer』的意思是在自定义的 Mailer 中维护一个数组保存已经创建过的 SwiftMailer 实例。或者更好的方式是创建一个 SwiftMailerManager 类,逻辑分离。Laravel 中大量使用了这种 Manager 类,例如 AuthManager, FilesystemManager, 或者最终执行邮件发送的 TransportManager等,都是为了实现「多实例」场景。 你随便找一个系统的 Manager看看代码,实现一个 Manager 非常简单。我们的SwiftMailerManager
就是根据name和name 对应的配置文件来创建并缓存 SwiftMailer实例。$this->app->singleton('mailer', ...)
, 因为AppServiceProvider是在系统的MailServiceProvider之后(config/app.php 的 providers 数组中),所以 AppServiceProvider 会覆盖掉系统的 app('mailer') 注册行为,系统的 mailer 单例也不会生成的。建议使用我们的AppServiceProvider而不是继承 MailServiceProvider ,有一个好处是不用关心 MailServiceProvider 到底干了什么,或者哪天它把你重载的方法修改了或移除了,你都不用操心他。使用 AppServiceProvider 没有耦合。driver($name)
来获取需要的 transport。这部分功能就是在上面说的 SwiftMailerManager 里做的。这样下来的结果是 SwiftMailer 和 具体使用的 transport 都可以配置了,类似系统的其他管理类,比如 database, 支持多个connection 每个 connection 又可以灵活配置其 driver。所以我们的 SwiftMailerManager 同时支持多个邮件服务商,同一邮件服务商支持多个不同账号, 运行时也可以随意切换:根据收件人地址选择国内还是国外的服务商(transport driver) 或者同一服务商的不同账号(比如申请100个免费额度的账号:laughing: )。 当然,对于切换账号这种行为可能就不需要再新建 swift+transport了,替换现成的也可以,这就属于优化范畴了。protected $swiftMailers = [];
,然后在__construct
中加入默认项$this->swiftMailers['default'] = $this->swift;
上面提到的getSwiftMailerForMessage
方法判断 $message 逻辑后会从这个数组中获取 SwiftMailer 实例,如果不存在就创建并缓存到 $swiftMailers 数组中。app('swift.mailer') 和 app('swift.transport')
的 register 也重写掉,使用 SwiftMailerManager 的 defaultDriver 即可。getTo()
,在 Mailer 里处理这块东西,getTo()
是最恰当的方式。以上就是我上一个回复的具体实现。总结下就是:
$this->swift
部分,在 AppServiceProvider 中注册并替换app('mailer')
单例。这样扩展后,外部调用方没有学习和使用成本,系统的所有发送邮件方式都兼容。有一点迁移工作就是把以前的 mail.config 改成多 driver 的 mail.config 。
p.s. 这种架构在 Laravel 中大量存在,我以为只需要提醒下别人就知道怎么做了,所以之前没有详细说明。:smiling_imp:
可以从手动设置 mail config 入手
@Summer 谢谢你, 但是这样的方法对Mail::queue是否能起效?因为我看Mailer的queue方法,它是依赖注入了一个全局单例的mailer,那是不是意味着queue使用的mailer是系统启动时就创建的单例,后续修改了config,这个单例会被重建吗?
@kvz take a try
@Summer tried, 但是好像还是使用的config中设定的mail driver:
@kvz 大概原因是在 MailServiceProvider 中已经把 mailer 根据 config里面的设置成单例了,所以后面修改了 config 也不会更新 mailer 。所以有个很奇葩想法,在修改完config后 ,new MailServiceProvider,然后调用register方法重新绑定 mailer 单例。不知是否可行,可以试试。
额 不对,mailer 本来就是延迟加载的,可以通过设置config来修改,除非在这个之前已经调用过 mailer。
这是测试:
我在Mailer里面写了个测试接口,
第一次,没有修改config,在 tinker 中查看返回的 transport, 很明显的看到是 smtp 的
Swift_SmtpTransport
:第二次,先修改 config,再实例化后调用测试接口,可以看到已经是 MailgunTransport 了:
所以,如果你的应用第一次用 mailer 的话应该修改设置是有效的,因为 mailer 使用了延迟加载。
如果像类似队列之类的,如何修改呢?要不就是设置config之后再重新调用 Serviceprocider 的 register 方法,重新绑定一个单例,要不就重新设置过 SwiftMailer。 在Mailer 里面有个接口,可以动态设置 SwiftMailer:
测试如下:
@oustn 如果这样,那我觉得不如直接创建两个Mailer实例,根据需要使用对应driver的mailer,不过总之觉得不够优雅,我在尝试弄些自己的Mail代码
@oustn 我尝试自定义MailServiceProvider看看可以不
@kvz 如果实例化两个 mailer 不清楚是否会不会对性能什么的有影响,可以仿造 MailServiceProvider 自己定义两个不同的 mailer.
但是用下面这种办法挺简单的啊
@oustn 我有试验过这种办法,好像Mail::send($mail)可以成功,但是Mail::queue($mail)不起作用,我是参照了这里的办法:http://stackoverflow.com/questions/2654682...
这个跟我那个本质上是不一样的,用send方法意味着没有用队列,也就是一次请求就会调用一次,所以修改 config 有用,因为每次都是一个新的 mailer 。如果用队列的话,因为 Mailer 是单例,所以更改 config 应该是不会影响已经生成的 mailer。但是我的那个方法,是动态的设置 mailer 里面的 swiftmailer ,所以可以动态的修改,也就可以用在队列中。公交车上打的字,不知道有没有错。。。
@oustn 额,辛苦了,我试试你的办法
@kvz 没有辛苦,我其实很喜欢回答问题,很多东西之前也没有研究过,但是在看别人的问题的时候,就会想想自己会不会,不会的话刚好借着这个机会学习一下,毕竟自己关注点没那么多。一起讨论的话既能帮助到别人,也可以增长自己的知识,何乐不为?
@oustn 真厉害,试了你的办法,是我这两天尝试的各种办法中最简单、效果最理想的,就用你的办法,不再折腾了。谢谢~~
@oustn 感觉你对Laravel理解得很深,我是今天花了很多时间看了Laraval Mail部分代码,才基本了解它的原理。
@kvz 下班前刚好看到这个问题,想到自己也不会就研究了一会,大概看了下代码 很多细节方面的也没看......我觉得每个包的serviceprovider是比较重要的,从这个地方入手对这个原理会比较清晰。
@oustn 厉害,我是看得比较晕,看了半天才消化
@oustn 不好意思我犯了错误,您的方法仍然是不能起作用的。由于aliyun direct mail可以发送到QQ邮箱和Gmail邮箱,而mailgun只能发送到Gmail邮箱,导致我在试验时误判,实际上是,就算应用了你的那部分代码,Mail::queue仍然使用的是.env中配置的mail driver。
第一次看见「精华问题」,那不得不给个「精华答案」:laughing:
方案一:用国内的邮件服务发送所有邮件。除了价格,貌似国外的没什么优势。
方案二::tada: 替换系统的
Mailer
。重写其send
方法,根据$message
判断并创建合适的Swift_Mailer
实例然后调用这个Swift_Mailer
实例的send
方法。这种方案可以支持多个邮件服务商,对外没有影响。如果只是自动判断收件人地址,外部调用代码无需做任何修改。方案二无需修改现有的调用代码,学习和使用成本为0,兼容所有 Laravel 内置的邮件发送方式,支持 send 和 queue。建议缓存创建的
Swift_Mailer
实例。这就是你要的「最优雅」的方案。@kvz 好吧,这就尴尬了。
考虑的不是很周全,那个方式应该也是只适合一次性请求切换不同的驱动,对队列无解,下面是文档对于队列的说明。
一定要记得,队列处理器是长时间运行的进程,并在内存里保存着已经启动的应用状态。这样的结果就是,处理器运行后如果你修改代码那这些改变是不会应用到处理器中的。所以在你重新部署过程中,一定要 重启队列处理器。
也就是说,队列开始后你不管怎么修改,都没有办法变化。
在队列开始的时候已经注定了 Mailer 的驱动,按照这个说明,除非重启队列,否则无解。
不知道可不可以创建两个不同驱动的队列,然后在发送的时候推送到不同的队列中。
L5.3 可以考虑用
notification
,然后自己为不同的邮件邮件服务 自定义频道。然后在via
方法里面通过$notifiable
判断用户邮箱并指定邮件发送 频道 :eyes:mark。。
@kvz
这个问题真是精华问题 +1:
首先创建两个不同的 Job
SendLogEmail:
SendSmtpEmail:
控制器中:
运行队列
vagrant@homestead:~/Code/test$ php artisan queue:work
结果:
log 中:
mailtrap 发件箱中:
开始想着在 Job 的构造函数里面设置不同的,但是没有作用,队列在存储的时候会序列话,这个时候就算设置不同的驱动根本没有作用,毕竟反序列化的时候不会重新运行构造函数。所以把修改驱动的方法放在 handle 方法中,运行到这个job的时候会调用 handle 方法,这个时候设置驱动是可以的。
使用了 smtp 和 log 驱动,毕竟没有其他的邮箱驱动了, mailgun 一直用不了。
@oustn 赞一个,很有研究。
@overtrue 惭愧,关键@kvz 已经感谢我了,但是实际上却没帮上忙,很不好意思啊 哈哈
@oustn 这其实与国内环境有关,国外大部分邮件服务相比国内的复杂网络情况而言要稳定通畅好多
@overtrue 确实,基本上 google 都看不到相关问题,估计老外很少碰到会在队列中修改邮件驱动的情形。。。
不过也让我明白了一些队列方面的东西,一开始的时候以为只要运行了队列就没办法修改相关的状态,后来才弄明白,在 job 的 handle 方法里面是可以修改的。。
@oustn 序列化的时候 handle 肯定不会执行,所以 handle 里应该是可以改的
@oustn
@ElfSundae
你们的方法加深了我对这部分代码的理解,然后我撸出了一个补丁,安装后啥都不用做, 照常使用Laravel Mail就能实现自动根据目标邮箱切换driver的能力,只需要在配置文件中配置不同的driver的分工即可。
代码放在:https://github.com/kevinzheng/laravel-swit...
个人水平比较有限,欢迎大家指正和改进。
然后我讲下我的方法:
然后在Mailer中,覆写了send方法,在其中加入如下代码:
补充:还没有测试与Notifiable一起工作的情况
自定义Mailer的send方法完整代码,其中添加的三行代码的位置至关重要,否则无法获取到mailer的发送目标地址(或者谁有更好的办法?此处为获取to address大费周折才找到一个比较丑的办法):
@kvz 挺好,找到了合适自己的解决方法。
@kvz 嗯,我说的就是这个意思,从你代码来看方向已经对了。但是跟我说的「最优雅」方案还差一小丢丢,我下面详细列一下,以供参考。
Mailer
,Mailable
,MailableMailer
,Notification MailChannel
,每种方式都有其对应的send
和queue
方法。但是所有的send
和queue
都最终调用了Mailer::send
来执行发送。你可以跟踪下它们的源代码,最终就跑到同一个地方了。所以我们只需要重写Mailer::send
这一处就可以扩展(自定义)整个 Laravel Mailer 系统的发送功能了。Mail::sendSwiftMessage
这个方法,把$this->swift
这一处改成我们的,比如$this->getSwiftMailerForMessage($message)->send(....);
其他地方都不用动。getSwiftMailerForMessage
方法可以写到 Mailer 类中,就像你代码中那样。我在上面的回复里提到了『建议缓存 SwiftMailer』的意思是在自定义的 Mailer 中维护一个数组保存已经创建过的 SwiftMailer 实例。或者更好的方式是创建一个 SwiftMailerManager 类,逻辑分离。Laravel 中大量使用了这种 Manager 类,例如 AuthManager, FilesystemManager, 或者最终执行邮件发送的 TransportManager等,都是为了实现「多实例」场景。 你随便找一个系统的 Manager看看代码,实现一个 Manager 非常简单。我们的SwiftMailerManager
就是根据name和name 对应的配置文件来创建并缓存 SwiftMailer实例。$this->app->singleton('mailer', ...)
, 因为AppServiceProvider是在系统的MailServiceProvider之后(config/app.php 的 providers 数组中),所以 AppServiceProvider 会覆盖掉系统的 app('mailer') 注册行为,系统的 mailer 单例也不会生成的。建议使用我们的AppServiceProvider而不是继承 MailServiceProvider ,有一个好处是不用关心 MailServiceProvider 到底干了什么,或者哪天它把你重载的方法修改了或移除了,你都不用操心他。使用 AppServiceProvider 没有耦合。driver($name)
来获取需要的 transport。这部分功能就是在上面说的 SwiftMailerManager 里做的。这样下来的结果是 SwiftMailer 和 具体使用的 transport 都可以配置了,类似系统的其他管理类,比如 database, 支持多个connection 每个 connection 又可以灵活配置其 driver。所以我们的 SwiftMailerManager 同时支持多个邮件服务商,同一邮件服务商支持多个不同账号, 运行时也可以随意切换:根据收件人地址选择国内还是国外的服务商(transport driver) 或者同一服务商的不同账号(比如申请100个免费额度的账号:laughing: )。 当然,对于切换账号这种行为可能就不需要再新建 swift+transport了,替换现成的也可以,这就属于优化范畴了。protected $swiftMailers = [];
,然后在__construct
中加入默认项$this->swiftMailers['default'] = $this->swift;
上面提到的getSwiftMailerForMessage
方法判断 $message 逻辑后会从这个数组中获取 SwiftMailer 实例,如果不存在就创建并缓存到 $swiftMailers 数组中。app('swift.mailer') 和 app('swift.transport')
的 register 也重写掉,使用 SwiftMailerManager 的 defaultDriver 即可。getTo()
,在 Mailer 里处理这块东西,getTo()
是最恰当的方式。以上就是我上一个回复的具体实现。总结下就是:
$this->swift
部分,在 AppServiceProvider 中注册并替换app('mailer')
单例。这样扩展后,外部调用方没有学习和使用成本,系统的所有发送邮件方式都兼容。有一点迁移工作就是把以前的 mail.config 改成多 driver 的 mail.config 。
p.s. 这种架构在 Laravel 中大量存在,我以为只需要提醒下别人就知道怎么做了,所以之前没有详细说明。:smiling_imp:
@ElfSundae 分析得真透彻,让我大开眼界,稍后我再改进下代码。
@ElfSundae NB!
@ElfSundae Hi,我根据你的意思update了代码,麻烦帮看看怎么样:https://github.com/kevinzheng/laravel-swit...
但有俩问题,
一、“在 __construct 中加入默认项 $this->swiftMailers['default'] = $this->swift;”,这部分我不太理解它的必要性,我是直接在
SwiftMailerManager
中提供了一个getDefaultSwiftMailer
的方法,好像运行正常。二、如果我让
MailServiceProvider
直接继承自ServiceProvider
,然后在AppServiceProvider
中register singleton mailer时,好像没有覆盖掉Laravel Mail的mailer,只有继承才达到覆盖的效果。我是这样试验的:在
MailServiceProvider
中:在
AppServiceProvider
中:@kvz 嗯 我抽空看看
@ElfSundae NB!
本提问可以入选《本站最有价值 100 题》了~~
@Summer 使用了这种快速配置的方法,连续处理不同配置的邮件驱动时,偶尔会出现:Expected response code 250 but got code "440", with message "440 mail from account doesn't conform with authentication (Auth Account:mail01@test.com|Mail Account:mail02@test.com),
@Summer 这个可以用来配置相同驱动时,不同的发送邮箱。
www.jianshu.com/p/bc1df4b92d7e