offload-project/laravel-waitlist

Konventionen und APIs für das Paket offload-project/laravel-waitlist — mehrere Wartelisten, Eintragsstatus-Verfolgung, optionale E-Mail-Verifizierung, Lifecycle-Events, Mailinglisten-Synchronisation (Mailchimp/Kit/Audienceful) und Brücke zu laravel-invite-only.

Was ist laravel-waitlist?

Dieses Skill-Dokument beschreibt die Nutzung des Laravel-Pakets `offload-project/laravel-waitlist`, das die Verwaltung einer oder mehrerer Wartelisten mit detaillierter Statusverfolgung, optionalem E-Mail-Verifizierungs-Workflow und automatischer Synchronisation mit E-Mail-Marketing-Diensten wie Mailchimp, Kit (ConvertKit) und Audienceful ermöglicht. Es definiert klare Regeln für die Verwendung der `Waitlist`- und `MailingList`-Fassaden und vermeidet direkte Aufrufe von Modellmethoden. Der Entwickler findet Anleitungen zur Erstellung von Wartelisten und Einträgen, zur Durchführung von Einladungen (inklusive optionaler Integration mit `offload-project/laravel-invite-only`), zum Verifizierungsprozess, zur Konfiguration von Mailinglisten und zum Reagieren auf Lifecycle-Events. Das Skill ist für Laravel 11/12/13 mit PHP 8.3+ optimiert und wird in Projekten angewendet, die das Paket in ihrer `composer.json` haben.

Funktioniert mit✓Claude Code~Codex CLI✓Cursor
npx skills add https://github.com/offload-project/laravel-waitlist/tree/HEAD/skills

Installed? Explore more Produktivität & Zusammenarbeit skills: steipete/gemini, steipete/gh-issues, steipete/skill-creator · View all 6 →

In Ihrer bevorzugten KI fragen

Öffnet einen neuen Chat, in dem dieser Agent-Skill bereits geladen ist.

Dokumentation

Kontext

offload-project/laravel-waitlist ist ein Laravel 11/12/13-Paket (PHP 8.3+) zur Verwaltung einer oder mehrerer Wartelisten. Es bietet:

  • Ein Waitlist-Eloquent-Modell (eine benannte Warteliste mit einem slug) und ein WaitlistEntry-Modell (eine Person, die auf einer Liste wartet).
  • Einen WaitlistService (aufgelöst über die Waitlist-Fassade) mit for(), create(), add(), invite(), reject(), sendVerification(), verify() sowie Abfrage- und Zähl-Hilfsmethoden.
  • Optionalen E-Mail-Verifizierungs-Workflow mit einer veröffentlichten Route /waitlist/verify/{token}.
  • Optionale Brücke zu offload-project/laravel-invite-only: Ein Aufruf von Waitlist::invite() erstellt eine echte Invitation (Token, Ablaufdatum, Events) und speichert den Fremdschlüssel in WaitlistEntry::$invitation_id.
  • Zwei Benachrichtigungen: WaitlistInvited (optional über auto_send_invitation) und VerifyWaitlistEmail.
  • Lifecycle-Events in OffloadProject\Waitlist\Events: WaitlistCreated, WaitlistEntryAdded, WaitlistEntryVerified, WaitlistEntryInvited, WaitlistEntryRejected, sowie WaitlistEntrySubscribed, WaitlistEntryUnsubscribed und MailingListSyncFailed.
  • Eine Mailinglisten-Integration (MailingList-Fassade, MailingListManager), die Einträge in einem Warteschlangen-Job mit Mailchimp, Kit (ConvertKit) oder Audienceful synchronisiert, plus log- und array-Treiber sowie einen extend()-Hook für andere.
  • Typisierte Ausnahmen: UnverifiedEntryException (Einladungsversuch an einem unverifizierten Eintrag, während die Verifizierungsprüfung aktiv ist) und MailingListException (Treiber-/Anmeldeinformations-/Listen-ID-/API-Fehler).

