(已解决) 怎么让 Laravel 5.3 支持多个 mail driver,并且能在运行中切换?

遇到个特殊的场景估计Laravel开发团队未曾想到,就是QQ邮箱会拒收mailgun, mandrill等国外邮件服务发出的邮件,而QQ邮箱的用户比重较大,这导致必须对QQ邮箱启用国内的如sendcloud, aliyun direct mail等邮件服务,这意味着需要在系统中同时提供国外、国外各一套邮件服务,在系统运行中,当遇到qq.com等国内邮箱时,使用国内邮件服务,其它的则使用国外的服务。现在Laravel 5.3只支持config中定义的一个mail driver,如何可以最优雅地提供两套服务,同时支持运行时切换呢?谢谢大侠!

本帖已被设为精华帖!
本帖由 Summer 于 7年前 加精
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 44

@kvz 嗯,我说的就是这个意思,从你代码来看方向已经对了。但是跟我说的「最优雅」方案还差一小丢丢,我下面详细列一下,以供参考。

  1. Laravel 内置的几种发送邮件的方法:Mailer, Mailable, MailableMailer, Notification MailChannel ,每种方式都有其对应的 sendqueue 方法。但是所有的 sendqueue 都最终调用了 Mailer::send 来执行发送。你可以跟踪下它们的源代码,最终就跑到同一个地方了。所以我们只需要重写 Mailer::send 这一处就可以扩展(自定义)整个 Laravel Mailer 系统的发送功能了。
  2. 具体点我们要重写的是 Mail::sendSwiftMessage 这个方法,把 $this->swift 这一处改成我们的,比如 $this->getSwiftMailerForMessage($message)->send(....); 其他地方都不用动。
  3. 我们新增加的这个 getSwiftMailerForMessage 方法可以写到 Mailer 类中,就像你代码中那样。我在上面的回复里提到了『建议缓存 SwiftMailer』的意思是在自定义的 Mailer 中维护一个数组保存已经创建过的 SwiftMailer 实例。或者更好的方式是创建一个 SwiftMailerManager 类,逻辑分离。Laravel 中大量使用了这种 Manager 类,例如 AuthManager, FilesystemManager, 或者最终执行邮件发送的 TransportManager等,都是为了实现「多实例」场景。 你随便找一个系统的 Manager看看代码,实现一个 Manager 非常简单。我们的 SwiftMailerManager 就是根据name和name 对应的配置文件来创建并缓存 SwiftMailer实例。
  4. 自定义 MailServiceProvider 可以继承它,或者使用我们的 AppServiceProvider,AppServiceProvider 的 register 中 $this->app->singleton('mailer', ...), 因为AppServiceProvider是在系统的MailServiceProvider之后(config/app.php 的 providers 数组中),所以 AppServiceProvider 会覆盖掉系统的 app('mailer') 注册行为,系统的 mailer 单例也不会生成的。建议使用我们的AppServiceProvider而不是继承 MailServiceProvider ,有一个好处是不用关心 MailServiceProvider 到底干了什么,或者哪天它把你重载的方法修改了或移除了,你都不用操心他。使用 AppServiceProvider 没有耦合。
  5. 一个 SwiftMailer 对应一个 transport,可以说他两之间是一对一的关系。所以扩展的话应该是创建多个 SwiftMailer ,跟其内部的 transport 没有关系。也不要调用 TransportManager 的 setDefaultDriver 来改变默认的 transport。合理的做法是创建 SwiftMailer 时, 根据 SwiftMailer 的配置找到需要的 transport driver,然后调用 TransportManager 的 driver($name) 来获取需要的 transport。这部分功能就是在上面说的 SwiftMailerManager 里做的。这样下来的结果是 SwiftMailer 和 具体使用的 transport 都可以配置了,类似系统的其他管理类,比如 database, 支持多个connection 每个 connection 又可以灵活配置其 driver。所以我们的 SwiftMailerManager 同时支持多个邮件服务商,同一邮件服务商支持多个不同账号, 运行时也可以随意切换:根据收件人地址选择国内还是国外的服务商(transport driver) 或者同一服务商的不同账号(比如申请100个免费额度的账号:laughing: )。 当然,对于切换账号这种行为可能就不需要再新建 swift+transport了,替换现成的也可以,这就属于优化范畴了。
  6. 缓存创建的 SwiftMailer 对象。尤其是对于队列,创建的 SwfitMailer 对象可能不止一次被使用到。 SwiftMailerManager 已经自带缓存。如果要在自定义的 Mailer 中管理 SwiftMailer 实例,定义一个数组即可,比如 protected $swiftMailers = [];,然后在 __construct 中加入默认项 $this->swiftMailers['default'] = $this->swift; 上面提到的 getSwiftMailerForMessage 方法判断 $message 逻辑后会从这个数组中获取 SwiftMailer 实例,如果不存在就创建并缓存到 $swiftMailers 数组中。
  7. 如果是使用 SwiftMailerManager. 可以把 app('swift.mailer') 和 app('swift.transport') 的 register 也重写掉,使用 SwiftMailerManager 的 defaultDriver 即可。
  8. Mailer 中要发送的 $message 包含了所有邮件数据,具体方法请查看 Swift_Message 。获取收件地址的方法是 getTo(),在 Mailer 里处理这块东西, getTo() 是最恰当的方式。
  9. 建议做成一个独立的扩展包,并将其开源 :)

