Community生產力與協作github.com

offload-project/laravel-waitlist

管理多個候補名單、條目狀態追蹤、可選電子郵件驗證、生命週期事件,以及郵件清單同步(Mailchimp/Kit/Audienceful),並可橋接至 laravel-invite-only。

laravel-waitlist 是什麼?

此技能提供 `offload-project/laravel-waitlist` Laravel 套件的完整操作指引,適用於 Laravel 11/12/13 與 PHP 8.3+。它能建立與管理多個命名候補名單(Waitlist),並追蹤每個條目(WaitlistEntry)的狀態(pending、invited、rejected)。支援選用的電子郵件驗證流程,並可透過橋接功能與 `offload-project/laravel-invite-only` 整合,在邀請時自動建立正式的 Invitation 記錄。此外,套件內建郵件清單同步功能,支援 Mailchimp、Kit、Audienceful,並提供佇列作業與多種驅動程式(含 log、array 與擴充鉤子)。開發者應遵循技能中的規則,例如一律使用 Facade、避免直接修改模型或繞過驗證流程,以及正確處理生命週期事件與例外。此技能也涵蓋測試策略,如使用 `MailingList::fake()` 進行隔離測試。

相容平台✓Claude Code~Codex CLI✓Cursor
npx skills add https://github.com/offload-project/laravel-waitlist/tree/HEAD/skills

Installed? Explore more 生產力與協作 skills: steipete/gemini, steipete/gh-issues, steipete/skill-creator · View all 6 →

在你喜歡的 AI 中提問

開啟一個已預先載入此 Agent Skill 的新對話。

說明文件

背景

offload-project/laravel-waitlist 是一個 Laravel 11/12/13 套件(PHP 8.3+),用於管理一個或多個候補名單。它提供:

  • Waitlist Eloquent 模型(具名稱與 slug 的候補名單)與 WaitlistEntry 模型(等候者)。
  • WaitlistService(透過 Waitlist Facade 解析),包含 for()、create()、add()、invite()、reject()、sendVerification()、verify() 及查詢/計數輔助方法。
  • 可選的電子郵件驗證流程,內建 /waitlist/verify/{token} 路由。
  • 可選橋接至 offload-project/laravel-invite-only:呼叫 Waitlist::invite() 會建立真正的 Invitation(包含 token、過期時間、事件),並將外鍵保存於 WaitlistEntry::$invitation_id。
  • 兩個通知:WaitlistInvited(經由 auto_send_invitation 選擇啟用)與 VerifyWaitlistEmail。
  • 在 OffloadProject\Waitlist\Events 中的生命週期事件:WaitlistCreated、WaitlistEntryAdded、WaitlistEntryVerified、WaitlistEntryInvited、WaitlistEntryRejected,以及 WaitlistEntrySubscribed、WaitlistEntryUnsubscribed 與 MailingListSyncFailed。
  • 郵件清單整合(MailingList Facade、MailingListManager),可透過佇列作業將條目同步至 Mailchimp、Kit(ConvertKit)或 Audienceful,並提供 log 與 array 驅動程式及 extend() 擴充鉤子。
  • 型別例外:UnverifiedEntryException(在驗證閘門開啟時對未驗證條目進行邀請)與 MailingListException(驅動程式/憑證/清單 ID/API 失敗)。

在使用了 offload-project/laravel-waitlist(存在於 composer.json 中)的 Laravel 專案中,或當使用者請求 Waitlist、WaitlistEntry、Waitlist 或 MailingList Facade 的協助,或是關於此套件的候補名單流程時,請套用此技能。

規則

Facade 使用

  1. 請使用 Waitlist Facade(OffloadProject\Waitlist\Facades\Waitlist)——不要直接實例化 WaitlistService。Facade 是官方支援的進入點。
  2. 若要指定特定候補名單,請串聯 Waitlist::for($slugOrIdOrModel)->...。若未使用 for(...),則所有呼叫會作用於預設候補名單(首次使用時透過 getDefault() 自動建立)。
  3. Waitlist::for(...) 會改變單例服務的內部狀態。對於長時間運行的程序(佇列工作者、Octane),請在每次操作時呼叫 for(...),而不是依賴於先前留下的上下文。

候補名單 vs. 條目

  1. 透過 Waitlist::create(string $name, string $slug, ?string $description = null, bool $isActive = true) 建立候補名單。slug 是規範識別碼——for(...) 與 find(...) 會根據它來查找。
  2. 透過 Waitlist::for($slug)->add($name, $email, $metadata = []) 新增條目。若需要驗證流程執行,請不要直接呼叫 WaitlistEntry::create([...])——當 waitlist.verification.enabled 為 true 時,add() 會自動觸發 sendVerification()。
  3. waitlist_entries 的唯一約束是 ['waitlist_id', 'email'],不只是 email。同一個人可以加入多個候補名單。

