Contexte
offload-project/laravel-waitlist est un package Laravel 11/12/13 (PHP 8.3+) pour gérer une ou plusieurs listes d'attente. Il fournit :
- Un modèle Eloquent
Waitlist(une liste d'attente nommée avec unslug) et un modèleWaitlistEntry(une personne en attente sur une liste). - Un
WaitlistService(résolu via la façadeWaitlist) avec les méthodesfor(),create(),add(),invite(),reject(),sendVerification(),verify(), et des helpers de requête et de comptage. - Un flux de vérification e-mail optionnel avec une route publiée
/waitlist/verify/{token}. - Un pont optionnel vers
offload-project/laravel-invite-only: l'appel àWaitlist::invite()crée une véritableInvitation(token, expiration, événements) et persiste la clé étrangère surWaitlistEntry::$invitation_id. - Deux notifications :
WaitlistInvited(opt-in viaauto_send_invitation) etVerifyWaitlistEmail. - Des événements du cycle de vie dans
OffloadProject\Waitlist\Events:WaitlistCreated,WaitlistEntryAdded,WaitlistEntryVerified,WaitlistEntryInvited,WaitlistEntryRejected, plusWaitlistEntrySubscribed,WaitlistEntryUnsubscribedetMailingListSyncFailed. - Une intégration de liste de diffusion (
MailingListfaçade,MailingListManager) qui synchronise les inscriptions vers Mailchimp, Kit (ConvertKit) ou Audienceful dans un job en file d'attente, avec des driverslogetarrayet un hookextend()pour d'autres. - Des exceptions typées :
UnverifiedEntryException(tentative d'invitation sur une inscription non vérifiée alors que le garde de vérification est activé) etMailingListException(échecs de driver/identifiants/ID de liste/API).
Appliquez cette compétence lorsque vous travaillez dans une application Laravel qui contient offload-project/laravel-waitlist dans composer.json, ou lorsque l'utilisateur demande de l'aide avec Waitlist, WaitlistEntry, les façades Waitlist ou MailingList, ou les flux de listes d'attente dans ce package.
Règles
Utilisation de la façade
- Utilisez la façade
Waitlist(OffloadProject\Waitlist\Facades\Waitlist) — n'instanciez pasWaitlistServicedirectement. La façade est le point d'entrée officiel. - Pour cibler une liste d'attente spécifique, enchaînez
Waitlist::for($slugOrIdOrModel)->.... Sansfor(...), les appels opèrent sur la liste d'attente par défaut (créée automatiquement lors de la première utilisation viagetDefault()). Waitlist::for(...)modifie l'état interne d'un service singleton. Pour les processus longs (workers de file d'attente, Octane), appelezfor(...)pour chaque opération plutôt que de vous fier à un contexte précédent.
Listes d'attente vs inscriptions
- Créez des listes d'attente avec
Waitlist::create(string $name, string $slug, ?string $description = null, bool $isActive = true). Leslugest l'identifiant canonique — c'est ce quefor(...)etfind(...)attendent. - Ajoutez des inscriptions via
Waitlist::for($slug)->add($name, $email, $metadata = []). N'appelez pasWaitlistEntry::create([...])directement lorsque vous voulez que le flux de vérification s'exécute —add()déclenche automatiquementsendVerification()lorsquewaitlist.verification.enabledest àtrue. - La contrainte d'unicité sur
waitlist_entriesest['waitlist_id', 'email'], pas seulementemail. La même personne peut rejoindre plusieurs listes d'attente.
Invitations
- Invitez via
Waitlist::invite($entryOrId, $options = []). N'appelez pas$entry->markAsInvited()seul lorsque vous voulez des notifications et un enregistrementInvitation—invite()crée l'Invitationdelaravel-invite-only, la lie viainvitation_id, et marque l'inscription comme invitée. $optionsest transmis àInviteOnly::invite(...). Clés courantes :'invited_by'(Modèle ou int — se replie surauth()->user()),'role','metadata','expires_at'. Ne dupliquez pas les clés définies danswaitlist.invitable.metadata_mapper— les$optionsexplicites l'emportent viaarray_merge.- Si
waitlist.verification.enabledest àtrueetwaitlist.verification.require_before_inviteest àtrue, appelerinvite()sur une inscription non vérifiée lanceUnverifiedEntryException. Attrapez-la explicitement dans les flux utilisateur ; ne l'enterrez pas sous un\Throwablegénérique. - La notification
WaitlistInvitedest opt-in (waitlist.auto_send_invitationpar défaut àfalse). La notification d'invitation delaravel-invite-onlyest envoyée quoi qu'il arrive. Activezauto_send_invitationuniquement si vous voulez un second e-mail de marque lié à la liste d'attente.
Vérification
- Déclenchez la vérification via
Waitlist::sendVerification($entry)— elle génère un nouveau token (generateVerificationToken()écrase tout token existant) et envoieVerifyWaitlistEmail. N'écrivez pas votre propre génération de token ; utilisez celui du package pour que la route de vérification continue de fonctionner. - Confirmez les tokens via
Waitlist::verify($token). Retourne leWaitlistEntryen cas de succès,nullpour un token inconnu. Après vérification, le token est effacé (usage unique). - Personnalisez la notification de vérification via la config
waitlist.verification.notification; elle reçoit leWaitlistEntrydans son constructeur. Lisez le token via$entry->verification_tokenet appelezroute('waitlist.verify', ['token' => $entry->verification_token]). - La route de vérification du package est montée sous
waitlist.routes.prefix(par défautwaitlist) avecwaitlist.routes.middleware(par défaut['web']). Pour utiliser votre propre contrôleur, définissezwaitlist.routes.enabled => falseet appelezWaitlist::verify($token)depuis votre action.
Statut et vérifications
- Les statuts des inscriptions sont les chaînes littérales
pending,invited,rejected. Préférez$entry->isPending(),isInvited(),isRejected()plutôt que des comparaisons de chaînes brutes. - L'état de vérification réside dans
verified_atetverification_token. Vérifiez viaisVerified()etisPendingVerification(); ne comparez pas les timestamps bruts. - Pour un blocage "jusqu'à vérification" dans l'interface utilisateur, utilisez
isPendingVerification()(token défini, pas encore vérifié).isVerified()seul retournefalsepour les inscriptions qui n'ont jamais commencé la vérification — ces deux états sont différents.
Câblage de l'entité invitable
- Lorsque l'application hôte invite des personnes à une entité spécifique (Équipe, Organisation, Projet), configurez-la une fois dans
config/waitlist.phpsousinvitable:invitable.model— chaîne de classe ; le package appelle::first()dessus. Utilisez ceci uniquement pour les applications monolocataires.invitable.resolver— closurefn(WaitlistEntry $entry) => Model|nullpour le cas multilocataire. Extrayez l'ID du locataire depuis$entry->metadataou une autre colonne.invitable.metadata_mapper— closurefn(WaitlistEntry $entry) => arraypour traduire les métadonnées de l'inscription en métadonnées d'invitation (par ex.['role' => 'beta-tester']).
- Ne codez pas en dur une entité invitable par site d'appel. Si différents flux nécessitent différentes entités invitables, utilisez la closure
resolveravec un discriminateur dansmetadata.
Événements
- Accrochez-vous au cycle de vie avec les événements du package plutôt que d'envelopper la façade ou d'interroger la table. Ils se trouvent dans
OffloadProject\Waitlist\Eventset chacun porte le modèle comme propriété en lecture seule ($event->entry,$event->waitlist). - Les événements du cycle de vie sont déclenchés depuis les modèles (
WaitlistEntryAddedvia$dispatchesEventsà la création ; les autres depuismarkAsInvited()/markAsRejected()/markAsVerified()), donc les écouteurs s'exécutent même pour le code qui contourne la façade. - Utilisez
Event::fake([SpecificEvent::class])dans les tests, pas unEvent::fake()nu — un faux global avale aussiWaitlistEntryAdded, ce qui empêche l'écouteur de liste de diffusion de s'exécuter.
Synchronisation de liste de diffusion
- Activez-la avec
waitlist.mailing_list.enabledet choisissez un driver (mailchimp,kit,audienceful,log,array). Connectez une liste d'attente à une liste avecWaitlist::for($slug)->connectMailingList($listId, $driver = null)— l'ID de liste est un ID d'audience Mailchimp, un ID de formulaire Kit (ou un ID de tag lorsquelist_typeesttag), ou un ID de publication Audienceful (ou un nom de tag lorsquelist_typeesttag). Les listes d'attente sans liste propre utilisent lelist_idconfiguré dans le driver. - Ne construisez pas votre propre écouteur "s'abonner à l'inscription". L'abonnement est automatique et suit le paramètre de vérification : avec
waitlist.verification.enableddésactivé, l'inscription se synchronise lors deadd(); avec activé, l'inscription se synchronise aprèsWaitlist::verify(). Ce choix est délibéré — une adresse non confirmée ne doit jamais atteindre la newsletter. - Pour tout ce qui dépasse l'abonnement — taguer lors de l'invitation, supprimer lors du rejet, déplacer entre listes — écoutez les événements du cycle de vie et appelez
MailingList::tagEntry($entry, [...])ouWaitlist::unsubscribeFromMailingList($entry). N'ajoutez pas ces effets secondaires dans les contrôleurs de l'application hôte. - La synchronisation s'exécute via des jobs en file d'attente (
SyncEntryToMailingList,UnsubscribeEntryFromMailingList). Laissezmailing_list.queue.enabledactivé en production pour que les inscriptions ne bloquent jamais sur l'API du fournisseur. Rechargez les lignes existantes avecphp artisan waitlist:sync-mailing-list [slug] [--all] [--force]ouWaitlist::for($slug)->syncMailingList(). - Ajoutez un service non fourni par le package en implémentant
OffloadProject\Waitlist\Contracts\MailingListDriveret en l'enregistrant avecMailingList::extend('name', fn (array $config) => new YourDriver(...))depuis un fournisseur de services. Ne forkez pas les drivers fournis. - Dans les tests, utilisez
MailingList::fake(), qui remplace par leArrayDriveren mémoire, exécute les synchronisations en ligne, et exposehasSubscriber(),subscribers(), ettagsFor(). N'utilisezHttp::fake()que pour vérifier la requête exacte envoyée par un vrai driver.
À ne pas faire
- N'effectuez pas de modifications du cycle de vie via des appels
update()directs ($entry->update(['status' => 'invited'])). UtilisezmarkAsInvited()/markAsRejected()/markAsVerified()pour que les casts, les effets secondaires (timestamps, effacement du token) et les événements restent cohérents. Mieux encore : pilotez tout via la façade. - Ne modifiez pas les migrations publiées pour ajouter des colonnes — écrivez une migration de suivi dans l'application hôte. Le package peut ajouter des colonnes dans les versions futures et supposera le schéma publié.
- Ne sous-classez pas
WaitlistouWaitlistEntry; les deux sontfinal. Ajoutez du comportement côté application hôte via des écouteurs sur les événements du package, ou en étendant le service via une liaison personnalisée dans votre conteneur.
Exemples
Liste d'attente unique (sans configuration)
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),
]);
Listes d'attente multiples
Waitlist::create('Programme Bêta', 'beta');
Waitlist::create('Accès VIP', 'vip');
Waitlist::for('beta')->add('Jane Smith', '[email protected]');
Waitlist::for('vip')->add('Bob Wilson', '[email protected]');
$betaEnAttente = Waitlist::for('beta')->getPending();
$compteVIP = Waitlist::for('vip')->count();
Flux de vérification
// 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]');
// E-mail de vérification envoyé automatiquement.
try {
Waitlist::invite($entry);
} catch (UnverifiedEntryException) {
return back()->withErrors(['email' => 'Veuillez d\'abord vérifier votre e-mail.']);
}
Câblage d'un modèle invitable (invitations d'équipe)
// 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',
],
],
Puis :
Waitlist::for('beta')->add('Jane', '[email protected]', [
'team_id' => $team->id,
'role' => 'admin',
]);
Quand vous appelez plus tard Waitlist::invite($entry), l'invitation laravel-invite-only résultante est limitée à cette équipe avec role=admin.
Notification de vérification personnalisée
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('Confirmez votre place sur la liste d\'attente')
->greeting("Bonjour {$this->entry->name} !")
->action('Vérifier l\'e-mail', $url);
}
}
// config/waitlist.php
'verification' => [
'enabled' => true,
'notification' => \App\Notifications\CustomVerifyWaitlistEmail::class,
],
Synchronisation des inscriptions vers Mailchimp
// config/waitlist.php
'mailing_list' => [
'enabled' => true,
'default' => 'mailchimp',
'drivers' => [
'mailchimp' => [
'key' => env('MAILCHIMP_API_KEY'), // le suffixe porte le centre de données, par ex. -us14
'list_id' => env('MAILCHIMP_LIST_ID'),
],
],
],
// Une audience par liste d'attente (optionnel — sinon le list_id de la config est utilisé).
Waitlist::for('beta')->connectMailingList('a1b2c3d4e5');
// Abonné automatiquement, lors de l'ajout ou de la vérification selon la config.
Waitlist::for('beta')->add('Jane Smith', '[email protected]');
Réaction au cycle de vie
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));
Test d'un flux de liste de diffusion
use OffloadProject\Waitlist\Facades\MailingList;
MailingList::fake();
// … votre code qui ajoute, vérifie, invite …
$waitlist = Waitlist::for('beta');
$entry = $waitlist->add('Test', '[email protected]');
MailingList::assertSubscribed($entry);