Wenden Sie dieses Skill an, wenn Sie in einer Laravel-App arbeiten, die offload-project/laravel-waitlist in der composer.json hat, oder wenn der Benutzer um Hilfe mit Waitlist, WaitlistEntry, den Waitlist- oder MailingList-Fassaden oder Wartelisten-Abläufen in diesem Paket bittet.

Regeln

Fassaden-Nutzung

  1. Verwenden Sie die Waitlist-Fassade (OffloadProject\Waitlist\Facades\Waitlist) — instanziieren Sie nicht WaitlistService direkt. Die Fassade ist der unterstützte Einstiegspunkt.
  2. Um eine bestimmte Warteliste anzusprechen, verketten Sie Waitlist::for($slugOrIdOrModel)->.... Ohne for(...) arbeiten die Aufrufe auf der Standard-Warteliste (automatisch bei erster Nutzung über getDefault() erstellt).
  3. Waitlist::for(...) ändert den internen Zustand eines Singleton-Dienstes. Rufen Sie bei langlebigen Prozessen (Queue-Worker, Octane) for(...) für jeden Vorgang auf, anstatt sich auf einen vorherigen Kontext zu verlassen.

Wartelisten vs. Einträge

  1. Erstellen Sie Wartelisten mit Waitlist::create(string $name, string $slug, ?string $description = null, bool $isActive = true). Der slug ist die kanonische Kennung — das ist, was for(...) und find(...) erwarten.
  2. Fügen Sie Einträge via Waitlist::for($slug)->add($name, $email, $metadata = []) hinzu. Rufen Sie WaitlistEntry::create([...]) nicht direkt auf, wenn der Verifizierungsablauf laufen soll — add() löst automatisch sendVerification() aus, wenn waitlist.verification.enabled auf true gesetzt ist.
  3. Die Eindeutigkeitsbeschränkung für waitlist_entries ist ['waitlist_id', 'email'], nicht nur email. Dieselbe Person kann mehrere Wartelisten beitreten.

Einladen

  1. Laden Sie via Waitlist::invite($entryOrId, $options = []) ein. Rufen Sie nicht $entry->markAsInvited() allein auf, wenn Sie Benachrichtigungen und einen Invitation-Datensatz möchten — invite() erstellt die laravel-invite-only-Invitation, verknüpft sie über invitation_id und markiert den Eintrag als eingeladen.
  2. $options werden an InviteOnly::invite(...) weitergegeben. Übliche Schlüssel: 'invited_by' (Modell oder int — fällt auf auth()->user() zurück), 'role', 'metadata', 'expires_at'. Duplizieren Sie keine Schlüssel, die Sie in waitlist.invitable.metadata_mapper gesetzt haben — die expliziten $options gewinnen via array_merge.
  3. Wenn waitlist.verification.enabled auf true und waitlist.verification.require_before_invite auf true gesetzt ist, wirft der Aufruf von invite() bei einem unverifizierten Eintrag eine UnverifiedEntryException. Fangen Sie sie explizit in benutzerorientierten Abläufen ab; vergraben Sie sie nicht unter einem generischen \Throwable.
  4. Die WaitlistInvited-Benachrichtigung ist optional (waitlist.auto_send_invitation ist standardmäßig false). Die Einladungsbenachrichtigung von laravel-invite-only wird unabhängig davon gesendet. Aktivieren Sie auto_send_invitation nur, wenn Sie zusätzlich eine zweite, wartelisten-spezifische E-Mail versenden möchten.

Verifizierung

  1. Lösen Sie die Verifizierung durch Waitlist::sendVerification($entry) aus — es generiert einen neuen Token (generateVerificationToken() überschreibt jeden vorhandenen) und sendet VerifyWaitlistEmail. Bauen Sie keine eigene Token-Generierung; verwenden Sie die des Pakets, damit die Verify-Route funktioniert.
  2. Bestätigen Sie Token via Waitlist::verify($token). Gibt bei Erfolg das WaitlistEntry zurück, bei unbekanntem Token null. Nach der Verifizierung wird der Token gelöscht (Einmalverwendung).
  3. Passen Sie die Verifizierungsbenachrichtigung über die Konfiguration waitlist.verification.notification an; sie erhält das WaitlistEntry in ihrem Konstruktor. Lesen Sie den Token aus $entry->verification_token und rufen Sie route('waitlist.verify', ['token' => $entry->verification_token]) auf.
  4. Die Verify-Route des Pakets ist unter waitlist.routes.prefix (Standard waitlist) mit waitlist.routes.middleware (Standard ['web']) eingehängt. Um einen eigenen Controller zu verwenden, setzen Sie waitlist.routes.enabled => false und rufen Sie Waitlist::verify($token) aus Ihrer Aktion auf.

