nedwave/mandrill-bundle
nedwave/mandrill-bundle is a Symfony bundle for integrating Mandrill transactional email into PHP applications. It provides service configuration and API wrappers to send messages, manage templates, and handle Mandrill delivery from Symfony.
Read this first: nedwave/mandrill-bundle is a Symfony bundle, not a Laravel package. It has no service provider, no config publishing, no facade. Treat it as a thin DI wrapper around the mandrill/mandrill SDK — useful on Symfony 2/3, mostly useful as a reference implementation on Laravel.
Where to look before installing anything:
composer.json — check the Symfony version constraint and PHP constraint. If it pins Symfony ^2.8 or ~3.0 and PHP ~5.5, it will not install cleanly alongside Laravel 10/11 and PHP 8.2.DependencyInjection/NedwaveMandrillExtension.php — this is where the api_key config is mapped into the service definition. That's the only real logic in the bundle.Resources/config/services.xml (or .yml) — confirm the service ID and whether an alias (mandrill, nedwave.mandrill) is registered.Mandrill subclass or a __call passthrough. That tells you whether you get the raw SDK or a narrowed API.Minimal Symfony setup (the intended use case):
# config.yml
nedwave_mandrill:
api_key: %mandrill_api_key%
// AppKernel.php
new Nedwave\MandrillBundle\NedwaveMandrillBundle(),
$mandrill = $container->get('nedwave_mandrill'); // confirm ID in services.xml
$mandrill->messages->sendTemplate([
'template_name' => 'welcome-email',
'template_content' => [],
'message' => [
'to' => [['email' => $user->email, 'name' => $user->name]],
'from_email'=> '[email protected]',
'subject' => 'Welcome',
'global_merge_vars' => [['name' => 'FIRST_NAME', 'content' => $user->first_name]],
],
]);
Laravel translation: skip the bundle, composer require mandrill/mandrill, and register the SDK in a small service provider. First use case is identical — send a template. The bundle's only value is its DI wiring; you don't need it.
Constructor injection, not container pulls. The bundle exposes the client as a service; inject it so you can swap it in tests:
public function __construct(private \Mandrill $mandrill) {}
Template-first sending. Store all copy in Mandrill's template editor and only pass merge vars from code. This is the pattern that survives marketing changing subject lines without a deploy. Use messages->sendTemplate() (or send with template_name) rather than sendRaw for anything user-facing.
Merge var strategy. Use global_merge_vars for values shared across all recipients in a call, and per-message merge_vars for recipient-specific fields. If you're looping anyway, just use merge_vars.
Batch sends. messages->send() accepts an array of message objects (up to 1000 per call, with subaccount/tag/async mixed) and returns a list of per-message results. This is the idiomatic way to do bulk mail — one HTTP call, not N calls:
$results = $mandrill->messages->send($messages);
foreach ($results as $i => $r) {
if ($r['status'] !== 'sent' && $r['status'] !== 'queued') {
// log reject_reason for $messages[$i]
}
}
Async + webhooks. For non-critical mail, send with 'async' => true and handle outcomes via Mandrill webhooks (send, open, click, hard_bounce, soft_bounce, spam, unsub). Register a single endpoint and dispatch on $event['event']. Always verify X-Mandrill-Signature:
$signed = $webhookUrl; // exact URL you registered
ksort($_POST);
foreach ($_POST as $k => $v) { $signed .= $k . $v; }
$expected = base64_encode(hash_hmac('sha1', $signed, $webhookKey, true));
// compare with hash_equals()
Note the bundle does not include webhook receiving — that's yours.
Reputation isolation. Use subaccount per tenant/product and tags per campaign. This is the Mandrill feature people discover too late; retrofitting is painful because history isn't backfillable.
Attachments and headers. Mandrill expects base64 content and a MIME type. Total attachment size is capped (~25 MB). Custom headers go in message.headers — use them for Reply-To, X-Entity-Ref-ID, or list-unsubscribe if not handled by the template.
Test strategy. Do not mock the Mandrill class deeply — it uses magic __get for sub-objects. Prefer an interface you own (Mailer) implemented by MandrillMailer, and fake that. Alternatively set Mandrill's account-level test mode toggle and assert on your own mandrill_events webhook receiving the test payload rather than real delivery.
MandrillServiceProvider, no config/mandrill.php, no facade. Any tutorial implying otherwise is wrong. On Laravel, use the SDK directly or the official mailchimp/transactional client.require.mandrill/mandrill is the old SDK; Mailchimp renamed the product to Mailchimp Transactional and its official client is distributed via GitHub (VCS repository, not Packagist). Pin a commit if you go that route.messages->send() does not throw on failure. It returns [['status' => 'rejected', 'reject_reason' => 'hard-bounce', ...]]. Wrap every call and inspect statuses; do not rely on try/catch for delivery problems.send vs sendTemplate vs sendRaw. send with template_name is the flexible path; sendTemplate requires template_content even if empty; sendRaw skips templates entirely and is only useful for pre-rendered MIME. Don't mix.%env()% interpolation inside bundle config — keep parameters.yml out of VCS and inject via environment at the parameters level, not the bundle level.https://mandrillapp.com/api/1.0/. If you're behind a proxy or in a region with egress rules, the SDK's internal curl call may be the only place to change it — check whether the bundle exposes it; often it doesn't, another argument for the direct SDK.mandrill. Dump the container (bin/console debug:container mandrill) or read services.xml before injecting by string.$_POST with ksort, concatenate key+value with no separators, HMAC-SHA1 with the webhook key (not the API key), base64-encode, and compare with hash_equals. The URL must match exactly what you registered, including scheme and trailing slash.application/x-www-form-urlencoded with a mandrill_events JSON blob — not JSON body. $request->input('mandrill_events') (Laravel) or $_POST['mandrill_events'] (Symfony), then json_decode.reject_reason for quota-related rejections, which come back as 200s.$client->messages, $client->templates, $client->users, $client->inbound).How can I help you explore Laravel packages today?