以上就是我上一个回复的具体实现。总结下就是:

  • 创建 SwiftMailerManager ,其根据配置文件或 customCreator 创建、缓存、管理 SwiftMailer 实例。
  • 继承 Mailer 重写 $this->swift 部分,在 AppServiceProvider 中注册并替换 app('mailer') 单例。

这样扩展后,外部调用方没有学习和使用成本,系统的所有发送邮件方式都兼容。有一点迁移工作就是把以前的 mail.config 改成多 driver 的 mail.config 。

p.s. 这种架构在 Laravel 中大量存在,我以为只需要提醒下别人就知道怎么做了,所以之前没有详细说明。:smiling_imp:

7年前 评论
Summer

可以从手动设置 mail config 入手

   $mail=DB::table('mail_settings')->first();
  $config = array(
               'driver' => $mail->driver,
               'host' => $mail->host,
               'port' => $mail->port,
               'from' => array('address' => $mail->from_address, 'name' => $mail->from_name),
               'encryption' => $mail->encryption,
               'username' => $mail->username,
               'password' => $mail->password,
               'sendmail' => '/usr/sbin/sendmail -bs',
               'pretend' => false
           );
   Config::set('mail',$config);
7年前 评论
March 2年前

@Summer 谢谢你, 但是这样的方法对Mail::queue是否能起效?因为我看Mailer的queue方法,它是依赖注入了一个全局单例的mailer,那是不是意味着queue使用的mailer是系统启动时就创建的单例,后续修改了config,这个单例会被重建吗?

7年前 评论
Summer

@kvz take a try

7年前 评论

@Summer tried, 但是好像还是使用的config中设定的mail driver:

    public function queue($mail)
    {
        if ($this->useSencondaryMailDriver($mail->email)) {
            Config::set('mail', [
                'driver' => 'directmail',
            ]);
        } else {
            Config::set('mail', [
                'driver' => 'mailgun',
            ]);
        }
        Mail::queue($mail);
    }
7年前 评论

@kvz 大概原因是在 MailServiceProvider 中已经把 mailer 根据 config里面的设置成单例了,所以后面修改了 config 也不会更新 mailer 。所以有个很奇葩想法,在修改完config后 ,new MailServiceProvider,然后调用register方法重新绑定 mailer 单例。不知是否可行,可以试试。

7年前 评论

额 不对,mailer 本来就是延迟加载的,可以通过设置config来修改,除非在这个之前已经调用过 mailer。

这是测试:
我在Mailer里面写了个测试接口,

    public function getS()
    {
        dd($this->swift->getTransport());
    }

第一次,没有修改config,在 tinker 中查看返回的 transport, 很明显的看到是 smtp 的 Swift_SmtpTransport :

