mlpz/mailchimp-bundle
A Symfony bundle that integrates the Mailchimp API, providing service wrappers to manage audiences, members, and subscriptions from your PHP application. Useful for newsletter signups and list synchronization.
This is a Symfony bundle, not a Laravel package. There is no service provider, no facade, no auto-discovery, and no config/mailchimp.php. Treat it as a library of classes you may be able to wire up manually — not as a drop-in Laravel dependency. Before writing any application code, do a 15-minute feasibility pass.
The latest release (v1.3) changes exactly one thing: it adds a deprecation notice for API v1. It is not a fix, not a v3 migration, and not a Laravel compatibility patch. Read it as a confirmation of what the previous assessment suspected: this bundle talks to a retired API.
Minimal steps
Check installability first. In a throwaway branch:
composer require mlpz/mailchimp-bundle --dry-run
The package dates from 2014. Expect conflicts with modern symfony/* components, PHP 8, and possibly PSR-4 autoload mismatches. If Composer refuses, stop here — the cost of forcing it is higher than rewriting the integration.
Read the bundle source in this order (once installed, under vendor/mlpz/mailchimp-bundle/):
README.md — the only usage docs that exist.CHANGELOG.md / git tags — v1.3 is the "add deprecating message for API v1" release; expect a trigger_error/E_USER_DEPRECATED call somewhere on the request path.DependencyInjection/Configuration.php — the real config schema (api_key, list defaults, etc.).DependencyInjection/*Extension.php — shows the service ID and which class it instantiates.Services/ — the wrapper class exposing subscribe(), unsubscribe(), batchSubscribe(), etc. This is the only part you actually care about.Do not try to boot the bundle. Laravel has no Kernel::registerBundles(). Instead, instantiate the service class yourself in a service provider and bind it to the container:
// config/services.php
'mailchimp' => [
'key' => env('MAILCHIMP_API_KEY'),
'list_id' => env('MAILCHIMP_LIST_ID'),
],
// app/Providers/AppServiceProvider.php
public function register(): void
{
$this->app->singleton(\MZ\MailChimpBundle\Services\MailChimp::class, function () {
return new \MZ\MailChimpBundle\Services\MailChimp(
config('services.mailchimp.key')
);
});
}
Confirm the constructor signature against the actual source — it may expect a full config array or a pre-built client rather than a bare API key.
First use case: subscribe on registration. Wire a queued listener on Registered, resolve the bound service, subscribe the user's email to a dedicated test audience. If that round-trips successfully, the rest is mechanical. If it returns 404/410 — or throws the new deprecation as an ErrorException (see gotchas) — you've hit the API-version wall and should migrate rather than debug.
Where to look first when something breaks: Services/ for the call signature, then the underlying HTTP call it makes, then grep the release diff for the new deprecation trigger_error. Nine times out of ten the failure is the endpoint/version, not your code.
Wrap it, never call it directly. The bundle's service class leaks MailChimp-specific method signatures and exception types all over your codebase. Put one adapter behind an interface so you can swap the transport later — this matters more now that the vendor has publicly acknowledged the API it targets is deprecated:
interface Newsletter
{
public function subscribe(string $email, array $mergeFields = []): bool;
public function unsubscribe(string $email): bool;
}
final class BundleNewsletter implements Newsletter
{
public function __construct(
private \MZ\MailChimpBundle\Services\MailChimp $client,
private string $listId,
) {}
public function subscribe(string $email, array $mergeFields = []): bool
{
try {
$this->client->subscribe(
$this->listId,
strtolower(trim($email)),
$mergeFields,
'html',
false // single opt-in; see gotchas
);
return true;
} catch (\Exception $e) {
report($e);
return false;
}
}
}
Always subscribe from a queued job. MailChimp calls are slow and flaky. Never block a registration request on them:
class SyncSubscriber implements ShouldQueue
{
public function handle(Newsletter $newsletter): void
{
$newsletter->subscribe($this->user->email, [
'FNAME' => $this->user->first_name,
]);
}
}
Treat integration as event-driven, not inline. UserRegistered → listener → job, and UserUpdated → job when email or name changes. This keeps the MailChimp dependency at the edge of the app and makes it trivial to disable in tests.
Idempotency is your responsibility. Registration retries, double-clicks, and replayed queue jobs all produce duplicate subscribe calls. Design the adapter to treat "already subscribed" as success rather than an error (log at debug, return true).
Config resolution. Read everything from config/services.php backed by .env. The API key encodes the datacenter as its suffix after the last - (...-us14). If you ever fall back to raw HTTP calls, you need that segment in the host (us14.api.mailchimp.com).
Testing. Bind a fake Newsletter implementation in the test container. Do not let a unit test touch the bundle's client — it will attempt real DNS resolution and fail in CI. For any raw HTTP you add yourself, use Http::fake().
Extension point. If you must customise behaviour (retry policy, logging, field mapping), decorate the bundle service rather than editing vendor files, and put all field-mapping logic in your adapter. That mapping layer is exactly what you'll carry over to a modern SDK, so keep it isolated.
Plan the exit now. v1.3 is a tombstone, not a lifeline. Budget the migration to mailchimp/marketing or drewm/mailchimp-api in the same sprint as the integration work, and keep every call site behind the Newsletter interface so the swap is a one-file diff.
trigger_error(..., E_USER_DEPRECATED), Laravel's exception handler converts any reported error level — including deprecations — into an ErrorException. On a default E_ALL setup that turns a previously working subscribe call into a thrown exception inside your queued job. Wrap the call in your adapter's try/catch, or add E_USER_DEPRECATED to error_reporting exclusions in the environment where you still run this bundle./1.3/, /2.0/), which are retired. The v3 API uses different paths (/3.0/lists/{list_id}/members), different auth (HTTP Basic with any username and the API key as password), and different payloads. No amount of clean wiring makes v1 calls succeed.service_container, %parameters%, and Symfony event dispatchers. Manually instantiating the wrapper class is usually fine; trying to use its DI extension from Laravel is not.each(), create_function(), or untyped properties removed in PHP 8. Run php -l over the Services/ directory before assuming it loads.subscribe() defaulted to double_optin = true, meaning users get a confirmation email. Pass false explicitly if you've already captured consent, or your "subscribed" users will silently sit in pending.list vs audience. Old docs and method names say "list"; the current UI says "audience". Same thing. Don't go looking for a separate "audience" parameter.FNAME, LNAME are uppercase in the API but the UI may display them capitalised. Mismatched keys are silently ignored rather than rejected — a classic "why is the name blank" bug. Verify field names in the audience's Settings → Audience fields and merge tags.[email protected] and [email protected] as the same member; if you don't normalise, your local records won't match theirs and dedupe logic breaks.title: "Member Exists". Catch and swallow it — this is expected on re-sync, not a failure.mailchimp/marketing (official v3 SDK) or drewm/mailchimp-api, keep your Newsletter interface, and delete the adapter's internals. Your call sites — jobs, listeners, controllers — should not change at all. Given v1.3's deprecation notice, treat this as scheduled work rather than a contingency.How can I help you explore Laravel packages today?