NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages
Campaigns for Laravel

Campaign stores

A store is persistence only. Batches, status changes and events live in the actions, so a store just keeps campaigns and their recipients. The configured one (config: store, env CAMPAIGNS_STORE) is bound in the container as Contracts\CampaignStore — scoped, so each request or queued job gets its own — and its reads return copies — changing a returned object changes nothing until it is saved:

use RoundlyConsulting\Campaigns\Contracts\CampaignStore;

// The configured store, as the actions see it (bound scoped — one per request or queued job):
$store = app(CampaignStore::class);

$store->find('e184d08f-5081-4fbb-9604-7a2460c3fb3a');         // ?Campaign — a copy
$store->recipients('e184d08f-5081-4fbb-9604-7a2460c3fb3a');   // Collection<int, CampaignRecipient>
$store->countRecipients('e184d08f-5081-4fbb-9604-7a2460c3fb3a'); // RecipientCounts: total, processed, failed

The contract

MethodPurpose
find(string $campaignUuid): ?CampaignA stored campaign; null when unknown.
all(int $offset = 0, int $limit = 10): CollectionA page of campaigns in creation order.
insert(Campaign $campaign): boolInsert a new campaign; false — and nothing written — when its uuid is taken, live or soft-deleted. Must be one atomic step.
save(Campaign $campaign): voidInsert or update the campaign by its uuid.
saveIfStatus(Campaign $campaign, CampaignStatus $expected): boolSave only while the stored status is still $expected — the atomic compare-and-set every status change goes through. False for a campaign the store doesn’t hold.
saveRecipients(string $campaignUuid, array $recipients): voidInsert or update each recipient keyed by (campaign, uuid) — the same recipient saved under two campaigns is kept in both.
recipients(string $campaignUuid, int $offset = 0, ?int $limit = null): CollectionThe campaign’s recipients in the order they were added; a null limit returns them all.
findRecipient(string $campaignUuid, string $recipientUuid): ?CampaignRecipientA recipient of this campaign; one of any other campaign reads as null.
countRecipients(string $campaignUuid): RecipientCountsThe recipients counted by outcome — total, processed, failed — which progress is built from.

insert() (refuse a taken uuid) and saveIfStatus() (write only while the stored status is the expected one) must each be one atomic step: they are what make duplicate uuids and racing starts safe.

Shipped stores

  • Stores\InMemoryCampaignStore (default) — keeps campaigns in memory for the current request, console command or queued job. Ideal for tests and create-and-send flows; nothing survives it.
  • Stores\DatabaseCampaignStore — keeps campaigns in the CampaignRecord and CampaignRecipientRecord models. See Database persistence.

Both run through one shared contract test suite, so they behave identically. To keep campaigns anywhere else, implement Contracts\CampaignStore and point the config at it — it’s resolved from the container, so constructor dependencies are injected:

// config/campaigns.php
'store' => \App\Campaigns\ApiCampaignStore::class, // implements Contracts\CampaignStore

Code that sends campaigns never talks to the store directly — it goes through the Campaigns facade, an injected CampaignManager or the actions (see DI and actions).

Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

More ways to support, including crypto

By donating, you agree to our donation terms.

Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.