Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Campaign Monitor Bundle Laravel Package

intracto/campaign-monitor-bundle

Symfony bundle that integrates the Campaign Monitor email marketing API into your application, providing configured services for managing subscriber lists, adding and updating subscribers, and working with campaigns. Credentials and API clients are wired through bundle configuration, keeping them ou...

View on GitHub
Deep Wiki
Context7

Getting Started

Understand what you're adopting first. intracto/campaign-monitor-bundle is a Symfony bundle. It ships a DependencyInjection/Extension, a Configuration tree, and Resources/config/services.* that wire campaignmonitor/createsend-php into Symfony's container. Laravel has no bundle kernel, no bundles.php, and no extension lifecycle, so the package cannot be used idiomatically in a Laravel app. Day-to-day leverage means one of two things:

  1. Preferred: require the SDK directly and re-implement the bundle's ~40 lines of wiring as a Laravel service provider.
  2. Only if forced: composer require intracto/campaign-monitor-bundle and instantiate its classes by hand — at which point the bundle buys you nothing and complicates your dependency graph.

Minimal setup (5 minutes):

composer require campaignmonitor/createsend-php

Add config:

// config/services.php
'campaign_monitor' => [
    'api_key'   => env('CAMPAIGN_MONITOR_API_KEY'),
    'client_id' => env('CAMPAIGN_MONITOR_CLIENT_ID'),
],

Bind a configured client:

// app/Providers/CampaignMonitorServiceProvider.php
public function register(): void
{
    $this->app->singleton(CampaignMonitor::class, function ($app) {
        $config = $app['config']['services.campaign_monitor'];

        return new CampaignMonitor(
            new CS_REST_Clients($config['client_id'], ['api_key' => $config['api_key']]),
            $config['api_key'],
        );
    });
}

Where to look first in the repo (as a reference implementation, not a dependency):

  1. composer.json — the pinned campaignmonitor/createsend-php version tells you the SDK surface (CS_REST_Clients, CS_REST_Lists, CS_REST_Subscribers, CS_REST_Campaigns).
  2. DependencyInjection/Configuration.php — the canonical config keys (api_key, likely client_id, possibly timeout/secure flags).
  3. Resources/config/services.xml (or .yml) — the actual service IDs and constructor arguments. This is the entire bundle; it's small.
  4. DependencyInjection/IntractoCampaignMonitorExtension.php — how config becomes constructor args.

First use case — subscribe someone and confirm wiring works:

$subscriber = new CS_REST_Subscribers($listId, ['api_key' => config('services.campaign_monitor.api_key')]);
$result = $subscriber->add([
    'EmailAddress'   => $email,
    'Name'           => $name,
    'Resubscribe'    => true,
    'ConsentToTrack' => 'Yes',
    'CustomFields'   => [['Key' => 'source', 'Value' => 'signup-form']],
]);

if ($result->http_status_code !== 201) {
    throw new RuntimeException('Campaign Monitor add failed: ' . $result->http_status_code);
}

If that returns 201, you've matched everything the bundle would have done for you.

Implementation Patterns

Wrap the SDK behind a gateway interface — don't inject CS_REST_* directly into controllers. The bundle exposes raw SDK classes; replicating that in Laravel means controllers become SDK-aware and untestable. Put one application-owned interface in front:

interface CampaignMonitorGateway
{
    public function subscribe(string $listId, string $email, array $fields = []): void;
    public function unsubscribe(string $listId, string $email): void;
    public function lists(): array;
}

Bind the concrete implementation to the interface in the provider, and mock the interface in feature tests. The SDK's CS_REST_* classes use fsockopen/cURL internally and are awkward to mock — this wrapper is what makes your tests fast.

Queue every write. The bundle adds no async capability — it's a DI wrapper. Subscriber writes, list imports, and campaign triggers should be jobs:

class SyncSubscriberToCampaignMonitor implements ShouldQueue
{
    use Queueable, InteractsWithQueue;

    public int $tries = 3;
    public array $backoff = [10, 60, 300];

    public function __construct(
        public readonly string $listId,
        public readonly string $email,
        public readonly array $fields = [],
    ) {}

    public function handle(CampaignMonitorGateway $gateway): void
    {
        $gateway->subscribe($this->listId, $this->email, $this->fields);
    }
}

This is the day-to-day pattern: model events → dispatch job → gateway → SDK. It keeps API latency out of the request cycle and gives you retry semantics the bundle never had.

Chunk bulk list imports. Campaign Monitor's API is per-subscriber (with a separate import endpoint on CS_REST_Lists), and rate limits apply per account. Use ->chunk(200) on your collection and dispatch one job per chunk, keyed by list ID, so retries are idempotent (the SDK's add with Resubscribe => true is effectively upsert).

Handle the SDK's response shape consistently. Every CS_REST_* method returns an object with ->http_status_code and ->response. It does not throw exceptions on API errors. Centralise the check:

private function assertOk($result, string $context): void
{
    if ($result->http_status_code >= 400) {
        throw new CampaignMonitorException(
            "{$context} failed [{$result->http_status_code}]: " . json_encode($result->response)
        );
    }
}

