offload-project/laravel-waitlist

Conventions et API pour le package offload-project/laravel-waitlist – listes d'attentes multiples, suivi du statut des inscriptions, vérification e-mail optionnelle, événements du cycle de vie, synchronisation avec des listes de diffusion (Mailchimp/Kit/Audienceful) et pont avec laravel-invite-only.

Qu'est-ce que laravel-waitlist ?

Cette compétence décrit l'utilisation du package `offload-project/laravel-waitlist` pour Laravel 11/12/13 (PHP 8.3+), qui gère une ou plusieurs listes d'attente. Elle fournit les modèles `Waitlist` et `WaitlistEntry`, un service `WaitlistService` accessible via la façade `Waitlist`, et un flux de vérification e-mail optionnel avec une route publiée. Le package s'intègre avec `offload-project/laravel-invite-only` pour les invitations et permet de synchroniser les inscriptions avec des services de marketing par e-mail comme Mailchimp, Kit (ConvertKit) ou Audienceful via des drivers configurables. Les événements du cycle de vie (ajout, vérification, invitation, rejet, synchronisation) sont émis pour permettre une extensibilité propre. La compétence est utile pour les développeurs Laravel qui souhaitent gérer des listes d'attente avec des fonctionnalités avancées comme la vérification e-mail, les invitations liées à des entités, et la synchronisation avec des listes de diffusion, le tout en respectant les conventions du package (utilisation des façades, méthodes dédiées, événements, et configuration via les fichiers de configuration).

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

Installed? Explore more Productivité et collaboration skills: steipete/gemini, steipete/gh-issues, steipete/skill-creator · View all 6 →

Demander à votre IA préférée

Ouvre une nouvelle conversation avec cette compétence d'agent déjà préchargée.

