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';

← Messages | Next: Subscribers →