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

Newsletter Bundle Laravel Package

ibrows/newsletter-bundle

ibrows/newsletter-bundle is a Symfony bundle that adds newsletter management to PHP applications, helping you organize subscribers and send email campaigns. It offers a starting point for integrating newsletter workflows into existing projects.

View on GitHub
Deep Wiki
Context7

Getting Started

Before anything else: ibrows/newsletter-bundle is a Symfony bundle, not a Laravel package. There is no ServiceProvider, no extra.laravel block, no publishable config, no facade, and no Blade views. Running composer require ibrows/newsletter-bundle inside a Laravel app will pull in Symfony container/bundle contracts you cannot boot. The realistic "getting started" is vendoring it read-only as a specification.

Minimal steps that actually work:

  1. Clone, don't require. git clone into a scratch directory (or composer require --prefer-source in a throwaway Symfony sandbox) so you can read the source without contaminating your app's composer.json.
  2. Read in this order — the subscriber entity (fields: email, status, list association, timestamps), the confirmation/subscription entity or token field, then the controller actions for subscribe / confirm / unsubscribe. That's the whole state machine; everything else is Symfony wiring you will throw away.
  3. Check the declared constraints in its composer.json first — PHP version floor, Symfony version, and whether it requires sonata-project/*, doctrine/orm, or swiftmailer/swiftmailer. If it's SwiftMailer-era, note it and move on; you aren't installing it anyway.
  4. Write down the state transitions on a whiteboard: pending -> confirmed -> unsubscribed, plus expiry and re-subscribe paths. This is the only durable asset in the repo.

First use case to target: double opt-in. Subscribe → persist pending subscriber with a token → send confirmation mail → confirm endpoint flips to confirmed → unsubscribe endpoint records opt-out. Port that loop natively before touching lists or admin UI.

A five-minute Laravel skeleton of the destination:

Schema::create('subscribers', function (Blueprint $table) {
    $table->id();
    $table->string('email')->unique();           // duplicate suppression at the DB layer
    $table->string('status')->default('pending'); // pending|confirmed|unsubscribed
    $table->timestamp('confirmed_at')->nullable();
    $table->timestamp('unsubscribed_at')->nullable();
    $table->timestamps();
});

Schema::create('subscription_tokens', function (Blueprint $table) {
    $table->id();
    $table->foreignId('subscriber_id')->constrained()->cascadeOnDelete();
    $table->string('token', 64)->index();         // indexed, hashed, single-use
    $table->string('purpose');                    // confirm|unsubscribe
    $table->timestamp('expires_at');
    $table->timestamp('consumed_at')->nullable();
});

If that took you ten minutes, you have already replaced 80% of the package's value.

Implementation Patterns

Pattern 1 — Spec mining, not installing. Grep the bundle for its confirmation-token generation and expiry logic and lift the rules, not the code. Typical behaviours worth capturing in tests: tokens expire (24–72h), tokens are single-use, confirming an already-confirmed subscriber is idempotent, subscribing twice while pending resends rather than duplicating.

Pattern 2 — Actions over fat models. Keep the ported state machine framework-agnostic so it's testable and swappable. One action per transition:

final class ConfirmSubscription
{
    public function __construct(private SubscriptionTokens $tokens) {}

    public function handle(string $rawToken): Subscriber
    {
        $token = $this->tokens->findValid($rawToken);          // hashed lookup, not expired, not consumed
        abort_if($token === null, 404);

        return DB::transaction(function () use ($token) {
            $subscriber = $token->subscriber()->lockForUpdate()->first();
            $subscriber->update(['status' => 'confirmed', 'confirmed_at' => now()]);
            $token->update(['consumed_at' => now()]);
            event(new SubscriberConfirmed($subscriber));
            return $subscriber;
        });
    }
}

Pattern 3 — Queued confirmation mail, synchronously-triggered. Public signup should return fast. Dispatch SendConfirmationEmail on the queue; assert dispatch in the subscribe test. The bundle almost certainly sends inline (Symfony mailer call in a controller) — do not port that.

SubscribeToNewsletter::dispatch($email);
// inside the job:
Mail::to($email)->queue(new ConfirmSubscriptionMail($signedUrl));

Pattern 4 — Signed unsubscribe URLs, no login. Unsubscribe must work from an email client with zero friction, and must be a GET-safe, signed URL (Laravel URL::temporarySignedRoute). Pair it with the List-Unsubscribe header so mail clients surface it:

$message->withSymfonyMessage(function ($m) use ($url) {
    $m->getHeaders()->addTextHeader('List-Unsubscribe', "<{$url}>");
    $m->getHeaders()->addTextHeader('List-Unsubscribe-Post', 'List-Unsubscribe=One-Click');
});

Pattern 5 — ESP for sending, app for consent. The bundle's mail transport is its weakest and most framework-bound piece. Keep Laravel as the source of truth for consent (confirmed_at, unsubscribed_at, token audit trail) and push the address into whatever provider you choose (Resend, Postmark, Mailchimp) via a queued listener on SubscriberConfirmed. You get deliverability, bounces, and compliance tooling for free, and the Laravel side stays small.

Pattern 6 — Admin surface from scratch, not ported. The bundle's admin is Twig + likely Sonata. Rebuild as a Filament resource in an afternoon: table with status badge, email search, a bulk "resend confirmation" action, and an export action for the GDPR erasure workflow. Do not attempt to port templates.

Pattern 7 — Lists as a pivot, not a hierarchy. If you need multiple lists, list_id on a list_subscriptions pivot (subscriber_id, list_id, status, confirmed_at) beats a single status column once a user is on two lists. The bundle's model is likely simpler; don't inherit its limitation.

Gotchas and Tips

  • The install will fail or do nothing useful. No Laravel auto-discovery, so if you add it to composer.json you get zero behaviour change plus an unresolvable Symfony dependency graph. Treat any composer require attempt as a spike in a disposable branch.
  • Doctrine schemas don't translate 1:1. If you do port the data model, expect Doctrine annotation/attribute entities that map awkwardly to Eloquent (composite keys, nullable booleans as tinyint, no soft deletes). Redesign for Laravel conventions rather than preserving the schema unless live subscriber data must migrate.
  • Duplicate subscriptions are a concurrency bug, not an edge case. Two tabs, double-click, or a mail scanner prefetching the confirm link will produce duplicate rows or double-confirmations. Enforce unique(email) and wrap transitions in lockForUpdate(). Test with Concurrency or parallel PHPUnit processes if you can.
  • Email security scanners will "confirm" every subscription. Corporate scanners (Outlook Safe Links, Mimecast) GET links in emails. A GET confirm endpoint marks users confirmed who never clicked. Mitigation: require a POST confirmation behind an interstitial, or accept it and treat confirmed as "email is deliverable" rather than "human consented" — and document that choice, because it matters for GDPR audit.
  • Tokens must be hashed at rest and single-use. The bundle may store raw tokens with no consumed_at. Store hash('sha256', $raw), index the hash, and null out on use. Otherwise a leaked DB snapshot is an account-takeover vector for every pending subscriber.
  • Unsubscribe must be instant and unauthenticated-on-purpose. Do not put it behind auth, and do not require a login. A delayed or broken unsubscribe is the fastest route to a spam complaint and, in the EU, a compliance problem. Return the page with a 200 even for an already-unsubscribed address (idempotency).
  • pending rows are a GDPR grey area. Decide retention up front: unconfirmed signups older than 30 days should be purged by a scheduled command. Also ensure erasure requests delete queue payloads (failed_jobs, job_batches) containing the address — the package certainly doesn't do this.
  • Deliverability is not in scope for this package, ever. No SPF/DKIM/DMARC, no bounce or complaint suppression, no reputation handling. Sending marketing mail from your own app IP via SMTP will get you spam-foldered or blacklisted at the first 500 sends. Budget for an ESP from day one.
  • No maintenance backstop. Zero stars, zero dependents, no release activity. There will be no security advisory if a token-handling flaw is found. Once you copy code, you own it — which is why copying rules and writing your own tests beats copying code.
  • Read the license before forking. Non-permissive licensing is a hard stop; check LICENSE in the repo root before any code crosses into your codebase.
  • The genuinely useful takeaway is the requirements checklist, not the code: subscribe form, double opt-in, token expiry, list management, unsubscribe, status tracking. Scope a native Laravel implementation against that list and you'll finish in roughly a sprint with full test coverage and a consent trail you can show a lawyer. That is the real "day-to-day leverage" this package offers a Laravel developer.
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.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager