welp/mailchimp-bundle
Symfony bundle that integrates the MailChimp API into PHP applications, providing configuration and dependency-injection services for managing lists, subscribers, and newsletter signups. Simplifies MailChimp setup and API access within Symfony projects.
First, a compatibility check. welp/mailchimp-bundle is a Symfony bundle, not a Laravel package. It has no ServiceProvider, no facade, no auto-discovery, and no publishable Laravel config. If your goal is "call MailChimp from a Laravel app," this is the wrong dependency — it wraps drewm/mailchimp-api (the de-facto PHP V3 client), and you should depend on that directly, or on spatie/laravel-newsletter if you want a Laravel-native abstraction. Reach for this bundle only if you are actually in a Symfony app, or maintaining a Symfony service that a Laravel app talks to over HTTP.
If you are in Symfony:
composer require welp/mailchimp-bundleWelp\MailChimpBundle\WelpMailChimpBundle in config/bundles.phpsrc/DependencyInjection/Configuration.php in the repo, because the README is thin. Expect something close to:
# config/packages/welp_mail_chimp.yaml
welp_mail_chimp:
api_key: '%env(MAILCHIMP_API_KEY)%'
Welp\MailChimpBundle\Service\...) into your own service via constructor autowiring. Prefer injecting a concrete type over $container->get() so the alias stays visible to static analysis.Where to look first, in order: DependencyInjection/Configuration.php (the real config contract), Service/ (public API surface and method names), then Resources/config/services.xml/.yaml (service IDs and aliases). The README's examples are the least reliable artifact in a 44-star bundle; the DI definitions are the source of truth.
First use case: subscribe an address to a list. You need the list ID (a 10-char hex string, from the MailChimp UI) plus the member payload:
$mailChimp->addMemberToList($listId, strtolower($email), [
'status' => 'subscribed', // or 'pending' for double opt-in
'merge_fields' => ['FNAME' => $firstName, 'LNAME' => $lastName],
'tags' => ['lead'],
]);
Verify the exact method name against src/Service/ before you ship — the wrapper exposes a subset of the DrewM surface, and it is not documented.
Wrap it, don't call it directly. The single highest-value pattern with a low-activity bundle is a one-class indirection. Define your own NewsletterClient interface, implement it with the bundle's service, and inject that everywhere. When the bundle stalls (last release Oct 2025, zero dependents), you swap one implementation for raw drewm/mailchimp-api or Laravel's Http facade without touching call sites.
interface NewsletterClient {
public function subscribe(string $email, array $mergeFields = []): void;
public function unsubscribe(string $email): void;
public function tag(string $email, array $tags): void;
}
Keep the email → subscriber hash mapping in one place. V3 member endpoints identify members by md5(strtolower($email)), not by email. Centralize that:
private function hash(string $email): string {
return md5(strtolower(trim($email)));
}
Every "get / update / delete member" call needs it, and forgetting strtolower produces silent 404s that look like "member doesn't exist" rather than "you hashed it wrong."
Push writes onto a queue. MailChimp is an external API with a hard 10-requests-per-second ceiling per account and 429s that are easy to trigger from a signup spike. Fire an event or dispatch a job on registration, and let the worker do the subscribe. In Symfony that's Messenger; in Laravel, a queued job with a thin Http/DrewM client. This decouples user-facing latency from MailChimp availability.
Use upsert (PUT), not create-then-update. The V3 member endpoint is idempotent when you PUT to /lists/{id}/members/{hash} with status_if_new. That single call handles "new subscriber" and "existing subscriber changed their name" without a round-trip read. Where the bundle exposes only addMember-style methods, prefer the ones that map to PUT; if it doesn't, this is the first place you should bypass the bundle with the underlying client.
Batch for any backfill or bulk import. A one-off "sync 20k users" via per-member calls will take hours and get rate-limited. The underlying DrewM client supports newBatch() / executeBatch() (500 operations per batch, 10 concurrent). The bundle likely does not expose batching — treat bulk work as a raw-client task and document that fork in your codebase.
Model double opt-in explicitly. status: 'pending' sends MailChimp's confirmation email; status: 'subscribed' skips it and is a compliance risk in the EU/UK. Make it a parameter on your subscribe() signature, not a hardcoded string buried in the adapter.
Webhooks belong to your app, not the bundle. MailChimp's unsubscribe, profile, cleaned, and upemail webhooks arrive as form-encoded POSTs. Handle them in your own controller/route, verify the shared secret, and keep the handler idempotent — MailChimp retries. Do not expect the bundle to route or validate these for you.
Testing. Because the bundle's service is a concrete class wrapping a live HTTP client, unit tests against it are awkward. Test your NewsletterClient implementation's payload construction by extracting a pure buildMemberPayload() method, and mock the interface everywhere else. For integration tests, point the client at a MailChimp test audience, never production — a stray DELETE is irreversible.
extra.laravel block means composer require gives you a bundle that Laravel's container will never see. You can instantiate it manually (new MailChimp($apiKey)), but at that point you've paid for the Symfony DI wiring and gotten none of it. Just use drewm/mailchimp-api.abc123…-us14. The suffix is the datacenter; the underlying client parses it to build the base URL. A key with the suffix stripped produces auth failures that read like permission errors. Never trim() past the hyphen.getLastError() / getLastResponse() rather than throwing. If the bundle's wrapper doesn't surface these, a failed subscribe looks identical to a successful one. Check the return value on every write, or wrap the call in a decorator that inspects getLastError() and throws.429; do not retry immediately. Track X-Request-Id from failed responses — MailChimp support asks for it.status vs. delete. Deleting a member removes them from the audience and from your historical stats; status: 'unsubscribed' preserves the suppression record. For GDPR erasure requests you want the delete; for "user clicked unsubscribe" you want the status change. Getting this backwards creates compliance problems either way.FNAME to a list that doesn't define it returns a 400 with a per-field error body. Field tags are per-audience, so staging and production audiences with different field setups will diverge in behavior.%env(...)% at container compile time. If MAILCHIMP_API_KEY isn't set during bin/console cache:warmup in CI, the build fails for a reason unrelated to your tests. Set a dummy value in test env.App\Newsletter\LoggingMailChimp:
decorates: 'welp_mail_chimp.service' # confirm the ID in services.xml
This is also the cleanest path to adding getLastError() assertions globally.composer.json, watch the repo for abandonware signals, and keep the NewsletterClient boundary tight enough that vendoring the ~200 lines of wrapper logic is a 30-minute exercise if you ever need to.services.yaml. Run bin/console debug:container mailchimp once and write the answer down in your team's notes — it will save the next person an hour.How can I help you explore Laravel packages today?