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() は、カスタム生成処理を設定配列1つだけで呼びます。Cache::extend() のようなアプリケーションと設定の2引数ではありません。戻り値は TransportInterface を実装したTransportであり、LaravelのMailerや送信結果ではありません。
また、MailManager::extend() はクロージャを保持するだけで、Managerへの再バインドは行いません。例ではstaticクロージャと明示的な引数を使い、$this の解釈に依存しないようにしています。
外部API用のクライアントが必要なら、この生成処理で設定を適用してTransportへ渡します。登録時点で送信を行わず、同じTransport名の複数Mailerがある場合に設定や宛先を混在させない設計にします。
利用アプリケーションにMailerを追加する
config/mail.php の既存の mailers 配列へ、次の2つを追加します。パッケージ側から利用者のデフォルトや 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とそのTransportをMailer名ごとに保持します。設定変更や 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の参照まで置き換えることはできません。
本番では設定キャッシュの更新と長寿命ワーカーの再起動を通常のデプロイ手順へ組み込みます。再解決を全プロセスの一括更新と考えず、サービスのライフサイクルも確認してください。
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による送信の検証