mydnic/laravel-subscribers
A Laravel package for managing newsletter subscribers and mailing lists, with database migrations, models, and a simple API for handling subscriptions, unsubscriptions, and email opt-ins in your application.
Install and scaffold in four commands:
composer require mydnic/laravel-subscribers
php artisan vendor:publish --provider="Mydnic\Subscribers\SubscribersServiceProvider"
php artisan migrate
If the provider name differs in your version, run php artisan vendor:publish with no arguments and pick the mydnic/laravel-subscribers entry interactively — the package publishes config, migrations, and usually views/routes under separate tags (e.g. --tag=subscribers-config, --tag=subscribers-views). Check vendor/mydnic/laravel-subscribers/src right after install; that directory is the real documentation. Read in this order:
src/Models/ — the subscriber and campaign/email entities you'll be querying and extending.routes/ (or src/Http/) — the built-in subscribe/confirm/unsubscribe/tracking endpoints. These are your integration surface.config/subscribers.php — table names, mail settings, model bindings, tracking toggles.database/migrations/ — column names and nullability, which dictate what you can safely add.First use case to validate the install end-to-end: publish the config and views, drop the package's subscribe form (or POST to its subscribe route) on a staging page, submit an email, confirm the record appears in the subscribers table, and confirm the confirmation/welcome mail lands in Mailtrap or log driver. If that loop works, you know migrations, routing, mail config, and model bindings are all wired correctly before you touch campaigns.
Treat the package as a domain layer, not a UI kit. Its routes and Blade views are convenient for a marketing landing page, but in an existing app you'll usually keep your own controller and call the package's models/actions directly:
// app/Http/Controllers/NewsletterController.php
public function store(Request $request)
{
$request->validate(['email' => ['required', 'email']]);
Subscriber::firstOrCreate(
['email' => strtolower($request->email)],
['source' => 'footer-form', 'status' => 'pending']
);
return back()->with('status', 'Check your inbox to confirm.');
}
Using firstOrCreate with a normalized email makes re-submissions idempotent — critical because the package's unique index will otherwise throw on double clicks.
Double opt-in as the default, single opt-in as the exception. Wire confirmation through the package's signed/tokenized confirm route and only mark a subscriber active on that hit. If your product genuinely needs instant access (e.g. gated PDF download), branch explicitly rather than disabling confirmation globally:
if (config('subscribers.double_opt_in')) {
// send confirmation, status = pending
} else {
// status = active, send welcome
}
Campaign sending belongs on the queue, never in a request. Build campaigns as records first (subject, body, target segment), then dispatch a job that chunks subscribers and sends per-batch. This gives you pause/resume, retries, and per-recipient failure isolation:
Bus::chain(
Subscriber::where('status', 'active')->pluck('id')
->chunk(500)
->map(fn ($ids) => new SendCampaignBatch($campaign, $ids))
->all()
)->dispatch();
Hook into lifecycle with model events, not by forking. Map new subscribers into your CRM, tag them by signup source, or fire an internal Slack notification from an observer:
Subscriber::created(fn ($s) => SyncToCrm::dispatch($s));
Anything the package doesn't expose an event for, prefer a booted() observer over editing vendor files.
Segment via your own columns + scopes. Small newsletter packages rarely ship a query builder. Add source, tags (json), confirmed_at, unsubscribed_at columns in your own migration and compose scopes (active(), fromSource('pricing-page')). This keeps you one composer update away from breakage instead of entangled in package internals.
Route the package's mail through your existing transport. Set the mailer in config/subscribers.php rather than in config/mail.php, so campaign traffic can use a separate provider/domain from transactional mail. This is the single highest-value config change for deliverability: newsletter blasts on a dedicated subdomain protect password resets from reputation damage.
Override views, don't rewrite them. Publish views to resources/views/vendor/... and edit there. Same for any layout the package's mail templates extend — keep your brand header/footer in one partial so campaign emails and transactional emails match.
Testing. Mail::fake() + Queue::fake(), assert Subscriber::assertDatabaseHas(...) after hitting your subscribe endpoint, and assert the confirmation mailable was queued. For unsubscribe, assert the route works for valid tokens and 404/403s for tampered ones.
config/subscribers.php run php artisan config:clear. A stale cached config is the most common "my tracking toggle did nothing" cause.subscribers or newsletter_subscribers table, rename via config before migrating. Renaming later means hand-writing a migration and rewriting foreign keys.List-Unsubscribe and List-Unsubscribe-Post header; Gmail and Yahoo now expect it for bulk senders.composer.json ("mydnic/laravel-subscribers": "^1.0"), don't rely on dev-main, and check the repo's issue/commit cadence before betting a launch on it. If it goes quiet, the escape hatch is the database schema: your subscribers are plain rows, so you can swap the sending layer for Laravel's native Mail + a Mailable and lose almost nothing.->delay() or a rate-limited job middleware when chunking; a 10k-recipient blast that ignores provider limits silently drops mail.[email protected] and [email protected] are the same mailbox but different unique keys unless you lowercase at the boundary. Do it in a mutator so every entry path (form, API, import) is covered.log and inspect storage/logs/laravel.log for the rendered MIME before involving a real provider. If mail vanishes, check failed_jobs first — a broken view throws inside the queued job, not at dispatch time.How can I help you explore Laravel packages today?