Status & Prüfungen

  1. Eintragsstatus sind die Zeichenfolgen-Literale pending, invited, rejected. Bevorzugen Sie $entry->isPending(), isInvited(), isRejected() gegenüber rohen Zeichenfolgenvergleichen.
  2. Der Verifizierungsstatus befindet sich in verified_at und verification_token. Prüfen Sie über isVerified() und isPendingVerification(); vergleichen Sie keine rohen Zeitstempel.
  3. Für die UI-Sperre „blockieren bis verifiziert“ verwenden Sie isPendingVerification() (Token gesetzt, noch nicht verifiziert). isVerified() allein gibt false für Einträge zurück, die nie eine Verifizierung gestartet haben — diese beiden Zustände sind unterschiedlich.

Einladbare Verkabelung

  1. Wenn die Host-App Personen zu einer bestimmten Entität (Team, Organisation, Projekt) einlädt, konfigurieren Sie es einmal in config/waitlist.php unter invitable:
    • invitable.model — Klassennamen; das Paket ruft ::first() darauf auf. Verwenden Sie dies nur für Single-Tenant-Apps.
    • invitable.resolver — Closure fn(WaitlistEntry $entry) => Model|null für den Multi-Tenant-Fall. Ziehen Sie die Tenant-ID aus $entry->metadata oder einer anderen Spalte.
    • invitable.metadata_mapper — Closure fn(WaitlistEntry $entry) => array zur Übersetzung von Eintrags-Metadaten in Einladungs-Metadaten (z. B. ['role' => 'beta-tester']).
  2. Hartcodieren Sie keine einladbare Entität pro Aufrufort. Wenn verschiedene Abläufe unterschiedliche einladbare Entitäten benötigen, verwenden Sie die resolver-Closure mit einem Unterscheidungsmerkmal in metadata.

Events

  1. Haken Sie sich mit den paketeigenen Events in den Lebenszyklus ein, anstatt die Fassade zu umschließen oder die Tabelle abzufragen. Sie befinden sich in OffloadProject\Waitlist\Events und tragen das Modell als schreibgeschützte Eigenschaft ($event->entry, $event->waitlist).
  2. Die Lifecycle-Events werden von den Modellen ausgelöst (WaitlistEntryAdded via $dispatchesEvents bei create; die restlichen von markAsInvited() / markAsRejected() / markAsVerified()), sodass Listener auch für Code ausgeführt werden, der die Fassade umgeht.
  3. Verwenden Sie in Tests Event::fake([SpecificEvent::class]), nicht ein bloßes Event::fake() — eine pauschale Fälschung schluckt auch WaitlistEntryAdded, wodurch der Mailinglisten-Listener nie ausgeführt wird.