邀請

  1. 透過 Waitlist::invite($entryOrId, $options = []) 進行邀請。不要僅使用 $entry->markAsInvited(),因為 invite() 會建立 laravel-invite-only 的 Invitation,透過 invitation_id 關聯,並將條目標記為已邀請。
  2. $options 會傳遞給 InviteOnly::invite(...)。常見鍵值:'invited_by'(Model 或 int——預設為 auth()->user())、'role'、'metadata'、'expires_at'。不要在 waitlist.invitable.metadata_mapper 中重複設定已設定的鍵值——顯式傳入的 $options 會透過 array_merge 優先。
  3. 若 waitlist.verification.enabled 為 true 且 waitlist.verification.require_before_invite 為 true,則對未驗證條目呼叫 invite() 時會拋出 UnverifiedEntryException。請在使用者面向的流程中明確捕獲此例外,不要用泛用的 \Throwable 隱藏它。
  4. WaitlistInvited 通知是選擇啟用的(waitlist.auto_send_invitation 預設為 false)。來自 laravel-invite-only 的邀請通知無論如何都會發送。僅在希望同時發送第二封候補名單品牌郵件時才啟用 auto_send_invitation。

驗證

  1. 透過 Waitlist::sendVerification($entry) 觸發驗證——它會產生一個新的 token(generateVerificationToken() 會覆蓋任何現有 token)並發送 VerifyWaitlistEmail。不要自行產生 token,請使用套件的 token,以確保驗證路由能正常運作。
  2. 透過 Waitlist::verify($token) 確認 token。成功時回傳 WaitlistEntry,未知 token 時回傳 null。驗證後 token 會被清除(一次性使用)。
  3. 可透過 waitlist.verification.notification 設定自訂驗證通知;它的建構子會接收 WaitlistEntry。請從 $entry->verification_token 讀取 token,並呼叫 route('waitlist.verify', ['token' => $entry->verification_token])。
  4. 套件的驗證路由會掛載在 waitlist.routes.prefix(預設 waitlist)下,並使用 waitlist.routes.middleware(預設 ['web'])。若要使用自己的控制器,請設定 waitlist.routes.enabled => false,並從自己的 action 中呼叫 Waitlist::verify($token)。

狀態與檢查

  1. 條目狀態為字串常數 pending、invited、rejected。請優先使用 $entry->isPending()、isInvited()、isRejected(),而非原始字串比較。
  2. 驗證狀態存於 verified_at 與 verification_token。請透過 isVerified() 與 isPendingVerification() 檢查,不要比較原始時間戳。
  3. 若要實現「封鎖直到驗證」的 UI 閘門,請使用 isPendingVerification()(已設定 token,尚未驗證)。僅使用 isVerified() 會對從未開始驗證的條目回傳 false——這兩個狀態是不同的。

Invitable 配置

  1. 當主機應用程式要邀請人們加入特定實體(團隊、組織、專案)時,請在 config/waitlist.php 的 invitable 下一次性設定:
    • invitable.model —— 類別字串;套件會對其呼叫 ::first()。僅用於單租戶應用。
    • invitable.resolver —— 閉包 fn(WaitlistEntry $entry) => Model|null,用於多租戶情況。從 $entry->metadata 或其他欄位取得租戶 ID。
    • invitable.metadata_mapper —— 閉包 fn(WaitlistEntry $entry) => array,用於將條目中繼資料轉換為邀請中繼資料(例如 ['role' => 'beta-tester'])。
  2. 請不要在每個呼叫點硬編碼 invitable。若不同流程需要不同的 invitable,請在 resolver 閉包中使用 metadata 中的鑑別器。

事件

  1. 請使用套件自有事件來掛鉤生命週期,而不是包裝 Facade 或輪詢資料表。事件位於 OffloadProject\Waitlist\Events,每個事件都將模型作為唯讀屬性攜帶($event->entry、$event->waitlist)。
  2. 生命週期事件從模型觸發(WaitlistEntryAdded 透過 $dispatchesEvents 在建立時觸發;其餘從 markAsInvited() / markAsRejected() / markAsVerified() 觸發),因此即使程式碼繞過 Facade,監聽器仍會執行。
  3. 在測試中使用 Event::fake([SpecificEvent::class]),而不是裸的 Event::fake()——全面的 fake 也會吞掉 WaitlistEntryAdded,這會導致郵件清單監聽器永遠無法執行。

