sethsandaru/laravel-mail-switcher
Requirements for this release: PHP 8.1 and Laravel 9.x. If you are still on Laravel 8 / PHP 8.0, stay on the previous minor line — 1.1.0 is the release that formally moves the compatibility floor.
composer require <vendor>/<package>:^1.1
php artisan vendor:publish --provider="<Vendor>\<Package>\<Package>ServiceProvider" --tag=config
Mailer/Mailable pipeline (Symfony Mailer under the hood). Any code that assumed a Swift_Message instance needs attention before you rely on it.Where to look first: the service provider (to see what it binds into the container, especially anything registered as a mail transport), the published config (mailer name, queue settings, sender defaults), and the package's Mailable/Notification classes if it ships any. Those three places tell you whether the "new Email way" is transparent to you or whether you were reaching into package internals.
First use case: send one of the package's messages end to end and assert it in a test rather than eyeballing an inbox.
Mail::fake();
// trigger the package's send path
$this->post('/invite', ['email' => '[email protected]'])->assertOk();
Mail::assertSent(InviteMail::class, fn ($mail) => $mail->hasTo('[email protected]'));
Getting this green on Laravel 9 is the fastest signal that the upgrade is clean for your usage.
Send through the package's own messages, not hand-rolled ones. Where the package provides a Mailable or a notification channel, use it and pass your model in the constructor. This keeps you insulated from the mail internals that just changed in 1.1.0.
Queue by default. Set the queue flag in config (or ->onQueue() on the mailable) so the send happens in a worker, not in the request cycle:
InviteMail::to($user)->onQueue('mail')->queue();
If the message depends on a record that is created in the same transaction, use afterCommit() (or after_commit = true in config/queue.php) — otherwise a worker can pick up the job before the row is visible.
Notifications when the message is one of several channels. If the package ships a notification channel, prefer Notification::send() and let the channel resolve addressing; you get multi-channel fan-out and per-user preferences for free.
Testing patterns that survive the mailer swap:
Mail::fake() + assertSent / assertQueued with a closure asserting recipients, subject, and any view data.assertNothingSent() in negative-path tests.Queue::fake() and assert the job was pushed; then test the mailable in isolation.Customize through published views and config, not by subclassing internals. Override the view, set the sender/from address in config, and point the package at a named mailer in config/mail.php if you need a different transport (e.g. a transactional provider) than your app default.
Local development: MAIL_MAILER=log or Mailpit for a real SMTP surface. This matters more now — the failure modes of the new mailer (transport exceptions, envelope handling) look different from the SwiftMailer era, and reading the log or Mailpit UI is the quickest way to distinguish "the package didn't send" from "the transport rejected it".
SwiftMailer is gone. This is the headline risk of 1.1.0. Any custom transport, plugin, or event listener you wrote against Swift_Transport / Swift_Message will break or silently no-op on Laravel 9. Custom transports must now implement Symfony\Component\Mailer\Transport\TransportInterface and be registered via Mail::extend() or the mail.mailers config. If you had a Mail::macro or a listener on MessageSending that mutated the Swift message, port it to the Symfony Email/SentMessage objects.
Check the symfony/mailer constraint. Composer may resolve a version of the package that fights your framework's pinned Symfony Mailer. Run composer why symfony/mailer after upgrading and resolve conflicts deliberately rather than with --with-all-dependencies and hope.
Failover/roundrobin mailers changed semantics in Laravel 9. If you configured a failover mailer and the package sends through a named mailer, verify the ordering and that a failing transport falls through as you expect.
Deprecations on PHP 8.1 are warnings today, errors tomorrow. Look for "Return type should either be compatible with… or #[\ReturnTypeWillChange]" notices coming from the package in your logs — typically ArrayAccess, IteratorAggregate, or JsonSerializable implementations. They do not break the upgrade but they clutter logs and will bite on PHP 8.2/8.3.
Serialization of queued mailables. A queued mailable holds your model. If the model is large or contains non-serializable state (closures, connections), the worker will fail after the request succeeded. Use SerializesModels and prefer passing an ID where practical.
Mailable/Notification naming churn. "Applied new Email way of Laravel" suggests internal class renames. If you extended package mailables or referenced them by FQCN in config, Mail::fake() assertions, or assertSent(Mail::class), grep for the old names before deploying — a silently unasserted test is worse than a failing one.
Debugging checklist when mail "doesn't arrive":
MAIL_MAILER and the mailer name the package is configured to use.storage/logs/laravel.log for transport exceptions (the Symfony mailer throws loudly on envelope/recipient problems).Mail::fake() in a focused test to confirm the package reaches the send call at all — this separates "package bug" from "transport/config bug" in one step.Extension points to prefer: published config, published views, container bindings for the package's interfaces, and Laravel's own mail events (MessageSending, MessageSent). Avoid subclassing package classes — 1.1.0 just demonstrated how quickly those move.
How can I help you explore Laravel packages today?