Subscriber Lists #
Organize and segment your email subscribers with flexible list management.
Overview #
Subscriber Lists provide:
- Organization & Segmentation: Group subscribers by interest, product, or engagement
- Many-to-Many: Subscribers can belong to multiple lists
- Campaign Targeting: Send messages to specific lists
- Statistics Tracking: Monitor list growth and engagement
- Import/Export: Bulk operations for list management
The "Everyone" List #
Every organization automatically has a special list called "Everyone" (code: everyone):
- Auto-Created: Created automatically when an organization is set up
- Auto-Populated: All subscribers are automatically added to this list
- Cannot Be Deleted: This list cannot be removed from the system
- Always Current: New subscribers are immediately added to this list
- Convenient Targeting: Send messages to all subscribers without selecting multiple lists
// Get the "Everyone" list for an organization
$everyoneList = SubscriberList::getEveryoneList($organizationId);
// The list always exists and contains all subscribers
$allSubscribers = $everyoneList->subscribers;
Use Cases:
- Send announcements to all subscribers
- View total subscriber count in one place
- Ensure no subscriber is missed when creating campaigns
Database Structure #
Lists stored in campaign_subscriber_lists table:
Schema::create('campaign_subscriber_lists', function($table) {
$table->increments('id');
$table->integer('organization_id')->unsigned();
$table->string('name'); // Display name
$table->string('code')->unique(); // Unique identifier
$table->text('description')->nullable();
$table->integer('count_subscriber')->default(0); // Cached count
$table->integer('count_active')->default(0); // Active subscribers
$table->timestamps();
});
Pivot Table #
Subscribers attached via campaign_subscribers_subscriber_lists:
Schema::create('campaign_subscribers_subscriber_lists', function($table) {
$table->integer('subscriber_id')->unsigned();
$table->integer('subscriber_list_id')->unsigned();
$table->timestamps();
$table->primary(['subscriber_id', 'subscriber_list_id'], 'subscriber_list_pivot');
});
Model Relationships #
class SubscriberList extends Model
{
public $belongsTo = [
'organization' => Organization::class
];
public $belongsToMany = [
'subscribers' => [
Subscriber::class,
'table' => 'campaign_subscribers_subscriber_lists',
'key' => 'subscriber_list_id',
'otherKey' => 'subscriber_id'
],
'messages' => [
Message::class,
'table' => 'campaign_messages_subscriber_lists'
]
];
}
Creating Lists #
Basic List #
use Albrightlabs\Campaign\Models\SubscriberList;
$list = SubscriberList::create([
'organization_id' => $org->id,
'name' => 'Newsletter Subscribers',
'code' => 'newsletter',
'description' => 'Main newsletter subscriber list'
]);
With Validation #
public $rules = [
'organization_id' => 'required|exists:organizations,id',
'name' => 'required|min:2|max:255',
'code' => 'required|alpha_dash|unique:campaign_subscriber_lists,code',
'description' => 'nullable|max:500'
];
Code Generation #
If code not provided, auto-generate from name:
public function beforeValidate()
{
if (!$this->code) {
$this->code = str_slug($this->name);
}
}
Managing Subscribers #
Add Subscribers #
// Single subscriber
$list->subscribers()->attach($subscriberId);
// Multiple subscribers
$list->subscribers()->attach([1, 2, 3, 4, 5]);
// With timestamps
$list->subscribers()->attach($subscriberId, [
'created_at' => now(),
'updated_at' => now()
]);
Remove Subscribers #
// Single subscriber
$list->subscribers()->detach($subscriberId);
// Multiple subscribers
$list->subscribers()->detach([1, 2, 3]);
// All subscribers
$list->subscribers()->detach();
Sync Subscribers #
// Replace all with new set
$list->subscribers()->sync([1, 2, 3, 4, 5]);
// Sync without detaching
$list->subscribers()->syncWithoutDetaching([6, 7, 8]);
List Statistics #
Subscriber Counts #
// Total subscribers (including unsubscribed)
$total = $list->subscribers()->count();
// Active subscribers only
$active = $list->subscribers()
->whereHas('status', function($q) {
$q->where('code', 'active');
})
->count();
Updating Cached Counts #
public function updateSubscriberCount()
{
$this->count_subscriber = $this->subscribers()->count();
$this->count_active = $this->subscribers()
->whereHas('status', function($q) {
$q->where('code', 'active');
})
->count();
$this->save();
}
Growth Tracking #
// New subscribers in last 30 days
$recent = $list->subscribers()
->wherePivot('created_at', '>=', now()->subDays(30))
->count();
// Growth rate
$previousCount = $list->subscribers()
->wherePivot('created_at', '<', now()->subDays(30))
->count();
$growthRate = $previousCount > 0
? (($recent / $previousCount) * 100)
: 0;
Querying Lists #
Get Organization Lists #
$lists = SubscriberList::where('organization_id', $org->id)
->orderBy('name')
->get();
Find by Code #
$list = SubscriberList::where('code', 'newsletter')
->where('organization_id', $org->id)
->first();
Lists with Subscriber Count #
$lists = SubscriberList::withCount('subscribers')
->where('organization_id', $org->id)
->get();
foreach ($lists as $list) {
echo "{$list->name}: {$list->subscribers_count} subscribers";
}
Campaign Integration #
Assign Lists to Message #
$message->subscriberLists()->attach([1, 2, 3]);
// Or sync
$message->subscriberLists()->sync([1, 2, 3]);
Get Message Lists #
$lists = $message->subscriberLists;
foreach ($lists as $list) {
echo $list->name;
}
Target Specific Lists #
// Get all active subscribers from selected lists
$subscribers = Subscriber::whereHas('subscriberLists', function($q) use ($listIds) {
$q->whereIn('subscriber_list_id', $listIds);
})
->whereHas('status', function($q) {
$q->where('code', 'active');
})
->get();
Import/Export #
Export Subscribers #
// Get all subscribers for a list
$subscribers = $list->subscribers()
->with('status')
->get();
// Format for CSV
$csv = [];
foreach ($subscribers as $subscriber) {
$csv[] = [
'email' => $subscriber->email,
'first_name' => $subscriber->first_name,
'last_name' => $subscriber->last_name,
'status' => $subscriber->status->name,
'subscribed_at' => $subscriber->pivot->created_at
];
}
Import Subscribers #
// Import from CSV and assign to list
use Albrightlabs\Campaign\Models\SubscriberImport;
$import = new SubscriberImport;
$import->organization_id = $org->id;
$import->list_ids = [$list->id];
$import->import($csvFilePath);
List Strategies #
By Interest/Topic #
$lists = [
['name' => 'Product Updates', 'code' => 'product-updates'],
['name' => 'Marketing News', 'code' => 'marketing'],
['name' => 'Tech Blog', 'code' => 'tech-blog']
];
By Customer Type #
$lists = [
['name' => 'Free Plan Users', 'code' => 'free-users'],
['name' => 'Paid Customers', 'code' => 'paid-customers'],
['name' => 'Enterprise', 'code' => 'enterprise']
];
By Engagement Level #
$lists = [
['name' => 'Highly Engaged', 'code' => 'high-engagement'],
['name' => 'Moderate Engagement', 'code' => 'med-engagement'],
['name' => 'Low Engagement', 'code' => 'low-engagement']
];
Scopes & Helpers #
Active Subscribers Scope #
public function scopeActive($query)
{
return $query->whereHas('status', function($q) {
$q->where('code', 'active');
});
}
// Usage
$activeCount = $list->subscribers()->active()->count();
Display Name Accessor #
public function getDisplayNameAttribute()
{
return "{$this->name} ({$this->count_active})";
}
// Usage
echo $list->display_name; // "Newsletter (1,523)"
Backend UI #
List Controller #
Location: Controllers/Lists.php
class Lists extends Controller
{
public $implement = [
\Backend\Behaviors\FormController::class,
\Backend\Behaviors\ListController::class
];
public $formConfig = 'config_form.yaml';
public $listConfig = 'config_list.yaml';
}
List Columns #
# config_list.yaml
columns:
name:
label: List Name
searchable: true
code:
label: Code
searchable: true
count_active:
label: Active Subscribers
type: number
align: right
created_at:
label: Created
type: date
Form Fields #
# config_form.yaml
fields:
name:
label: List Name
required: true
code:
label: Code
required: true
comment: Unique identifier (lowercase, no spaces)
description:
label: Description
type: textarea
size: small
count_subscriber:
label: Total Subscribers
type: number
disabled: true
context: update
count_active:
label: Active Subscribers
type: number
disabled: true
context: update
Best Practices #
Naming Conventions #
- Use descriptive, clear names
- Keep codes lowercase with hyphens
- Avoid special characters in codes
- Document list purpose in description
Organization #
- Create lists for each campaign type
- Segment by user behavior/engagement
- Maintain separate transactional lists
- Archive unused lists
Performance #
- Cache subscriber counts
- Update counts asynchronously
- Use scopes for common queries
- Index foreign keys and codes
Compliance #
- Provide clear list descriptions
- Allow easy unsubscribe from all lists
- Track subscription source
- Honor user preferences
Troubleshooting #
Subscriber Count Mismatch #
// Recalculate from database
$actualCount = $list->subscribers()->count();
if ($actualCount != $list->count_subscriber) {
$list->updateSubscriberCount();
}
Duplicate Code Error #
// Check existing codes
$existing = SubscriberList::where('code', $code)->exists();
// Generate unique code
$baseCode = str_slug($name);
$code = $baseCode;
$counter = 1;
while (SubscriberList::where('code', $code)->exists()) {
$code = "{$baseCode}-{$counter}";
$counter++;
}
Subscribers Not Receiving Messages #
// Verify list assignment
$lists = $message->subscriberLists->pluck('id')->toArray();
// Check subscriber is in list and active
$subscriber = Subscriber::find($id);
$inList = $subscriber->subscriberLists()
->whereIn('subscriber_list_id', $lists)
->exists();
$isActive = $subscriber->status->code == 'active';