v13.39.0 实现的确认。基本的发送方式请参阅发送邮件,服务提供者的准备请参阅包开发。
区分 Mailer 名称与 Transport 名称
Mail::mailer('receipts') 会读取 mail.mailers.receipts,并调用与 transport 对应的创建处理。MailManager::build() 会将该 Transport 传给 Laravel 的 Mailer,在常规的按名称解析中还会设置全局发件人等。
自定义创建处理会优先于内置 Transport 被选中。不要覆盖 smtp 或 ses,而应使用包专属的 Transport 名称。
在不对外发送的情况下构建注册路径
首先确认的不是投递目标的 API,而是配置与解析路径。在Acme\Postbox 包的 src/Transport/MemoryTransport.php 中,创建一个继承标准 ArrayTransport 的类。
ArrayTransport::send() 会将 Symfony 的 SentMessage 保存到内部的 Collection 中。可以通过 messages() 查看,并通过 flush() 清除该实例的记录。标签用于确认配置已传递到创建处理。
注册到延迟加载的 Manager
官方文档在boot() 中调用 Mail::extend()。在包中,为了避免在不使用邮件的应用中多余地创建 Manager,可以结合使用服务解析后钩子。
创建 src/PostboxServiceProvider.php。
- Laravel 的
MailServiceProvider是延迟提供者,会将mail.manager注册为 singleton。Mail门面解析的也是这个键。 callAfterResolving()在尚未解析时会等待之后的解析,对已解析的 Manager 则会立即应用扩展。它并不会替换已创建的 Mailer。- 避免将注册扩展的自己的服务提供者也简单地设为
DeferrableProvider。要确保在钩子注册之前不会请求自定义 Transport。 - 本例在自己的
boot()中注册钩子。不保证其他服务提供者在此之前使用自定义 Mailer 的启动顺序。请设计好注册顺序,不要在register()中发送邮件。
不要将 extend 的参数与其他 Manager 混淆
MailManager::createSymfonyTransport() 调用自定义创建处理时只传入一个配置数组。这与 Cache::extend() 那样传入应用和配置两个参数不同。返回值是实现了 TransportInterface 的 Transport,而不是 Laravel 的 Mailer 或发送结果。
此外,MailManager::extend() 只是保存闭包,不会重新绑定到 Manager。示例中使用 static 闭包和显式参数,以免依赖 $this 的解释。
如果需要外部 API 的客户端,应在该创建处理中应用配置并传给 Transport。注册时不要执行发送,并在设计上确保使用同一 Transport 名称的多个 Mailer 之间不会混用配置或目标地址。
在使用方应用中添加 Mailer
在config/mail.php 现有的 mailers 数组中添加以下两项。不要从包一侧覆盖使用者的默认值或整个 mailers。
MAIL_MAILER 中指定的不是 Transport 名称,而是 receipts 这个 Mailer 名称。如果单独指定,则无需更改现有默认值即可使用。
raw() 是一个无需准备视图、用于确认同步发送路径的示例。使用 Mailable 的常规发送也一样,Mailer 生成正文等内容后会传给同一个 Transport。
创建实际投递 Transport 时的边界
在投递到外部服务的实现中,可以按照官方文档继承Symfony\Component\Mailer\Transport\AbstractTransport,并实现 doSend(SentMessage $message): void 和 __toString(): string。上述注册方式可以直接沿用。
Laravel 的
Mailer::sendSymfonyMessage() 会调用 Transport 的 send()。如果 MessageSending 监听器中止了发送,或 Transport 返回了 null,Laravel 的 send() 也不会返回发送结果。不要仅凭没有异常就判定投递成功。 即使返回了 SentMessage,也不能保证邮件已到达收件人的邮箱。
区分配置变更与 Mailer 的重新解析
MailManager 会按 Mailer 名称保留 Mailer 及其 Transport。仅变更配置或重新注册 extend(),并不会更新已创建的对象。
$same 与 $before 是同一个 Mailer。在 purge('receipts') 之后获取的 $after 中,会根据新的配置创建 Transport。
经确认,
purge() 的实现只是从 Manager 内部数组中移除引用。旧的 $before 以及已注入该 Mailer 的服务不会被替换。它也不是显式关闭外部连接的处理。
failover 等父 Mailer 也会被单独保留
failover 和 roundrobin 会读取子 Mailer 名称的配置,直接创建 Transport 并将其嵌入父 Transport 中。这并不是共享子 Mail::mailer() 实例的机制。
因此,即使变更子配置并调用 purge('receipts'),已解析的父 Mailer 的内部 Transport 也不会更新。需要更新的父 Mailer 也必须重新解析。即使使用 forgetMailers(),也无法替换使用方已持有的父 Mailer 引用。
在生产环境中,应将配置缓存的更新与长驻 worker 的重启纳入常规部署流程。不要将重新解析视为对所有进程的统一更新,还请参阅服务的生命周期。
不仅使用 Mail::fake,还要测试实际的创建路径
Mail::fake() 用于记录 Mailable 的发送与入队。MailFake::mailer() 不会解析实际的 Mailer,而是记录 Mailer 名称并返回 fake 自身。raw() 也不会执行发送处理。即使没有注册自定义 Transport,Mailable 的发送断言也可能成功。
在包的测试环境中,按如下方式不使用 fake,让流程经过实际的 Manager 和 Transport。本例的 Transport 不会对外发送,因此可以在没有实际投递风险的情况下确认注册路径。先加载辅助服务提供者,以便同时验证 Manager 已被解析的情况。
参考的一手资料
- Laravel 官方文档:邮件的自定义 Transport 与测试(确认时的版本)
- Laravel 官方文档:包开发
- MailManager:创建处理、Mailer 的保留、purge 与复合 Transport
- MailServiceProvider:MailManager 的绑定
- ServiceProvider:callAfterResolving
- Mailer:向 Transport 发送与事件
- ArrayTransport:消息的记录
- Mail 门面与 MailFake
- MailFake:Mailer 的选择与 Mailable 的记录
- Laravel 官方测试:使用 Mailer 与 ArrayTransport 验证发送