Documentation

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 un slug) et un modèle WaitlistEntry (une personne en attente sur une liste).
  • Un WaitlistService (résolu via la façade Waitlist) avec les méthodes for(), 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éritable Invitation (token, expiration, événements) et persiste la clé étrangère sur WaitlistEntry::$invitation_id.
  • Deux notifications : WaitlistInvited (opt-in via auto_send_invitation) et VerifyWaitlistEmail.
  • Des événements du cycle de vie dans OffloadProject\Waitlist\Events : WaitlistCreated, WaitlistEntryAdded, WaitlistEntryVerified, WaitlistEntryInvited, WaitlistEntryRejected, plus WaitlistEntrySubscribed, WaitlistEntryUnsubscribed et MailingListSyncFailed.
  • Une intégration de liste de diffusion (MailingList façade, MailingListManager) qui synchronise les inscriptions vers Mailchimp, Kit (ConvertKit) ou Audienceful dans un job en file d'attente, avec des drivers log et array et un hook extend() 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é) et MailingListException (é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

  1. Utilisez la façade Waitlist (OffloadProject\Waitlist\Facades\Waitlist) — n'instanciez pas WaitlistService directement. La façade est le point d'entrée officiel.
  2. Pour cibler une liste d'attente spécifique, enchaînez Waitlist::for($slugOrIdOrModel)->.... Sans for(...), les appels opèrent sur la liste d'attente par défaut (créée automatiquement lors de la première utilisation via getDefault()).
  3. Waitlist::for(...) modifie l'état interne d'un service singleton. Pour les processus longs (workers de file d'attente, Octane), appelez for(...) pour chaque opération plutôt que de vous fier à un contexte précédent.

Listes d'attente vs inscriptions

  1. Créez des listes d'attente avec Waitlist::create(string $name, string $slug, ?string $description = null, bool $isActive = true). Le slug est l'identifiant canonique — c'est ce que for(...) et find(...) attendent.
  2. Ajoutez des inscriptions via Waitlist::for($slug)->add($name, $email, $metadata = []). N'appelez pas WaitlistEntry::create([...]) directement lorsque vous voulez que le flux de vérification s'exécute — add() déclenche automatiquement sendVerification() lorsque waitlist.verification.enabled est à true.
  3. La contrainte d'unicité sur waitlist_entries est ['waitlist_id', 'email'], pas seulement email. La même personne peut rejoindre plusieurs listes d'attente.

Invitations

  1. Invitez via Waitlist::invite($entryOrId, $options = []). N'appelez pas $entry->markAsInvited() seul lorsque vous voulez des notifications et un enregistrement Invitation — invite() crée l'Invitation de laravel-invite-only, la lie via invitation_id, et marque l'inscription comme invitée.
  2. $options est transmis à InviteOnly::invite(...). Clés courantes : 'invited_by' (Modèle ou int — se replie sur auth()->user()), 'role', 'metadata', 'expires_at'. Ne dupliquez pas les clés définies dans waitlist.invitable.metadata_mapper — les $options explicites l'emportent via array_merge.
  3. Si waitlist.verification.enabled est à true et waitlist.verification.require_before_invite est à true, appeler invite() sur une inscription non vérifiée lance UnverifiedEntryException. Attrapez-la explicitement dans les flux utilisateur ; ne l'enterrez pas sous un \Throwable générique.
  4. La notification WaitlistInvited est opt-in (waitlist.auto_send_invitation par défaut à false). La notification d'invitation de laravel-invite-only est envoyée quoi qu'il arrive. Activez auto_send_invitation uniquement si vous voulez un second e-mail de marque lié à la liste d'attente.

Vérification

  1. Déclenchez la vérification via Waitlist::sendVerification($entry) — elle génère un nouveau token (generateVerificationToken() écrase tout token existant) et envoie VerifyWaitlistEmail. N'écrivez pas votre propre génération de token ; utilisez celui du package pour que la route de vérification continue de fonctionner.
  2. Confirmez les tokens via Waitlist::verify($token). Retourne le WaitlistEntry en cas de succès, null pour un token inconnu. Après vérification, le token est effacé (usage unique).
  3. Personnalisez la notification de vérification via la config waitlist.verification.notification ; elle reçoit le WaitlistEntry dans son constructeur. Lisez le token via $entry->verification_token et appelez route('waitlist.verify', ['token' => $entry->verification_token]).
  4. La route de vérification du package est montée sous waitlist.routes.prefix (par défaut waitlist) avec waitlist.routes.middleware (par défaut ['web']). Pour utiliser votre propre contrôleur, définissez waitlist.routes.enabled => false et appelez Waitlist::verify($token) depuis votre action.

Statut et vérifications

  1. 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.
  2. L'état de vérification réside dans verified_at et verification_token. Vérifiez via isVerified() et isPendingVerification() ; ne comparez pas les timestamps bruts.
  3. Pour un blocage "jusqu'à vérification" dans l'interface utilisateur, utilisez isPendingVerification() (token défini, pas encore vérifié). isVerified() seul retourne false pour les inscriptions qui n'ont jamais commencé la vérification — ces deux états sont différents.

Câblage de l'entité invitable

  1. Lorsque l'application hôte invite des personnes à une entité spécifique (Équipe, Organisation, Projet), configurez-la une fois dans config/waitlist.php sous invitable :
    • invitable.model — chaîne de classe ; le package appelle ::first() dessus. Utilisez ceci uniquement pour les applications monolocataires.
    • invitable.resolver — closure fn(WaitlistEntry $entry) => Model|null pour le cas multilocataire. Extrayez l'ID du locataire depuis $entry->metadata ou une autre colonne.
    • invitable.metadata_mapper — closure fn(WaitlistEntry $entry) => array pour traduire les métadonnées de l'inscription en métadonnées d'invitation (par ex. ['role' => 'beta-tester']).
  2. 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 resolver avec un discriminateur dans metadata.

Événements

  1. 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\Events et chacun porte le modèle comme propriété en lecture seule ($event->entry, $event->waitlist).
  2. Les événements du cycle de vie sont déclenchés depuis les modèles (WaitlistEntryAdded via $dispatchesEvents à la création ; les autres depuis markAsInvited() / markAsRejected() / markAsVerified()), donc les écouteurs s'exécutent même pour le code qui contourne la façade.
  3. Utilisez Event::fake([SpecificEvent::class]) dans les tests, pas un Event::fake() nu — un faux global avale aussi WaitlistEntryAdded, ce qui empêche l'écouteur de liste de diffusion de s'exécuter.

Synchronisation de liste de diffusion

  1. Activez-la avec waitlist.mailing_list.enabled et choisissez un driver (mailchimp, kit, audienceful, log, array). Connectez une liste d'attente à une liste avec Waitlist::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 lorsque list_type est tag), ou un ID de publication Audienceful (ou un nom de tag lorsque list_type est tag). Les listes d'attente sans liste propre utilisent le list_id configuré dans le driver.
  2. 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.enabled désactivé, l'inscription se synchronise lors de add() ; avec activé, l'inscription se synchronise après Waitlist::verify(). Ce choix est délibéré — une adresse non confirmée ne doit jamais atteindre la newsletter.
  3. 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, [...]) ou Waitlist::unsubscribeFromMailingList($entry). N'ajoutez pas ces effets secondaires dans les contrôleurs de l'application hôte.
  4. La synchronisation s'exécute via des jobs en file d'attente (SyncEntryToMailingList, UnsubscribeEntryFromMailingList). Laissez mailing_list.queue.enabled activé en production pour que les inscriptions ne bloquent jamais sur l'API du fournisseur. Rechargez les lignes existantes avec php artisan waitlist:sync-mailing-list [slug] [--all] [--force] ou Waitlist::for($slug)->syncMailingList().
  5. Ajoutez un service non fourni par le package en implémentant OffloadProject\Waitlist\Contracts\MailingListDriver et en l'enregistrant avec MailingList::extend('name', fn (array $config) => new YourDriver(...)) depuis un fournisseur de services. Ne forkez pas les drivers fournis.
  6. Dans les tests, utilisez MailingList::fake(), qui remplace par le ArrayDriver en mémoire, exécute les synchronisations en ligne, et expose hasSubscriber(), subscribers(), et tagsFor(). N'utilisez Http::fake() que pour vérifier la requête exacte envoyée par un vrai driver.

À ne pas faire

  1. N'effectuez pas de modifications du cycle de vie via des appels update() directs ($entry->update(['status' => 'invited'])). Utilisez markAsInvited() / 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.
  2. 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é.
  3. Ne sous-classez pas Waitlist ou WaitlistEntry ; les deux sont final. 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);

Skills associés