Mailinglisten-Synchronisation

  1. Schalten Sie sie mit waitlist.mailing_list.enabled ein und wählen Sie einen Treiber (mailchimp, kit, audienceful, log, array). Verbinden Sie eine Warteliste mit einer Liste via Waitlist::for($slug)->connectMailingList($listId, $driver = null) — die Listen-ID ist eine Mailchimp-Audience-ID, eine Kit-Formular-ID (oder Tag-ID, wenn list_type tag ist) oder eine Audienceful-Publikations-ID (oder Tag-Name, wenn list_type tag ist). Wartelisten ohne eigene Liste fallen auf die konfigurierte list_id des Treibers zurück.
  2. Bauen Sie keinen eigenen „bei Anmeldung abonnieren“-Listener. Das Abonnieren erfolgt automatisch und folgt der Verifizierungseinstellung: bei deaktiviertem waitlist.verification.enabled synchronisiert der Eintrag bei add(), bei aktiviertem nach Waitlist::verify(). Diese Reihenfolge ist bewusst — eine unbestätigte Adresse darf niemals in den Newsletter gelangen.
  3. Für alles über das Abonnieren hinaus — Tagging bei Einladung, Entfernen bei Ablehnung, Verschieben zwischen Listen — hören Sie auf die Lifecycle-Events und rufen Sie MailingList::tagEntry($entry, [...]) oder Waitlist::unsubscribeFromMailingList($entry) auf. Fügen Sie diese Nebeneffekte nicht in den Controllern der Host-App hinzu.
  4. Die Synchronisation läuft über Warteschlangen-Jobs (SyncEntryToMailingList, UnsubscribeEntryFromMailingList). Lassen Sie mailing_list.queue.enabled in der Produktion aktiviert, damit Anmeldungen nie auf die API des Anbieters warten. Füllen Sie vorhandene Zeilen mit php artisan waitlist:sync-mailing-list [slug] [--all] [--force] oder Waitlist::for($slug)->syncMailingList() nach.
  5. Fügen Sie einen Dienst hinzu, den das Paket nicht ausliefert, indem Sie OffloadProject\Waitlist\Contracts\MailingListDriver implementieren und ihn mit MailingList::extend('name', fn (array $config) => new YourDriver(...)) von einem Service Provider registrieren. Forken Sie die ausgelieferten Treiber nicht.
  6. Verwenden Sie in Tests MailingList::fake(), das den speicherinternen ArrayDriver einsetzt, Synchronisationen inline ausführt und hasSubscriber(), subscribers() und tagsFor() bereitstellt. Greifen Sie nur zu Http::fake(), wenn Sie die exakte Anfrage eines echten Treibers bestätigen möchten.

Verbote

  1. Führen Sie keine Lebenszyklusänderungen durch direkte update()-Aufrufe durch ($entry->update(['status' => 'invited'])). Verwenden Sie markAsInvited() / markAsRejected() / markAsVerified(), damit Umwandlungen, Nebeneffekte (Zeitstempel, Token-Löschung) und Events konsistent bleiben. Noch besser: Steuern Sie alles über die Fassade.
  2. Bearbeiten Sie nicht die veröffentlichten Migrationen, um Spalten hinzuzufügen — schreiben Sie eine Folge-Migration in der Host-App. Das Paket kann in zukünftigen Versionen Spalten hinzufügen und wird das veröffentlichte Schema voraussetzen.
  3. Leiten Sie nicht von Waitlist oder WaitlistEntry ab; beide sind final. Fügen Sie Verhalten auf der Host-App-Seite über Listener auf den Paket-Events hinzu oder indem Sie den Dienst über eine benutzerdefinierte Bindung in Ihrem App-Container erweitern.

Beispiele

Einfache Warteliste (keine Konfiguration)

use OffloadProject\Waitlist\Facades\Waitlist;

$entry = Waitlist::add('John Doe', '[email protected]', ['source' => 'landing-page']);

Waitlist::invite($entry, [
    'invited_by' => auth()->user(),
    'expires_at' => now()->addDays(14),
]);

Mehrere Wartelisten

Waitlist::create('Beta Program', 'beta');
Waitlist::create('VIP Access', 'vip');

Waitlist::for('beta')->add('Jane Smith', '[email protected]');
Waitlist::for('vip')->add('Bob Wilson', '[email protected]');

$pendingBeta = Waitlist::for('beta')->getPending();
$vipCount    = Waitlist::for('vip')->count();

Verifizierungsablauf

// config/waitlist.php
'verification' => [
    'enabled' => true,
    'require_before_invite' => true,
    'notification' => \OffloadProject\Waitlist\Notifications\VerifyWaitlistEmail::class,
],
use OffloadProject\Waitlist\Exceptions\UnverifiedEntryException;
use OffloadProject\Waitlist\Facades\Waitlist;