Call this after every gateway method. Missing this is the single most common cause of silent data loss.

Webhook ingestion belongs in your routes, not the bundle. The bundle provides no routing. Add POST /webhooks/campaign-monitor and verify the payload signature against your stored secret before mutating local state (bounces, unsubscribes, opens). Campaign Monitor webhooks are JSON POSTs; verify X-CampaignMonitor-Signature (HMAC-SHA256 of the body with your webhook secret) and reject anything that doesn't match.

OAuth vs API key — pick one model per environment. Newer Campaign Monitor accounts may prefer OAuth tokens. The bundle's config tree (from the era of its last commits) almost certainly assumes a static API key. If you need OAuth, bypass the config layer entirely and construct the SDK with the token; don't try to shoehorn it into the bundle's config schema.

Bind a test double in the provider for local/dev. Since the gateway is a singleton, a LocalCampaignMonitorGateway that logs calls is a one-line swap in AppServiceProvider when app()->environment('local') — useful for local QA without burning API quota.

Gotchas and Tips

  • Composer will fight you on modern PHP. The bundle almost certainly pins symfony/framework-bundle to 2.x/3.x and PHP 5.x/7.x. Installing it in a Laravel 10/11 app on PHP 8.2 typically fails resolution. This is the primary reason to skip the bundle entirely — you're not losing functionality, you're losing a services.xml you can copy in two minutes.

  • The SDK is not Laravel-friendly by default. CS_REST_* constructors take ['api_key' => ...] as a second arg — there's no global client and no config() injection. Wrap construction in the provider exactly once; never new CS_REST_Subscribers(...) inside a controller.

  • cs_rest_* responses are not exceptions. As above: every method returns a response object. A 404 on get for a missing subscriber is normal flow, not an error — check http_status_code explicitly.

  • Rate limiting is per-account, not per-client. Campaign Monitor throttles aggressively for bulk operations. If you're syncing thousands of subscribers, expect 429s and design the job's backoff accordingly. The bundle adds no connection pooling, no caching, no retry — you build all of that.

  • Resubscribe and ConsentToTrack are load-bearing fields. Resubscribe => true is required to re-add someone who previously unsubscribed (the API returns a 400 for "already unsubscribed" without it). ConsentToTrack => 'Yes' is required in jurisdictions where marketing consent is captured — setting it to 'No' suppresses open/click tracking but still delivers. Getting these wrong silently produces subscribers who don't appear in reports.

  • CustomFields uses array-of-['Key' => ..., 'Value' => ...]. It's not an associative array. This is a common mistake and the API error message is unhelpful (Invalid fields).

  • List IDs and client IDs are different identifiers. CS_REST_Lists takes a list ID; CS_REST_Clients takes a client ID; CS_REST_Campaigns takes a campaign ID. The bundle's config tree exposes a single client_id — don't assume that's all you need; per-account list IDs still have to be configured or persisted elsewhere.

  • Don't put the API key in the bundle's config tree if you ever serialise config. Symfony bundle configs are dumped at container compile time into a PHP file. If you fork the bundle or copy its patterns, ensure the key comes from %env(...)% and not from a literal — Laravel's config/services.php pattern is safer by default.

  • The SDK may pull in old Guzzle. Check composer why guzzlehttp/guzzle after installing. If the SDK's Guzzle constraint conflicts with Laravel's HTTP client expectations, you have two options: use the SDK's native transport (which it defaults to) and ignore Guzzle, or pin the SDK to a version whose Guzzle constraint is compatible.

  • Test with Http::fake() only after wrapping. You can't Http::fake() the SDK directly because it uses its own transport. The gateway interface is what lets you $this->mock(CampaignMonitorGateway::class) in tests — that's the testing seam, and it's why the wrapper pattern isn't optional.

  • Webhook verification is not the bundle's job. Don't assume the bundle provides signature verification just because it's "a Campaign Monitor integration." It doesn't. Build HMAC verification yourself and log signature failures as security events, not 500s.

  • Extension points are zero. The bundle exposes no events, no middlewares, no decorators. If you need to log API calls, instrument rate limits, or inject idempotency keys, you do it in your own gateway — plan for that upfront rather than expecting to configure it.

  • Pin your SDK version. With 0 dependents and no active maintenance, assume no upstream will fix a broken SDK constraint when Campaign Monitor changes the API. Pin campaignmonitor/createsend-php explicitly in composer.json and treat upgrades as a deliberate, tested action.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
stevenay/myanfont
gmostafa/php-graphql-client
xeviant/paystack
snowsoft/faker
meditationsteps/blogetc
animalyoung/laravel-influxdb
onix-solutions/sms-telera
lonely-lights/prosetta
oxygen/data
oxygen/core
jengo/base
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin
artisan-build/community-prompts
vleroy/smart
ayles-software/laravel-australian-public-holidays
cboxdk/laravel-id-spatie
cboxdk/laravel-id-billing
cboxdk/laravel-id-billing-bridge