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。MailFacade 也會解析這個鍵。 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。設計上不要在註冊時就寄送,且在多個 Mailer 使用同一個 Transport 名稱時,不要讓設定或投遞目標混在一起。
在使用端應用程式中新增 Mailer
在config/mail.php 既有的 mailers 陣列中新增以下兩項。套件端不要覆寫使用者的預設值或整個 mailers。
MAIL_MAILER 中指定的是 receipts 這個 Mailer 名稱,而不是 Transport 名稱。若個別指定,就能在不變更既有預設值的情況下使用。
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 Facade 與 MailFake
- MailFake:Mailer 的選擇與 Mailable 的記錄
- Laravel 官方測試:以 Mailer 與 ArrayTransport 驗證寄送