メインコンテンツへスキップ
パッケージから独自のメール送信方法を提供するには、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() は、カスタム生成処理を設定配列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が解決済みの場合も検証します。
このテストは登録・設定・同期送信・保持済みオブジェクトの境界を確認するものです。実際の配送用Transportでは、別途APIクライアントのモックやfakeでペイロードと失敗応答を検証します。キュー送信を提供する場合は、キューのテストも分け、ワーカー側でもプロバイダーが読み込まれることを確認してください。

参照した一次情報

最終更新日 2026年10月30日