expertcoder/swiftmailer-send-grid-bundle
Symfony bundle that adds a SendGrid transport for SwiftMailer, letting you send mail through SendGrid's PHP library. Configure your API key and optional categories, then set the swiftmailer transport to expertcoder_swift_mailer.send_grid. Works with Symfony Flex and older applications.
Recognize first: this is not a Laravel package. It is a Symfony bundle that ships a Swift_Transport implementation backed by the SendGrid PHP SDK (v3). Because Laravel's mailer relied on SwiftMailer itself through Laravel 8, the transport class is the only piece worth lifting. You will not composer require this and call it done — you will vendor the transport.
Minimal steps (Laravel ≤ 8, SwiftMailer still in composer.lock):
Confirm your stack actually has SwiftMailer:
composer show swiftmailer/swiftmailer
If it returns nothing (Laravel 9+), stop — you're on symfony/mailer and this transport will not plug in.
Add the SendGrid SDK directly (the bundle's dependency, not the bundle itself):
composer require sendgrid/sendgrid:^7.0
Copy the bundle's transport class into app/Mail/Transport/SendGridTransport.php (or vendor the package and reference the class). The class implements Swift_Transport, which is exactly the interface Laravel's Illuminate\Mail\TransportManager resolves.
Register a custom transport in AppServiceProvider::boot():
$this->app->extend('mailer', function ($mailer, $app) {
$transport = new SendGridTransport(config('mail.sendgrid.api_key'));
return new \Illuminate\Mail\Mailer(
$app['view'], $transport, $app['events']
);
});
Set the API key in config/mail.php / .env:
'sendgrid' => ['api_key' => env('SENDGRID_API_KEY')],
Where to look first: the Transport/ directory of the repo. SendGridTransport::send() is the integration point — read how it converts a Swift_Message into the SDK's Mail object before assuming anything else works.
First use case: a password-reset or receipt email. Send it through your existing Mail::to($user)->send(new ReceiptMail($order)) — if the transport binding is right, the call site should be unchanged. That unchanged call site is the entire value proposition.
Extract-don't-install. Because the bundle assumes Symfony bundle registration and a swiftmailer.transport alias, the cleanest Laravel integration is to fork the transport class into your app. This makes the EOL problem explicit and reviewable in code review, rather than a silent 2019 dependency.
Keep the Mailable API. All the Laravel patterns still hold — Mailable classes, ->to(), ->cc(), ->bcc(), ->attach(), ->with(). The transport is invisible to those call sites. Build the mail layer as you normally would and test with Mail::fake() for behavior; the transport itself needs an integration test.
Wire categories from config. The bundle reads categories from YAML; in Laravel put the same list in config/mail.php and pass it to the transport constructor. Use distinct category names per notification type (receipts, password-reset, digest) so SendGrid's dashboard analytics become useful — that is the main reason to use SendGrid over SMTP.
Custom headers per message. The transport maps Swift headers to the SendGrid payload (2.2.1 added custom-header support). Setting ->getHeaders()->addTextHeader('X-Entity-Ref-ID', $id) on the Swift message is a legitimate pattern for transaction deduplication — but verify the transport forwards non-reserved headers before you rely on it.
Personalization. The SDK supports a Personalization block with multiple recipient tos. The bundle respects an existing personalization from the message constructor (2.1.2), so if you batch-send you need to be aware of how it merges with the per-message recipients — check the constructor path before assuming batching works.
Test the transport, not the framework. Mail::fake() short-circuits the transport, so it does not prove the payload is valid. Write one integration test that constructs a Swift_Message, runs the transport with a mocked SendGrid client (or a stubbed send()), and asserts on the JSON body — especially multipart ordering and headers.
Pin and vendor. Add a comment above the vendored class with the upstream commit SHA and the SendGrid SDK version it was tested against. If you must composer require the bundle itself, pin the exact version (2.2.3).
Laravel 9+ is a hard stop. Laravel moved to symfony/mailer and Symfony\Component\Mailer\Transport\TransportInterface — a different abstraction. Swift_Transport implementations cannot be registered there. If you're on Laravel 10+ and reading this, your path is symfony/sendgrid-mailer (via the Symfony Mailer transport), or SendGrid's HTTP API directly, or SMTP with MAIL_MAILER=smtp.
SwiftMailer itself is EOL (November 2021). You are stacking an abandoned adapter on an abandoned framework component. Treat any use as a bridge with a decommission ticket, not a destination.
Multipart ordering is load-bearing. Per RFC 1341 §7.2, if both text/plain and text/html are present, text/plain must come first. The bundle documents this, but it's easy to violate via a view/text combo in a Mailable. If you see emails rendering as plaintext in clients that should render HTML, check the ordering first.
Reserved headers. Release 2.2.2 fixed case-insensitive reserved header handling. The SendGrid SDK rejects some header names (e.g. X-SMTPAPI variants, To, From) with an error. If you get a sendgrid exception that doesn't match a transport failure, inspect the headers you added — the failure is usually a header name collision, not auth.
No retry, backoff, rate-limit, or dead-letter handling. The transport translates and delegates; it does not queue with retry semantics. Use Laravel's queue with ->onQueue('mail') and retryUntil on the job, and add a failed-job handler that logs to a table or alerts. Do not assume SendGrid 429s are retried — they'll surface as an exception out of the queue job.
Errors are opaque. Failures bubble up as Swift_TransportException wrapping whatever the SDK threw. Log the underlying exception message and the request body (scrub the API key) — otherwise debugging a rejected send means guessing.
API key handling. Never let the key leak through a stack trace in the exception message. The SDK occasionally echoes request details; wrap send() in your own try/catch that logs a sanitized message.
Attachments and dynamic templates are partial. The adapter covers basic multipart; large attachments and SendGrid dynamic templates (template_id with substitutions) are not first-class. If you need dynamic templates, skip the transport and call the SDK's Mail + Mail::send() directly — you lose the Mailable abstraction but gain full template control.
Categories config is global by default. The bundle adds the configured category list to every message. To vary by notification you'll need to modify the transport to read categories off the message (e.g. via a custom header X-Category) and pass them through — not hard, but it is a code change, not a config change.
Fork ownership matters more than the code. With 0 dependents and a 2019 last release, nothing will break for you by surprise, but nothing will be fixed either. Assign an internal owner, add an integration test, and put a written migration note in the repo before the first send.
How can I help you explore Laravel packages today?