offload-project/laravel-waitlist

本スキルは、Laravel アプリケーションで `offload-project/laravel-waitlist` パッケージを利用するための規約とAPIを提供します。複数のウェイティングリスト、エントリーステータス追跡、オプションのメール確認、ライフサイクルイベント、メーリングリスト同期(Mailchimp/Kit/Audienceful)、および `laravel-invite-only` との連携をサポートします。

laravel-waitlist とは?

`offload-project/laravel-waitlist` は、Laravel 11/12/13およびPHP 8.3以上に対応した、ウェイティングリスト管理パッケージです。このパッケージは、`Waitlist`(名前とスラッグを持つリスト)と `WaitlistEntry`(リストに登録されたユーザー)の2つのEloquentモデル、統一された `Waitlist` ファサード、オプションのメール確認フロー、`laravel-invite-only` パッケージへのブリッジ、Mailchimp・Kit・Audiencefulなどのメーリングリストサービスへの自動同期、そしてライフサイクル全体にわたる豊富なイベントを提供します。このスキルは、`composer.json` にこのパッケージが含まれているLaravelアプリケーション、またはユーザーがウェイティングリストに関連する機能(エントリーの追加、招待、確認、同期など)について支援を求めている場合に適用されます。開発者は `Waitlist` ファサードを介してすべての操作を行い、内部の `WaitlistService` を直接インスタンス化しないことが推奨されます。

対応✓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に質問する

このエージェントスキルを事前に読み込んだ状態で新しいチャットを開きます。

ドキュメント

コンテキスト

offload-project/laravel-waitlist は、1つまたは複数のウェイティングリストを管理するためのLaravel 11/12/13パッケージ(PHP 8.3+)です。以下の機能を提供します。

  • Waitlist Eloquentモデル(slugを持つ名前付きウェイティングリスト)と WaitlistEntry モデル(リストに登録された人)。
  • Waitlist ファサードを介して解決される WaitlistService(for()、create()、add()、invite()、reject()、sendVerification()、verify()、およびクエリ/カウントヘルパーを提供)。
  • 公開された /waitlist/verify/{token} ルートを使用したオプションのメール確認フロー。
  • offload-project/laravel-invite-only へのオプションのブリッジ:Waitlist::invite() を呼び出すと、実際の Invitation(トークン、有効期限、イベント)が作成され、WaitlistEntry::$invitation_id に外部キーが永続化されます。
  • 2つの通知:WaitlistInvited(auto_send_invitation を介したオプトイン)と VerifyWaitlistEmail。
  • OffloadProject\Waitlist\Events 内のライフサイクルイベント:WaitlistCreated、WaitlistEntryAdded、WaitlistEntryVerified、WaitlistEntryInvited、WaitlistEntryRejected、さらに WaitlistEntrySubscribed、WaitlistEntryUnsubscribed、および MailingListSyncFailed。
  • メーリングリスト統合(MailingList ファサード、MailingListManager):キュー化されたジョブでエントリーをMailchimp、Kit (ConvertKit)、またはAudiencefulに同期します。log および array ドライバーと、他のドライバーのための extend() フックを提供します。
  • 型付き例外:UnverifiedEntryException(確認ゲートが有効な場合に未確認エントリーに対して招待を試行)および MailingListException(ドライバー/資格情報/リストID/APIの失敗)。

このスキルは、composer.json に offload-project/laravel-waitlist が含まれているLaravelアプリケーションで作業する場合、またはユーザーが Waitlist、WaitlistEntry、Waitlist または MailingList ファサード、またはこのパッケージのウェイティングリストフローについて支援を求めた場合に適用します。

ルール

ファサードの使用法

  1. Waitlist ファサード(OffloadProject\Waitlist\Facades\Waitlist)を使用してください。WaitlistService を直接インスタンス化しないでください。ファサードがサポートされているエントリーポイントです。
  2. 特定のウェイティングリストをターゲットにするには、Waitlist::for($slugOrIdOrModel)->... をチェーンします。for(...) がない場合、呼び出しはデフォルトのウェイティングリスト(getDefault() を介して最初の使用時に自動作成されます)に対して動作します。
  3. Waitlist::for(...) はシングルトンサービスの内部状態を変更します。長時間実行プロセス(キューワーカー、Octane)の場合は、以前のコンテキスト呼び出しが保持されることを期待せず、操作ごとに for(...) を呼び出してください。

ウェイティングリストとエントリー

  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([...]) を直接呼び出さないでください。add() は、waitlist.verification.enabled が true の場合に自動的に sendVerification() をトリガーします。
  3. waitlist_entries の一意制約は ['waitlist_id', 'email'] であり、email のみではありません。同じ人が複数のウェイティングリストに参加できます。

招待

  1. Waitlist::invite($entryOrId, $options = []) で招待します。通知と Invitation レコードが必要な場合は、$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 は、追加でウェイティングリストブランドの2通目のメールを送信したい場合にのみ有効にしてください。

