terehinis/mandrill-bundle
A Laravel bundle that integrates the Mandrill transactional email API, letting you configure API credentials and send template or dynamic emails through a simple, framework-friendly interface.
This is a Symfony bundle. In a Laravel app, "getting started" means getting started with mandrill/mandrill — not with this repository. There is no ServiceProvider, no facade, no extra.laravel autodiscovery, and no bootstrap/app.php equivalent. composer require terehinis/mandrill-bundle will pull a Symfony bundle and its constraints, not a Laravel integration.
Where to look first (in the repo):
composer.json — declares type: symfony-bundle, PHP constraint, and Symfony version (~2.x/3.x). This is the fastest way to confirm it won't resolve on Laravel 10–12.<Vendor>/<Bundle>Bundle.php — the Bundle class registered in AppKernel/bundles.php. No Laravel counterpart.DependencyInjection/*Extension.php + Resources/config/services.yml — the config tree (mandrill: api_key: ...) and service wiring. This is the shape you'll mirror in a Laravel service provider.mandrill/mandrill. Read it for request shape only.Minimal path a Laravel dev should actually take:
composer require mandrill/mandrill (the underlying SDK) or, preferably, call the Mandrill HTTP API with Illuminate\Support\Facades\Http.AppServiceProvider::boot():
Mail::extend('mandrill', fn () => new MandrillTransport(config('services.mandrill.key')));
MandrillTransport implements Symfony\Component\Mailer\Transport\TransportInterface..env: MAIL_MAILER=mandrill, MANDRILL_KEY=.... .env/config/services.php — never a committed YAML.php artisan make:mail PasswordReset → Mail::to($user)->send(new PasswordReset($token)). Prove /users/ping returns ok before wiring anything else.If you are inheriting a Symfony 2.x/3.x app that already uses this bundle, "getting started" means running the existing sends, reading mandrill: config in config.yml, and using the call sites as a migration checklist to Symfony Mailer's Mailchimp transport.
Pattern 1 — Custom Laravel mail transport (the "correct" integration).
Implement Symfony\Component\Mailer\Transport\TransportInterface (Laravel 9+ uses Symfony Mailer under Illuminate\Mail). It receives an Envelope and RawMessage, and you POST to /messages/send. Wire via Mail::extend(). Keep it under 200 lines and inject a PSR-18 client (Guzzle) — never let the transport new a client internally.
Pattern 2 — Bound service for template sends.
The Mail facade is rigid for merge_vars, async, subaccount, metadata, and template slugs. Register a small MandrillClient in the container:
$this->app->singleton(MandrillClient::class, fn ($app) =>
new MandrillClient($app['http'], config('services.mandrill.key')));
Expose sendTemplate(string $slug, string $to, array $vars, array $opts = []) and sendRaw(). Use Laravel's Http client or Guzzle with a hard timeout injected from config (5s).
Pattern 3 — Queue the send.
Never send in the request cycle. Wrap the service call in a Job:
SendTransactionalEmail::dispatch($mailable)->onQueue('mail')
->tries(3)->backoff([10, 60, 300]);
This decouples user-facing latency from Mandrill's throttle and gives you retries, dead-lettering, and observability for free.
Pattern 4 — Blade vs. Mandrill templates. Pick one, deliberately:
html and version-control templates in git. Preferred for transactional (password resets, receipts).merge_vars → faster for marketing, but the contract is live in the dashboard and drift is silent. Use X-MC-* headers for tagging.Pattern 5 — Webhook controller.
Route POST /webhooks/mandrill, verify X-Mandrill-Signature, then dispatch to handlers keyed on event (send, hard_bounce, soft_bounce, spam, unsub, open, click). Persist raw payloads first (an email_events table), act on them second — you will need to replay.
Pattern 6 — Testing.
Mail::fake() covers the facade path but not the transport, so the payload shape is never asserted. Add one feature test that binds Http::fake() and asserts the JSON body.FakeMandrillClient in AppServiceProvider under APP_ENV=testing.async=false for the one contract test that touches the network; everything else stays offline.Pattern 7 — Migration from an existing bundle install (Symfony → Laravel, or Mandrill wrapper → Mailer).
grep -R "mandrill" app/ src/ config/ to build the call-site inventory.MAIL_TRANSPORT=mandrill|mailchimp|ses.Pattern 8 — Subaccount / metadata passthrough.
Expose subaccount, metadata, and tags as first-class parameters in the service. Ad-hoc X-MC-* headers scattered across mailables are impossible to audit later.
package:discover handling, no facade. Any integration is a port. Budget 1–2 dev days for a production-grade transport, not 30 minutes.mandrill/mandrill is a thin curl wrapper (~2015 era). It does not respect PSR-18, has limited timeout/retry controls, and may emit PHP 8 deprecations. Prefer Http::withToken(...)->timeout(5)->retry(2, 100)->post('https://mandrillapp.com/api/1.0/...').status: "error". Mandrill signals application errors with a 200 response. You must inspect the body, not just the status code. Map at minimum: -1 invalid key, -2 suspended, -3 invalid recipient, -4 payment required, -6 invalid template, -8 invalid domain. Anything unmapped → dead-letter and alert.async=true hides delivery outcomes. It returns a job id you cannot query. For deterministic behaviour in tests and critical paths, use async=false.merge_vars name mismatches fail silently. The email still sends with blanks. Assert the rendered HTML in your Http::fake() feature test.X-Mandrill-Signature is HMAC-SHA1 over the webhook URL followed by sorted POST key/value pairs, keyed with the webhook's signing key — not your API key. Verify before writing anything to the DB. Missing this means spoofable bounce/complaint events.POST /password/email. Hard 5s timeout, bounded backoff, circuit breaker with a fallback transport.mandrill: api_key: <literal> in config.yml, that key is in git history. Rotate it.email_events webhook persistence, suppression list sync, dead-letter queue, per-subaccount rate limiting, X-MC-Tags/X-MC-Metadata passthrough.LICENSE before forking or copying code out of the repo. MIT is conventional here, but verify — you are copying logic into your own namespace.App\Mail\Mandrill\* is never mistaken for upstream; a Terehinis\... copy is. Budget ~10–15% of one engineer per quarter for API drift, PHP upgrades, and credential rotation.How can I help you explore Laravel packages today?