跳转到主要内容
要从包中提供自己的邮件发送方式,只需将创建 Symfony Transport 的处理注册到 Laravel。无需重新实现 Mailable 或 Laravel 的 Mailer。 本页面基于对 Laravel Framework 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() 清除该实例的记录。标签用于确认配置已传递到创建处理。
本例不会将邮件投递到外部,仅用于学习注册与测试。内存中的记录不会跨进程共享或持久化,也不能用来确认邮件已送达收件人。如果目的只是记录到内存,内置的 array Transport 就足够了。

注册到延迟加载的 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 已被解析的情况。
该测试用于确认注册、配置、同步发送以及已保留对象的边界。对于实际投递用的 Transport,需要另外使用 API 客户端的 mock 或 fake 来验证载荷与失败响应。如果提供队列发送,请将队列的测试也分开,并确认 worker 端同样会加载服务提供者。

参考的一手资料

最后修改于 2026年10月30日