$entry = Waitlist::add('John Doe', '[email protected]');
// Bestätigungs-E-Mail wird automatisch gesendet.

try {
    Waitlist::invite($entry);
} catch (UnverifiedEntryException) {
    return back()->withErrors(['email' => 'Bitte bestätigen Sie zuerst Ihre E-Mail-Adresse.']);
}

Einladbare Modellverkabelung (Team-Einladungen)

// config/waitlist.php
'invitable' => [
    'model'    => null,
    'resolver' => fn (\OffloadProject\Waitlist\Models\WaitlistEntry $entry) =>
        \App\Models\Team::find($entry->metadata['team_id'] ?? null),
    'metadata_mapper' => fn (\OffloadProject\Waitlist\Models\WaitlistEntry $entry) => [
        'role' => $entry->metadata['role'] ?? 'member',
    ],
],

Dann:

Waitlist::for('beta')->add('Jane', '[email protected]', [
    'team_id' => $team->id,
    'role'    => 'admin',
]);

Wenn Sie später Waitlist::invite($entry) aufrufen, ist die resultierende laravel-invite-only-Einladung auf dieses Team mit role=admin beschränkt.

Benutzerdefinierte Verifizierungsbenachrichtigung

use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
use OffloadProject\Waitlist\Models\WaitlistEntry;

class CustomVerifyWaitlistEmail extends Notification
{
    public function __construct(public WaitlistEntry $entry) {}

    public function via($notifiable): array
    {
        return ['mail'];
    }

    public function toMail($notifiable): MailMessage
    {
        $url = route('waitlist.verify', ['token' => $this->entry->verification_token]);

        return (new MailMessage)
            ->subject('Bestätigen Sie Ihren Platz auf der Warteliste')
            ->greeting("Hallo {$this->entry->name}!")
            ->action('E-Mail bestätigen', $url);
    }
}
// config/waitlist.php
'verification' => [
    'enabled' => true,
    'notification' => \App\Notifications\CustomVerifyWaitlistEmail::class,
],

Synchronisation von Anmeldungen mit Mailchimp

// config/waitlist.php
'mailing_list' => [
    'enabled' => true,
    'default' => 'mailchimp',
    'drivers' => [
        'mailchimp' => [
            'key' => env('MAILCHIMP_API_KEY'),   // Suffix trägt das Rechenzentrum, z.B. -us14
            'list_id' => env('MAILCHIMP_LIST_ID'),
        ],
    ],
],
// Ein Zielgruppe pro Warteliste (optional — sonst wird die konfigurierte list_id verwendet).
Waitlist::for('beta')->connectMailingList('a1b2c3d4e5');

// Automatisch abonniert, je nach Konfiguration bei add oder nach Verifizierung.
Waitlist::for('beta')->add('Jane Smith', '[email protected]');

Reaktion auf den Lebenszyklus

use Illuminate\Support\Facades\Event;
use OffloadProject\Waitlist\Events\WaitlistEntryInvited;
use OffloadProject\Waitlist\Events\WaitlistEntryRejected;
use OffloadProject\Waitlist\Facades\MailingList;
use OffloadProject\Waitlist\Facades\Waitlist;

Event::listen(fn (WaitlistEntryInvited $event) => MailingList::tagEntry($event->entry, ['invited']));
Event::listen(fn (WaitlistEntryRejected $event) => Waitlist::unsubscribeFromMailingList($event->entry));

Testen eines Mailinglisten-Ablaufs

use OffloadProject\Waitlist\Facades\MailingList;

// Fake einrichten — Einträge werden synchron im Speicher verarbeitet.
MailingList::fake();

$waitlist = Waitlist::for($slug);
$waitlist->connectMailingList('test-list-id');
$entry = $waitlist->add('John', '[email protected]');

// … je nach Verifizierungskonfiguration ggf. verifizieren.
Waitlist::verify($entry->verification_token);

// Bestätigen, dass der Eintrag als Abonnent vorhanden ist.
MailingList::assertHasSubscriber('[email protected]');

Verwandte Skills