確認

  1. Waitlist::sendVerification($entry) で確認をトリガーします。新しいトークンを生成し(generateVerificationToken() は既存のものを上書きします)、VerifyWaitlistEmail を送信します。独自のトークン生成を行わないでください。パッケージのものを使用して、確認ルートが機能し続けるようにしてください。
  2. Waitlist::verify($token) でトークンを確認します。成功した場合は WaitlistEntry を返し、不明なトークンの場合は null を返します。確認後、トークンはクリアされます(使い捨て)。
  3. waitlist.verification.notification 設定を介して確認通知をカスタマイズします。コンストラクタで WaitlistEntry を受け取ります。トークンは $entry->verification_token から読み取り、route('waitlist.verify', ['token' => $entry->verification_token]) を呼び出します。
  4. パッケージの確認ルートは、waitlist.routes.prefix(デフォルト waitlist)の下に、waitlist.routes.middleware(デフォルト ['web'])を使用してマウントされます。独自のコントローラを使用するには、waitlist.routes.enabled => false を設定し、アクションから Waitlist::verify($token) を呼び出します。

ステータスとチェック

  1. エントリーステータスは文字列リテラル pending、invited、rejected です。生の文字列比較よりも $entry->isPending()、isInvited()、isRejected() を優先してください。
  2. 確認状態は verified_at と verification_token にあります。isVerified() と isPendingVerification() でチェックしてください。生のタイムスタンプを比較しないでください。
  3. 「確認までブロック」するUIゲーティングには、isPendingVerification()(トークンは設定されているが、まだ確認されていない)を使用します。isVerified() だけでは、確認を開始していないエントリーに対して false を返します。これら2つの状態は異なります。

Invitable の配線

  1. ホストアプリが特定のエンティティ(Team、Organization、Project)に人を招待する場合、config/waitlist.php の invitable の下で一度設定します。
    • invitable.model — クラス文字列。パッケージは ::first() を呼び出します。シングルテナントアプリでのみ使用してください。
    • invitable.resolver — マルチテナントの場合のクロージャ fn(WaitlistEntry $entry) => Model|null。テナントIDを $entry->metadata または別のカラムから取得します。
    • invitable.metadata_mapper — エントリーメタデータを招待メタデータに変換するクロージャ fn(WaitlistEntry $entry) => array(例:['role' => 'beta-tester'])。
  2. 呼び出しサイトごとに invitable をハードコードしないでください。異なるフローで異なる invitable が必要な場合は、metadata 内の識別子とともに resolver クロージャを使用します。

イベント

  1. ファサードをラップしたりテーブルをポーリングしたりする代わりに、パッケージ独自のイベントを使用してライフサイクルにフックします。これらは OffloadProject\Waitlist\Events にあり、それぞれがモデルを読み取り専用プロパティ($event->entry、$event->waitlist)として保持します。
  2. ライフサイクルイベントはモデルから発生します(WaitlistEntryAdded は作成時の $dispatchesEvents を介し、その他は markAsInvited() / markAsRejected() / markAsVerified() から発生します)。そのため、ファサードをバイパスするコードに対してもリスナーは実行されます。
  3. テストでは Event::fake([SpecificEvent::class]) を使用し、Event::fake() をそのまま使用しないでください。無差別なフェイクは WaitlistEntryAdded も飲み込み、メーリングリストリスナーが実行されなくなります。

メーリングリストの同期

  1. これを有効にするには waitlist.mailing_list.enabled を true に設定し、ドライバー(mailchimp、kit、audienceful、log、array)を選択します。ウェイティングリストをリストに接続するには、Waitlist::for($slug)->connectMailingList($listId, $driver = null) を使用します。リストIDは、MailchimpオーディエンスID、KitフォームID(list_type が tag の場合はタグID)、またはAudiencefulパブリケーションID(list_type が 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() を使用して、キャスト、副作用(タイムスタンプ、トークンクリア)、およびイベントの一貫性を保ってください。さらに良いのは、すべてをファサードを介して駆動することです。
  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]');
// Verification email sent automatically.

try {
    Waitlist::invite($entry);
} catch (UnverifiedEntryException) {
    return back()->withErrors(['email' => 'Please verify your email first.']);
}

Invitableモデルの配線(チーム招待)

// 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 招待は、そのチームに role=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('Confirm your spot on the waitlist')
            ->greeting("Hi {$this->entry->name}!")
            ->action('Verify Email', $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'),   // suffix carries the data centre, e.g. -us14
            'list_id' => env('MAILCHIMP_LIST_ID'),
        ],
    ],
],
// One audience per waitlist (optional — otherwise the config list_id is used).
Waitlist::for('beta')->connectMailingList('a1b2c3d4e5');

// Subscribed automatically, on add or on verification depending on the config.
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;
use OffloadProject\Waitlist\Facades\Waitlist;

// …

関連スキル