gulaandrij/swiftmailer-send-grid-bundle
A Symfony bundle that integrates SendGrid with Swift Mailer by providing a dedicated transport, letting applications send transactional email through SendGrid's API with minimal configuration and familiar Swift Mailer usage.
Reality check before you install anything. This bundle is a 2018-era Symfony bridge for Swiftmailer's SendGrid transport. Swiftmailer reached EOL in November 2021 and the Symfony SwiftmailerBundle was dropped in Symfony 6, so this is only viable on Symfony 2.x–5.x with swiftmailer/swiftmailer 6.x and PHP 7.x. Before adding it, check whether the swiftmailer config in your swiftmailer-bundle version already accepts transport: sendgrid with an api_key — if it does, this bundle is redundant.
Minimal steps
composer.json: PHP version, symfony/framework-bundle, swiftmailer/swiftmailer, and sendgrid/sendgrid. The SendGrid PHP SDK had two incompatible generations (v5 SendGrid\Mail / v6+ SendGrid\Mail\Mail, SendGrid\Mail\To, etc.), and the bundle was written against one of them — pin whichever version it expects.composer require gulaandrij/swiftmailer-send-grid-bundle sendgrid/sendgrid
Gulaandrij\SwiftmailerSendGridBundle\SwiftmailerSendGridBundle):
// app/AppKernel.php (Symfony <4) or config/bundles.php
new Gulaandrij\SwiftmailerSendGridBundle\SwiftmailerSendGridBundle(),
# config/packages/swiftmailer_send_grid.yaml
swiftmailer_send_grid:
api_key: '%env(SENDGRID_API_KEY)%'
swiftmailer mailer at the bundle's transport service (service id is defined in Resources/config/; verify it, don't guess), or leave swiftmailer configured as usual and inject the transport into your own Swift_Mailer.Where to look first
DependencyInjection/Configuration.php and …Extension.php — the only source of truth for real config keys and defaults.Resources/config/services.xml (or .yml) — the transport's service id and whether it is wired as swiftmailer.mailer.default.transport.Swift_Mime_Message is translated into a SendGrid Mail object. Everything this package can and cannot do lives in that one file: to/cc/bcc, attachments, inline images, custom headers, reply-to, sandbox mode, templates.First use case: send one transactional email through Swift_Mailer exactly as you would with SMTP. No application code should change — $mailer->send($message) — which is the whole point of a transport. Get a password-reset email to yourself, then check the SendGrid Activity Feed to confirm it left the building.
Standard transactional send. Build the Swift_Message, set both HTML and plain-text parts (addPart()), and hand it to the mailer. Because the bundle is a transport, it composes cleanly with everything you already do: Twig-rendered bodies, Swift_Message::setEncoder(), address objects, Swift_Plugins_* decorators.
$message = (new Swift_Message('Reset your password'))
->setFrom('[email protected]', 'Example')
->setTo($user->getEmail())
->setBody($html, 'text/html')
->addPart($text, 'text/plain');
$mailer->send($message);
Two-mailer topology is the most useful pattern here. Keep SendGrid for transactional traffic and a real SMTP/DSN-capable transport for everything else, so a SendGrid 401 or outage doesn't stop password resets and so you don't lose bounce data on bulk mail. Register the bundle's transport as a service and build a dedicated mailer:
services:
app.mailer.transactional:
class: Swift_Mailer
arguments: ['@<bundle transport service id>']
Then alias it as Swift_Mailer $transactionalMailer for autowiring, or expose it under a named swiftmailer.mailers key.
Always spool when sending more than a handful. Each send is a synchronous HTTPS round-trip; 500 recipients in a request loop will time out your worker. Use swiftmailer.spool (file or a custom Swift_Spool) and drain with a cron/worker:
bin/console swiftmailer:spool:send --message-limit=200 --time-limit=60
Run that under Supervisor, and wrap it so a single transport exception doesn't abandon the rest of the spool.
Rate-limit and back off. SendGrid's v3 mail-send limit is ~100 requests/second per account and it will return 429. Since each send() is one request, throttle in your batch job (sleep between chunks) and treat Swift_TransportException as retryable only for 429/5xx — never blanket-retry a 400, which will just fail again and burn quota.
Observe delivery separately from sending. send() returning true only means SendGrid accepted the request (HTTP 202). The pattern that actually works: configure the SendGrid Event Webhook to a Symfony controller that verifies the signature, enqueues the event, and updates your message/recipient status table on delivered, bounce, dropped, spamreport, unsubscribe. Without this you will have no idea that half your list is suppressed.
Prefer SendGrid-side templates when you can. If the transport doesn't surface template_id / dynamic_template_data, don't fight it — either render in Twig (simplest, keeps versioning in your repo) or subclass the bundle's transport and override the message-conversion method to attach the template. That single method is the intended extension point for anything the bundle doesn't expose: templates, mail_settings.sandbox_mode, tracking_settings, categories, custom_args.
Test with a null transport, never a live key. Set swiftmailer.disable_delivery: true (or point the mailer at Swift_NullTransport / a Swift_Transport_SpoolTransport with an array spool) in test env, and assert on the spooled Swift_Message objects. In dev, a Swift_Events_SendListener that logs the recipient, subject, and message-id is worth more than any debug toolbar panel.
Guard the key. '%env(SENDGRID_API_KEY)%', never committed. The key needs only the Mail Send permission — do not reuse a full-access key. Restrict outbound senders with a domain authentication entry in SendGrid so a leaked key can't spoof your domain.
Migration target. For new work, symfony/mailer + symfony/sendgrid-mailer covers the same ground with maintained code and native Symfony\Component\Mime\Email, plus MailerInterface and TransportInterface that are far easier to decorate. Porting is mostly mechanical: Swift_Message → Email, $mailer->send() → $mailer->send().
sendgrid/sendgrid v7's SendGrid\Mail\* namespace. If you get Class 'SendGrid\Mail' not found (or the inverse), you installed the wrong major — pin sendgrid/sendgrid to whatever the bundle's composer.json was built against, and lock it.host, port, encryption, username, password, auth_mode, and stream_options from swiftmailer config are silently ignored. If mail "sends" but never arrives, check that the mailer is actually bound to this transport and not to a FailoverTransport/SpoolTransport chain that swallowed the call — walk $mailer->getTransport() and unwrap.send() can throw after partial acceptance. With multiple recipients, a transport exception may surface after some were accepted; a naive retry duplicates mail. Use $mailer->send($message, $failedRecipients) and log $failedRecipients, and make retries idempotent via a X-Message-Id-style custom arg that SendGrid echoes back through the event webhook.From domain isn't authenticated or the single sender isn't verified in SendGrid. Fix it in the SendGrid dashboard, not in Symfony.default_socket_timeout, ensure curl's connect timeout is configured (often via the SDK, sometimes only by forking the transport), and keep sends out of user-facing requests via the spool.swiftmailer:spool:send stops when the transport throws; a poisoned message can wedge the queue. Run it with --message-limit, monitor exit codes, and consider a dead-letter path.setReturnPath() / envelope sender behaves differently over an API transport than over SMTP; SendGrid derives bounce routing from from and your domain settings. Set Reply-To explicitly and add a List-Unsubscribe header for anything bulk-ish — SendGrid expects it and mailbox providers will penalise you without it.mail_settings.sandbox_mode, subclass it and enable sandbox while you're validating payloads — you get 202s and full validation without delivering mail. It's the cleanest way to test the whole path in staging.decorates: in Symfony 4.1+, or an explicit wrapper) and log the SendGrid\Mail\Mail JSON before it's POSTed. That one log line answers 90% of "why did this render wrong" questions, and it's the only way to see header/cid/attachment mapping mistakes.SENDGRID_API_KEY requires cache:clear in prod (config:cache doesn't exist pre-4.2 but %env()% is resolved at container compile time in some setups). If the key contains characters that trip the YAML/DIC parser, use a ! prefix or an env processor rather than quoting gymnastics.App\Mailer\Transport (it's small), pin it in composer.json with a comment explaining why, and schedule the symfony/mailer migration. If you must keep it, add a test that asserts the generated SendGrid payload shape so an accidental SDK bump fails CI instead of production.How can I help you explore Laravel packages today?