郵件清單同步

  1. 使用 waitlist.mailing_list.enabled 開啟此功能,並選擇驅動程式(mailchimp、kit、audienceful、log、array)。使用 Waitlist::for($slug)->connectMailingList($listId, $driver = null) 將候補名單連接到一個清單——清單 ID 可為 Mailchimp 的 audience ID、Kit 的 form ID(或當 list_type 為 tag 時的 tag ID),或 Audienceful 的 publication ID(或當 list_type 為 tag 時的 tag 名稱)。沒有自訂清單的候補名單會回退使用驅動程式設定的 list_id。
  2. 不要自行建立「註冊時訂閱」的監聽器。訂閱是自動的,並遵循驗證設定:若 waitlist.verification.enabled 關閉,則條目在 add() 時同步;若開啟,則在 Waitlist::verify() 之後同步。此順序是經過設計的——未確認的地址絕不能進入電子報。
  3. 對於訂閱以外的操作——例如在邀請時打標籤、在拒絕時移除、在清單間移動——請監聽生命週期事件,並呼叫 MailingList::tagEntry($entry, [...]) 或 Waitlist::unsubscribeFromMailingList($entry)。請不要在宿主應用程式的控制器中加入這些副作用。
  4. 同步會透過佇列作業(SyncEntryToMailingList、UnsubscribeEntryFromMailingList)執行。請在正式環境中保持 mailing_list.queue.enabled 為開啟,以避免註冊阻塞於提供者的 API。可使用 php artisan waitlist:sync-mailing-list [slug] [--all] [--force] 或 Waitlist::for($slug)->syncMailingList() 來回填現有資料列。
  5. 若要加入套件未提供的服務,請實作 OffloadProject\Waitlist\Contracts\MailingListDriver,並在服務提供者中使用 MailingList::extend('name', fn (array $config) => new YourDriver(...)) 註冊。請不要複製修改內建驅動程式。
  6. 在測試中使用 MailingList::fake(),它會替換成記憶體中的 ArrayDriver,同步會內聯執行,並提供 hasSubscriber()、subscribers() 與 tagsFor()。僅在需要斷言真實驅動程式發送的確切請求時才使用 Http::fake()。

禁止事項

  1. 請不要透過直接 update() 呼叫(例如 $entry->update(['status' => 'invited']))來執行生命週期變更。請使用 markAsInvited() / markAsRejected() / markAsVerified(),以確保型別轉換、副作用(時間戳、清除 token)與事件保持一致。更好的是:透過 Facade 驅動一切。
  2. 請不要編輯已發布的遷移檔案來新增欄位——請在宿主應用程式中編寫後續遷移。套件可能在未來版本中新增欄位,並會假定發佈的結構描述不變。
  3. 請不要繼承 Waitlist 或 WaitlistEntry,兩者皆為 final。請透過監聽套件事件在宿主應用端加入行為,或透過在應用程式的容器中自訂綁定來擴充服務。

範例

單一候補名單(無設定)

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),
]);

多個候補名單

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();

驗證流程

// 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]');
// 驗證郵件已自動發送。

try {
    Waitlist::invite($entry);
} catch (UnverifiedEntryException) {
    return back()->withErrors(['email' => '請先驗證您的電子郵件。']);
}

配置可邀請模型(團隊邀請)

// 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',
    ],
],

接著:

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

之後當您呼叫 Waitlist::invite($entry) 時,產生的 laravel-invite-only 邀請將限定於該團隊,且角色為 admin。

自訂驗證通知

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('確認您的候補名單位置')
            ->greeting("嗨,{$this->entry->name}!")
            ->action('驗證電子郵件', $url);
    }
}
// config/waitlist.php
'verification' => [
    'enabled' => true,
    'notification' => \App\Notifications\CustomVerifyWaitlistEmail::class,
],

將註冊同步至 Mailchimp

// config/waitlist.php
'mailing_list' => [
    'enabled' => true,
    'default' => 'mailchimp',
    'drivers' => [
        'mailchimp' => [
            'key' => env('MAILCHIMP_API_KEY'),   // 後綴攜帶數據中心,例如 -us14
            'list_id' => env('MAILCHIMP_LIST_ID'),
        ],
    ],
],
// 每個候補名單可指定不同受眾(可選——否則會使用設定中的 list_id)。
Waitlist::for('beta')->connectMailingList('a1b2c3d4e5');

// 根據設定,會在新增時或驗證時自動訂閱。
Waitlist::for('beta')->add('Jane Smith', '[email protected]');

對生命週期做出反應

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));

測試郵件清單流程

use OffloadProject\Waitlist\Facades\MailingList;

// 使用 fake 驅動程式
MailingList::fake();

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

MailingList::assertSubscriber('[email protected]');
MailingList::assertHasTag('[email protected]', 'beta');

相關技能