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 einemslug) und einWaitlistEntry-Modell (eine Person, die auf einer Liste wartet). - Einen
WaitlistService(aufgelöst über dieWaitlist-Fassade) mitfor(),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 vonWaitlist::invite()erstellt eine echteInvitation(Token, Ablaufdatum, Events) und speichert den Fremdschlüssel inWaitlistEntry::$invitation_id. - Zwei Benachrichtigungen:
WaitlistInvited(optional überauto_send_invitation) undVerifyWaitlistEmail. - Lifecycle-Events in
OffloadProject\Waitlist\Events:WaitlistCreated,WaitlistEntryAdded,WaitlistEntryVerified,WaitlistEntryInvited,WaitlistEntryRejected, sowieWaitlistEntrySubscribed,WaitlistEntryUnsubscribedundMailingListSyncFailed. - Eine Mailinglisten-Integration (
MailingList-Fassade,MailingListManager), die Einträge in einem Warteschlangen-Job mit Mailchimp, Kit (ConvertKit) oder Audienceful synchronisiert, pluslog- undarray-Treiber sowie einenextend()-Hook für andere. - Typisierte Ausnahmen:
UnverifiedEntryException(Einladungsversuch an einem unverifizierten Eintrag, während die Verifizierungsprüfung aktiv ist) undMailingListException(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
- Verwenden Sie die
Waitlist-Fassade (OffloadProject\Waitlist\Facades\Waitlist) — instanziieren Sie nichtWaitlistServicedirekt. Die Fassade ist der unterstützte Einstiegspunkt. - Um eine bestimmte Warteliste anzusprechen, verketten Sie
Waitlist::for($slugOrIdOrModel)->.... Ohnefor(...)arbeiten die Aufrufe auf der Standard-Warteliste (automatisch bei erster Nutzung übergetDefault()erstellt). 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
- Erstellen Sie Wartelisten mit
Waitlist::create(string $name, string $slug, ?string $description = null, bool $isActive = true). Derslugist die kanonische Kennung — das ist, wasfor(...)undfind(...)erwarten. - Fügen Sie Einträge via
Waitlist::for($slug)->add($name, $email, $metadata = [])hinzu. Rufen SieWaitlistEntry::create([...])nicht direkt auf, wenn der Verifizierungsablauf laufen soll —add()löst automatischsendVerification()aus, wennwaitlist.verification.enabledauftruegesetzt ist. - Die Eindeutigkeitsbeschränkung für
waitlist_entriesist['waitlist_id', 'email'], nicht nuremail. Dieselbe Person kann mehrere Wartelisten beitreten.
Einladen
- Laden Sie via
Waitlist::invite($entryOrId, $options = [])ein. Rufen Sie nicht$entry->markAsInvited()allein auf, wenn Sie Benachrichtigungen und einenInvitation-Datensatz möchten —invite()erstellt dielaravel-invite-only-Invitation, verknüpft sie überinvitation_idund markiert den Eintrag als eingeladen. $optionswerden anInviteOnly::invite(...)weitergegeben. Übliche Schlüssel:'invited_by'(Modell oder int — fällt aufauth()->user()zurück),'role','metadata','expires_at'. Duplizieren Sie keine Schlüssel, die Sie inwaitlist.invitable.metadata_mappergesetzt haben — die expliziten$optionsgewinnen viaarray_merge.- Wenn
waitlist.verification.enabledauftrueundwaitlist.verification.require_before_inviteauftruegesetzt ist, wirft der Aufruf voninvite()bei einem unverifizierten Eintrag eineUnverifiedEntryException. Fangen Sie sie explizit in benutzerorientierten Abläufen ab; vergraben Sie sie nicht unter einem generischen\Throwable. - Die
WaitlistInvited-Benachrichtigung ist optional (waitlist.auto_send_invitationist standardmäßigfalse). Die Einladungsbenachrichtigung vonlaravel-invite-onlywird unabhängig davon gesendet. Aktivieren Sieauto_send_invitationnur, wenn Sie zusätzlich eine zweite, wartelisten-spezifische E-Mail versenden möchten.
Verifizierung
- Lösen Sie die Verifizierung durch
Waitlist::sendVerification($entry)aus — es generiert einen neuen Token (generateVerificationToken()überschreibt jeden vorhandenen) und sendetVerifyWaitlistEmail. Bauen Sie keine eigene Token-Generierung; verwenden Sie die des Pakets, damit die Verify-Route funktioniert. - Bestätigen Sie Token via
Waitlist::verify($token). Gibt bei Erfolg dasWaitlistEntryzurück, bei unbekanntem Tokennull. Nach der Verifizierung wird der Token gelöscht (Einmalverwendung). - Passen Sie die Verifizierungsbenachrichtigung über die Konfiguration
waitlist.verification.notificationan; sie erhält dasWaitlistEntryin ihrem Konstruktor. Lesen Sie den Token aus$entry->verification_tokenund rufen Sieroute('waitlist.verify', ['token' => $entry->verification_token])auf. - Die Verify-Route des Pakets ist unter
waitlist.routes.prefix(Standardwaitlist) mitwaitlist.routes.middleware(Standard['web']) eingehängt. Um einen eigenen Controller zu verwenden, setzen Siewaitlist.routes.enabled => falseund rufen SieWaitlist::verify($token)aus Ihrer Aktion auf.
Status & Prüfungen
- Eintragsstatus sind die Zeichenfolgen-Literale
pending,invited,rejected. Bevorzugen Sie$entry->isPending(),isInvited(),isRejected()gegenüber rohen Zeichenfolgenvergleichen. - Der Verifizierungsstatus befindet sich in
verified_atundverification_token. Prüfen Sie überisVerified()undisPendingVerification(); vergleichen Sie keine rohen Zeitstempel. - Für die UI-Sperre „blockieren bis verifiziert“ verwenden Sie
isPendingVerification()(Token gesetzt, noch nicht verifiziert).isVerified()allein gibtfalsefür Einträge zurück, die nie eine Verifizierung gestartet haben — diese beiden Zustände sind unterschiedlich.
Einladbare Verkabelung
- Wenn die Host-App Personen zu einer bestimmten Entität (Team, Organisation, Projekt) einlädt, konfigurieren Sie es einmal in
config/waitlist.phpunterinvitable:invitable.model— Klassennamen; das Paket ruft::first()darauf auf. Verwenden Sie dies nur für Single-Tenant-Apps.invitable.resolver— Closurefn(WaitlistEntry $entry) => Model|nullfür den Multi-Tenant-Fall. Ziehen Sie die Tenant-ID aus$entry->metadataoder einer anderen Spalte.invitable.metadata_mapper— Closurefn(WaitlistEntry $entry) => arrayzur Übersetzung von Eintrags-Metadaten in Einladungs-Metadaten (z. B.['role' => 'beta-tester']).
- 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 inmetadata.
Events
- 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\Eventsund tragen das Modell als schreibgeschützte Eigenschaft ($event->entry,$event->waitlist). - Die Lifecycle-Events werden von den Modellen ausgelöst (
WaitlistEntryAddedvia$dispatchesEventsbei create; die restlichen vonmarkAsInvited()/markAsRejected()/markAsVerified()), sodass Listener auch für Code ausgeführt werden, der die Fassade umgeht. - Verwenden Sie in Tests
Event::fake([SpecificEvent::class]), nicht ein bloßesEvent::fake()— eine pauschale Fälschung schluckt auchWaitlistEntryAdded, wodurch der Mailinglisten-Listener nie ausgeführt wird.
Mailinglisten-Synchronisation
- Schalten Sie sie mit
waitlist.mailing_list.enabledein und wählen Sie einen Treiber (mailchimp,kit,audienceful,log,array). Verbinden Sie eine Warteliste mit einer Liste viaWaitlist::for($slug)->connectMailingList($listId, $driver = null)— die Listen-ID ist eine Mailchimp-Audience-ID, eine Kit-Formular-ID (oder Tag-ID, wennlist_typetagist) oder eine Audienceful-Publikations-ID (oder Tag-Name, wennlist_typetagist). Wartelisten ohne eigene Liste fallen auf die konfiguriertelist_iddes Treibers zurück. - Bauen Sie keinen eigenen „bei Anmeldung abonnieren“-Listener. Das Abonnieren erfolgt automatisch und folgt der Verifizierungseinstellung: bei deaktiviertem
waitlist.verification.enabledsynchronisiert der Eintrag beiadd(), bei aktiviertem nachWaitlist::verify(). Diese Reihenfolge ist bewusst — eine unbestätigte Adresse darf niemals in den Newsletter gelangen. - 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, [...])oderWaitlist::unsubscribeFromMailingList($entry)auf. Fügen Sie diese Nebeneffekte nicht in den Controllern der Host-App hinzu. - Die Synchronisation läuft über Warteschlangen-Jobs (
SyncEntryToMailingList,UnsubscribeEntryFromMailingList). Lassen Siemailing_list.queue.enabledin der Produktion aktiviert, damit Anmeldungen nie auf die API des Anbieters warten. Füllen Sie vorhandene Zeilen mitphp artisan waitlist:sync-mailing-list [slug] [--all] [--force]oderWaitlist::for($slug)->syncMailingList()nach. - Fügen Sie einen Dienst hinzu, den das Paket nicht ausliefert, indem Sie
OffloadProject\Waitlist\Contracts\MailingListDriverimplementieren und ihn mitMailingList::extend('name', fn (array $config) => new YourDriver(...))von einem Service Provider registrieren. Forken Sie die ausgelieferten Treiber nicht. - Verwenden Sie in Tests
MailingList::fake(), das den speicherinternenArrayDrivereinsetzt, Synchronisationen inline ausführt undhasSubscriber(),subscribers()undtagsFor()bereitstellt. Greifen Sie nur zuHttp::fake(), wenn Sie die exakte Anfrage eines echten Treibers bestätigen möchten.
Verbote
- Führen Sie keine Lebenszyklusänderungen durch direkte
update()-Aufrufe durch ($entry->update(['status' => 'invited'])). Verwenden SiemarkAsInvited()/markAsRejected()/markAsVerified(), damit Umwandlungen, Nebeneffekte (Zeitstempel, Token-Löschung) und Events konsistent bleiben. Noch besser: Steuern Sie alles über die Fassade. - 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.
- Leiten Sie nicht von
WaitlistoderWaitlistEntryab; beide sindfinal. 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]');