>>> $m = app('mailer')
=> Illuminate\Mail\Mailer {#1096}
>>> $m->getS()
Swift_SmtpTransport {#1101
  -_handlers: array:1 [
    "AUTH" => Swift_Transport_Esmtp_AuthHandler {#1105
      -_authenticators: array:5 [

      .......
}

第二次,先修改 config,再实例化后调用测试接口,可以看到已经是 MailgunTransport 了:

>>> Config::set('mail', [
            'driver' => 'mailgun',
         ]);
=> null
>>> $m = app('mailer')
=> Illuminate\Mail\Mailer {#1092}
>>> $m->getS()
Illuminate\Mail\Transport\MailgunTransport {#1101
  #client: GuzzleHttp\Client {#1102
    -config: array:8 [
      "connect_timeout" => 60
      "handler" => GuzzleHttp\HandlerStack {#1103
         ........
                }

所以,如果你的应用第一次用 mailer 的话应该修改设置是有效的,因为 mailer 使用了延迟加载。

如果像类似队列之类的,如何修改呢?要不就是设置config之后再重新调用 Serviceprocider 的 register 方法,重新绑定一个单例,要不就重新设置过 SwiftMailer。 在Mailer 里面有个接口,可以动态设置 SwiftMailer:

app('swift.transport')->setDefaultDriver('mailgun');           // 修改默认的 driver
Mail::setSwiftMailer(new Swift_Mailer(app('swift.transport')->driver()));    // 重新设置 SwiftMailer

测试如下:

// 直接查看
>>> $m = app('mailer')
=> Illuminate\Mail\Mailer {#1096}
>>> $m->getS()
Swift_SmtpTransport {#1101
......
}
// 修改driver
app('swift.transport')->setDefaultDriver('mailgun')
// 设置新的 SwiftMailer
$m->setSwiftMailer(new Swift_Mailer(app('swift.transport')->driver()))

// 查看
>>> $m->getS()
Illuminate\Mail\Transport\MailgunTransport {#1115
  #client: GuzzleHttp\Client {#1116
  ......
}
7年前 评论

@oustn 如果这样,那我觉得不如直接创建两个Mailer实例,根据需要使用对应driver的mailer,不过总之觉得不够优雅,我在尝试弄些自己的Mail代码

7年前 评论

@oustn 我尝试自定义MailServiceProvider看看可以不

7年前 评论

@kvz 如果实例化两个 mailer 不清楚是否会不会对性能什么的有影响,可以仿造 MailServiceProvider 自己定义两个不同的 mailer.

但是用下面这种办法挺简单的啊

app('swift.transport')->setDefaultDriver('mailgun');           // 修改默认的 driver
Mail::setSwiftMailer(new Swift_Mailer(app('swift.transport')->driver()));    // 重新设置 SwiftMailer

public function queue($mail)
    {
        if ($this->useSencondaryMailDriver($mail->email)) {
           app('swift.transport')->setDefaultDriver('directmail');  
        } else {
            app('swift.transport')->setDefaultDriver('mailgun');  
        }
        Mail::setSwiftMailer(new Swift_Mailer(app('swift.transport')->driver())); 
        Mail::queue($mail);
    }
7年前 评论

@oustn 我有试验过这种办法,好像Mail::send($mail)可以成功,但是Mail::queue($mail)不起作用,我是参照了这里的办法:http://stackoverflow.com/questions/2654682...

7年前 评论

这个跟我那个本质上是不一样的,用send方法意味着没有用队列,也就是一次请求就会调用一次,所以修改 config 有用,因为每次都是一个新的 mailer 。如果用队列的话,因为 Mailer 是单例,所以更改 config 应该是不会影响已经生成的 mailer。但是我的那个方法,是动态的设置 mailer 里面的 swiftmailer ,所以可以动态的修改,也就可以用在队列中。公交车上打的字,不知道有没有错。。。

7年前 评论

@oustn 额,辛苦了,我试试你的办法

7年前 评论

@kvz 没有辛苦,我其实很喜欢回答问题,很多东西之前也没有研究过,但是在看别人的问题的时候,就会想想自己会不会,不会的话刚好借着这个机会学习一下,毕竟自己关注点没那么多。一起讨论的话既能帮助到别人,也可以增长自己的知识,何乐不为?

7年前 评论

@oustn 真厉害,试了你的办法,是我这两天尝试的各种办法中最简单、效果最理想的,就用你的办法,不再折腾了。谢谢~~

7年前 评论

@oustn 感觉你对Laravel理解得很深,我是今天花了很多时间看了Laraval Mail部分代码,才基本了解它的原理。

7年前 评论

@kvz 下班前刚好看到这个问题,想到自己也不会就研究了一会,大概看了下代码 很多细节方面的也没看......我觉得每个包的serviceprovider是比较重要的,从这个地方入手对这个原理会比较清晰。

7年前 评论

@oustn 厉害,我是看得比较晕,看了半天才消化

7年前 评论

@oustn 不好意思我犯了错误,您的方法仍然是不能起作用的。由于aliyun direct mail可以发送到QQ邮箱和Gmail邮箱,而mailgun只能发送到Gmail邮箱,导致我在试验时误判,实际上是,就算应用了你的那部分代码,Mail::queue仍然使用的是.env中配置的mail driver。

7年前 评论

第一次看见「精华问题」,那不得不给个「精华答案」:laughing:

方案一:用国内的邮件服务发送所有邮件。除了价格,貌似国外的没什么优势。

方案二::tada: 替换系统的 Mailer 。重写其 send 方法,根据 $message 判断并创建合适的 Swift_Mailer 实例然后调用这个 Swift_Mailer 实例的 send 方法。这种方案可以支持多个邮件服务商,对外没有影响。如果只是自动判断收件人地址,外部调用代码无需做任何修改。

方案二无需修改现有的调用代码,学习和使用成本为0,兼容所有 Laravel 内置的邮件发送方式,支持 send 和 queue。建议缓存创建的 Swift_Mailer 实例。这就是你要的「最优雅」的方案。

7年前 评论

@kvz 好吧,这就尴尬了。

考虑的不是很周全,那个方式应该也是只适合一次性请求切换不同的驱动,对队列无解,下面是文档对于队列的说明。

一定要记得,队列处理器是长时间运行的进程,并在内存里保存着已经启动的应用状态。这样的结果就是,处理器运行后如果你修改代码那这些改变是不会应用到处理器中的。所以在你重新部署过程中,一定要 重启队列处理器。

也就是说,队列开始后你不管怎么修改,都没有办法变化。

在队列开始的时候已经注定了 Mailer 的驱动,按照这个说明,除非重启队列,否则无解。

不知道可不可以创建两个不同驱动的队列,然后在发送的时候推送到不同的队列中。

7年前 评论

L5.3 可以考虑用 notification,然后自己为不同的邮件邮件服务 自定义频道。然后在 via 方法里面通过 $notifiable 判断用户邮箱并指定邮件发送 频道 :eyes:

7年前 评论

@kvz

这个问题真是精华问题 +1:

首先创建两个不同的 Job

php artisan make:job SendLogEmail
php artisan make:job SendSmtpEmail

SendLogEmail:

class SendLogEmail implements ShouldQueue
{
    use InteractsWithQueue, Queueable, SerializesModels;
    /**
     * @var
     */
    private $email;
    private $mailer;

    /**
     * Create a new job instance.
     *
     * @return void
     */
    public function __construct($email)
    {
        $this->email = $email;
    }

    /**
     * Execute the job.
     *
     * @return void
     */
    public function handle()
    {
        app('swift.transport')->setDefaultDriver('log');
        Mail::setSwiftMailer(new \Swift_Mailer(app('swift.transport')->driver()));
        Mail::to($this->email)->send(new TestMail());
    }
}

SendSmtpEmail:

class SendSmtpEmail implements ShouldQueue
{
    use InteractsWithQueue, Queueable, SerializesModels;

    protected $email;
    /**
     * Create a new job instance.
     *
     * @return void
     */
    public function __construct($email)
    {
        $this->email = $email;
    }

    /**
     * Execute the job.
     *
     * @return void
     */
    public function handle()
    {
        app('swift.transport')->setDefaultDriver('smtp');
        Mail::setSwiftMailer(new \Swift_Mailer(app('swift.transport')->driver()));
        Mail::to($this->email)->send(new TestMail());
    }
}

控制器中:

    protected $mails = [
        '123@163.com',
        '456@126.com',
        '789@qq.com',
        '123@test.com',
        '456@test.com',
        '789@test.com'
    ];

    public function send()
    {
        foreach ($this->mails as $mail) {
            if ($mail == '123@test.com' || $mail == '456@test.com' || $mail == '789@test.com') {
                $job = (new SendLogEmail($mail));
            } else {
                $job = (new SendSmtpEmail($mail));
            }
            dispatch($job);
        }
    }

运行队列 vagrant@homestead:~/Code/test$ php artisan queue:work

file

结果:

log 中:

[2016-11-23 03:39:30] local.DEBUG: Message-ID: <33feda5d0f5cb1df11388f97c14559ed@swift.generated>
Date: Wed, 23 Nov 2016 03:39:30 +0000
Subject: Test Mail
From: Example <hello@example.com>
To: 123@test.com
MIME-Version: 1.0
Content-Type: text/html; charset=utf-8
Content-Transfer-Encoding: quoted-printable

<h1>Test mail</h1>  
[2016-11-23 03:39:30] local.DEBUG: Message-ID: <965effb0fe67dae95e5784a1593a43d3@swift.generated>
Date: Wed, 23 Nov 2016 03:39:30 +0000
Subject: Test Mail
From: Example <hello@example.com>
To: 456@test.com
MIME-Version: 1.0
Content-Type: text/html; charset=utf-8
Content-Transfer-Encoding: quoted-printable

<h1>Test mail</h1>  
[2016-11-23 03:39:30] local.DEBUG: Message-ID: <66b73e1c2329270629e79c99858405bc@swift.generated>
Date: Wed, 23 Nov 2016 03:39:30 +0000
Subject: Test Mail
From: Example <hello@example.com>
To: 789@test.com
MIME-Version: 1.0
Content-Type: text/html; charset=utf-8
Content-Transfer-Encoding: quoted-printable

<h1>Test mail</h1>  

mailtrap 发件箱中:

file

开始想着在 Job 的构造函数里面设置不同的,但是没有作用,队列在存储的时候会序列话,这个时候就算设置不同的驱动根本没有作用,毕竟反序列化的时候不会重新运行构造函数。所以把修改驱动的方法放在 handle 方法中,运行到这个job的时候会调用 handle 方法,这个时候设置驱动是可以的。

使用了 smtp 和 log 驱动,毕竟没有其他的邮箱驱动了, mailgun 一直用不了。

7年前 评论

@oustn 赞一个,很有研究。

7年前 评论

@overtrue 惭愧,关键@kvz 已经感谢我了,但是实际上却没帮上忙,很不好意思啊 哈哈

7年前 评论

@oustn 这其实与国内环境有关,国外大部分邮件服务相比国内的复杂网络情况而言要稳定通畅好多

7年前 评论

@overtrue 确实,基本上 google 都看不到相关问题,估计老外很少碰到会在队列中修改邮件驱动的情形。。。

不过也让我明白了一些队列方面的东西,一开始的时候以为只要运行了队列就没办法修改相关的状态,后来才弄明白,在 job 的 handle 方法里面是可以修改的。。

7年前 评论

@oustn 序列化的时候 handle 肯定不会执行,所以 handle 里应该是可以改的

7年前 评论

@oustn
@ElfSundae
你们的方法加深了我对这部分代码的理解,然后我撸出了一个补丁,安装后啥都不用做, 照常使用Laravel Mail就能实现自动根据目标邮箱切换driver的能力,只需要在配置文件中配置不同的driver的分工即可。
代码放在:https://github.com/kevinzheng/laravel-swit...

个人水平比较有限,欢迎大家指正和改进。
然后我讲下我的方法:

  1. 我是自定义了一个Mailer和MailServiceProvider,分别继承自\Illuminate\Mail\Mailer和\Illuminate\Mail\MailServiceProvider
  2. 然后在Mailer中,覆写了send方法,在其中加入如下代码:

    // $to variable of $message is an array, this implementation is rough and temporary.
    $to = $message->getTo();
    $address = array_keys($to)[0];
    $this->determineTransport($address);
    /**
     * @param $address
     */
    public function determineTransport($address)
    {
        $mailDriver = $this->determineMailDriver($address);
        app('swift.transport')->setDefaultDriver($mailDriver);
        $this->setSwiftMailer(new Swift_Mailer(app('swift.transport')->driver()));
    }
    
    /**
     * @param $address
     * @return string
     */
    public function determineMailDriver($address)
    {
        $divisionMap = config('switchable-mail.drivers_division');
        if (is_array($divisionMap) && isset($divisionMap)) {
            $mailServiceDomain = $this->getMailServiceFromAddress($address);
            foreach ($divisionMap as $key => $value) {
                if (in_array($mailServiceDomain, $value)) {
                    return $key;
                }
            }
        }
        $defaultDriver = config('switchable-mail.default_driver');
        return $defaultDriver ? $defaultDriver : config('mail.driver');
    }
    
    /**
     * @param $address
     * @return mixed
     */
    public function getMailServiceFromAddress($address)
    {
        return explode('@', $address)[1];
    }
  3. 在MailServiceProvider中覆写了register方法,其实此处代码完全拷贝自\Illuminate\Mail\MailServiceProvider未做更改,还需要自定义是因为
        $this->app->singleton('mailer', function ($app) {
            $mailer = new Mailer(  // 确保此处的Mailer是我们自定义的Mailer,仅此而已
                $app['view'], $app['swift.mailer'], $app['events']
            );
            .......
            .......
            return $mailer;
        });
  4. 然后在app.php中加入:
    KVZ\Laravel\SwitchableMail\MailServiceProvider::class

补充:还没有测试与Notifiable一起工作的情况

7年前 评论

自定义Mailer的send方法完整代码,其中添加的三行代码的位置至关重要,否则无法获取到mailer的发送目标地址(或者谁有更好的办法?此处为获取to address大费周折才找到一个比较丑的办法):

    public function send($view, array $data = [], $callback = null)
    {
        if ($view instanceof MailableContract) {
            return $view->send($this);
        }

        // 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.
        $this->addContent($message, $view, $plain, $raw, $data);

        $this->callMessageBuilder($callback, $message);

        if (isset($this->to['address'])) {
            $message->to($this->to['address'], $this->to['name'], true);
        }

        $message = $message->getSwiftMessage();

        /*************************/
        // $to variable of $message is an array, this implementation is rough and temporary.
        $to = $message->getTo();
        $address = array_keys($to)[0];
        $this->determineTransport($address);
        /*************************/

        $this->sendSwiftMessage($message);
    }
7年前 评论

@kvz 挺好,找到了合适自己的解决方法。

7年前 评论

@kvz 嗯,我说的就是这个意思,从你代码来看方向已经对了。但是跟我说的「最优雅」方案还差一小丢丢,我下面详细列一下,以供参考。

  1. Laravel 内置的几种发送邮件的方法:Mailer, Mailable, MailableMailer, Notification MailChannel ,每种方式都有其对应的 sendqueue 方法。但是所有的 sendqueue 都最终调用了 Mailer::send 来执行发送。你可以跟踪下它们的源代码,最终就跑到同一个地方了。所以我们只需要重写 Mailer::send 这一处就可以扩展(自定义)整个 Laravel Mailer 系统的发送功能了。
  2. 具体点我们要重写的是 Mail::sendSwiftMessage 这个方法,把 $this->swift 这一处改成我们的,比如 $this->getSwiftMailerForMessage($message)->send(....); 其他地方都不用动。
  3. 我们新增加的这个 getSwiftMailerForMessage 方法可以写到 Mailer 类中,就像你代码中那样。我在上面的回复里提到了『建议缓存 SwiftMailer』的意思是在自定义的 Mailer 中维护一个数组保存已经创建过的 SwiftMailer 实例。或者更好的方式是创建一个 SwiftMailerManager 类,逻辑分离。Laravel 中大量使用了这种 Manager 类,例如 AuthManager, FilesystemManager, 或者最终执行邮件发送的 TransportManager等,都是为了实现「多实例」场景。 你随便找一个系统的 Manager看看代码,实现一个 Manager 非常简单。我们的 SwiftMailerManager 就是根据name和name 对应的配置文件来创建并缓存 SwiftMailer实例。
  4. 自定义 MailServiceProvider 可以继承它,或者使用我们的 AppServiceProvider,AppServiceProvider 的 register 中 $this->app->singleton('mailer', ...), 因为AppServiceProvider是在系统的MailServiceProvider之后(config/app.php 的 providers 数组中),所以 AppServiceProvider 会覆盖掉系统的 app('mailer') 注册行为,系统的 mailer 单例也不会生成的。建议使用我们的AppServiceProvider而不是继承 MailServiceProvider ,有一个好处是不用关心 MailServiceProvider 到底干了什么,或者哪天它把你重载的方法修改了或移除了,你都不用操心他。使用 AppServiceProvider 没有耦合。
  5. 一个 SwiftMailer 对应一个 transport,可以说他两之间是一对一的关系。所以扩展的话应该是创建多个 SwiftMailer ,跟其内部的 transport 没有关系。也不要调用 TransportManager 的 setDefaultDriver 来改变默认的 transport。合理的做法是创建 SwiftMailer 时, 根据 SwiftMailer 的配置找到需要的 transport driver,然后调用 TransportManager 的 driver($name) 来获取需要的 transport。这部分功能就是在上面说的 SwiftMailerManager 里做的。这样下来的结果是 SwiftMailer 和 具体使用的 transport 都可以配置了,类似系统的其他管理类,比如 database, 支持多个connection 每个 connection 又可以灵活配置其 driver。所以我们的 SwiftMailerManager 同时支持多个邮件服务商,同一邮件服务商支持多个不同账号, 运行时也可以随意切换:根据收件人地址选择国内还是国外的服务商(transport driver) 或者同一服务商的不同账号(比如申请100个免费额度的账号:laughing: )。 当然,对于切换账号这种行为可能就不需要再新建 swift+transport了,替换现成的也可以,这就属于优化范畴了。
  6. 缓存创建的 SwiftMailer 对象。尤其是对于队列,创建的 SwfitMailer 对象可能不止一次被使用到。 SwiftMailerManager 已经自带缓存。如果要在自定义的 Mailer 中管理 SwiftMailer 实例,定义一个数组即可,比如 protected $swiftMailers = [];,然后在 __construct 中加入默认项 $this->swiftMailers['default'] = $this->swift; 上面提到的 getSwiftMailerForMessage 方法判断 $message 逻辑后会从这个数组中获取 SwiftMailer 实例,如果不存在就创建并缓存到 $swiftMailers 数组中。
  7. 如果是使用 SwiftMailerManager. 可以把 app('swift.mailer') 和 app('swift.transport') 的 register 也重写掉,使用 SwiftMailerManager 的 defaultDriver 即可。
  8. Mailer 中要发送的 $message 包含了所有邮件数据,具体方法请查看 Swift_Message 。获取收件地址的方法是 getTo(),在 Mailer 里处理这块东西, getTo() 是最恰当的方式。
  9. 建议做成一个独立的扩展包,并将其开源 :)

以上就是我上一个回复的具体实现。总结下就是:

  • 创建 SwiftMailerManager ,其根据配置文件或 customCreator 创建、缓存、管理 SwiftMailer 实例。
  • 继承 Mailer 重写 $this->swift 部分,在 AppServiceProvider 中注册并替换 app('mailer') 单例。

这样扩展后,外部调用方没有学习和使用成本,系统的所有发送邮件方式都兼容。有一点迁移工作就是把以前的 mail.config 改成多 driver 的 mail.config 。

p.s. 这种架构在 Laravel 中大量存在,我以为只需要提醒下别人就知道怎么做了,所以之前没有详细说明。:smiling_imp:

7年前 评论

@ElfSundae 分析得真透彻,让我大开眼界,稍后我再改进下代码。

7年前 评论

@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,只有继承才达到覆盖的效果。我是这样试验的:

  1. MailServiceProvider中:

    public function register()
    {
        $this->registerSwiftTransport();
        $this->registerSwiftMailerManager();
        $this->registerSwiftMailer();
        $this->registerMailer();
    }
    
    public function registerMailer()
    {
        $this->app->singleton('mailer', function ($app) {
            $mailer = new Mailer(
                $app['view'], $app['swift.mailer'], $app['events']
            );
            ..............................
            return $mailer;
        });
    }
  2. AppServiceProvider中:

    public function register()
    {
        if ($this->app->environment() == 'local') {
            $this->app->register(\Laracasts\Generators\GeneratorsServiceProvider::class);
        }
    
        $this->app->register(\KVZ\Laravel\SwitchableMail\MailServiceProvider::class);
    }
7年前 评论

@kvz 嗯 我抽空看看

7年前 评论

本提问可以入选《本站最有价值 100 题》了~~

6年前 评论

@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),

5年前 评论

@Summer 这个可以用来配置相同驱动时,不同的发送邮箱。

5年前 评论
// 备份原有Mailer
$backup = Mail::getSwiftMailer();

// 设置邮箱账号
$transport = \Swift_SmtpTransport::newInstance('smtp.exmail.qq.com', 465, 'ssl');

$transport->setUsername('xxx@xxx.xxx');
$transport->setPassword('xxxxxx');

$mailer = new \Swift_Mailer($transport);

Mail::setSwiftMailer($mailer);

Mail::send('mail.template', array $data, function ($message) {
    $message->from('xxx@xxx.xxx', 'xxxxxx');
    $message->subject('邮件主题');
    $message->to('receiver@xxx.xxx');
});

// 发送后还原
Mail::setSwiftMailer($backup);
3年前 评论
huxiaochu 1年前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!