コンテキスト
offload-project/laravel-waitlist は、1つまたは複数のウェイティングリストを管理するためのLaravel 11/12/13パッケージ(PHP 8.3+)です。以下の機能を提供します。
WaitlistEloquentモデル(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 ファサード、またはこのパッケージのウェイティングリストフローについて支援を求めた場合に適用します。
ルール
ファサードの使用法
Waitlistファサード(OffloadProject\Waitlist\Facades\Waitlist)を使用してください。WaitlistServiceを直接インスタンス化しないでください。ファサードがサポートされているエントリーポイントです。- 特定のウェイティングリストをターゲットにするには、
Waitlist::for($slugOrIdOrModel)->...をチェーンします。for(...)がない場合、呼び出しはデフォルトのウェイティングリスト(getDefault()を介して最初の使用時に自動作成されます)に対して動作します。 Waitlist::for(...)はシングルトンサービスの内部状態を変更します。長時間実行プロセス(キューワーカー、Octane)の場合は、以前のコンテキスト呼び出しが保持されることを期待せず、操作ごとにfor(...)を呼び出してください。
ウェイティングリストとエントリー
Waitlist::create(string $name, string $slug, ?string $description = null, bool $isActive = true)でウェイティングリストを作成します。slugは正規の識別子であり、for(...)やfind(...)が期待するものです。Waitlist::for($slug)->add($name, $email, $metadata = [])でエントリーを追加します。確認フローを実行させたい場合は、WaitlistEntry::create([...])を直接呼び出さないでください。add()は、waitlist.verification.enabledが true の場合に自動的にsendVerification()をトリガーします。waitlist_entriesの一意制約は['waitlist_id', 'email']であり、emailのみではありません。同じ人が複数のウェイティングリストに参加できます。
招待
Waitlist::invite($entryOrId, $options = [])で招待します。通知とInvitationレコードが必要な場合は、$entry->markAsInvited()を単独で呼び出さないでください。invite()はlaravel-invite-onlyのInvitationを作成し、invitation_idを介してリンクし、エントリーを招待済みとしてマークします。$optionsはInviteOnly::invite(...)に渡されます。一般的なキー:'invited_by'(Model または int —auth()->user()にフォールバック)、'role'、'metadata'、'expires_at'。waitlist.invitable.metadata_mapperで設定したキーを重複させないでください。明示的な$optionsがarray_mergeを介して優先されます。waitlist.verification.enabledが true でwaitlist.verification.require_before_inviteが true の場合、未確認のエントリーに対してinvite()を呼び出すとUnverifiedEntryExceptionがスローされます。ユーザー向けのフローでは明示的にキャッチし、汎用的な\Throwableで隠さないでください。WaitlistInvited通知はオプトインです(waitlist.auto_send_invitationのデフォルトはfalse)。laravel-invite-onlyからの招待通知は常に送信されます。auto_send_invitationは、追加でウェイティングリストブランドの2通目のメールを送信したい場合にのみ有効にしてください。
確認
Waitlist::sendVerification($entry)で確認をトリガーします。新しいトークンを生成し(generateVerificationToken()は既存のものを上書きします)、VerifyWaitlistEmailを送信します。独自のトークン生成を行わないでください。パッケージのものを使用して、確認ルートが機能し続けるようにしてください。Waitlist::verify($token)でトークンを確認します。成功した場合はWaitlistEntryを返し、不明なトークンの場合はnullを返します。確認後、トークンはクリアされます(使い捨て)。waitlist.verification.notification設定を介して確認通知をカスタマイズします。コンストラクタでWaitlistEntryを受け取ります。トークンは$entry->verification_tokenから読み取り、route('waitlist.verify', ['token' => $entry->verification_token])を呼び出します。- パッケージの確認ルートは、
waitlist.routes.prefix(デフォルトwaitlist)の下に、waitlist.routes.middleware(デフォルト['web'])を使用してマウントされます。独自のコントローラを使用するには、waitlist.routes.enabled => falseを設定し、アクションからWaitlist::verify($token)を呼び出します。
ステータスとチェック
- エントリーステータスは文字列リテラル
pending、invited、rejectedです。生の文字列比較よりも$entry->isPending()、isInvited()、isRejected()を優先してください。 - 確認状態は
verified_atとverification_tokenにあります。isVerified()とisPendingVerification()でチェックしてください。生のタイムスタンプを比較しないでください。 - 「確認までブロック」するUIゲーティングには、
isPendingVerification()(トークンは設定されているが、まだ確認されていない)を使用します。isVerified()だけでは、確認を開始していないエントリーに対してfalseを返します。これら2つの状態は異なります。
Invitable の配線
- ホストアプリが特定のエンティティ(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'])。
- 呼び出しサイトごとに invitable をハードコードしないでください。異なるフローで異なる invitable が必要な場合は、
metadata内の識別子とともにresolverクロージャを使用します。
イベント
- ファサードをラップしたりテーブルをポーリングしたりする代わりに、パッケージ独自のイベントを使用してライフサイクルにフックします。これらは
OffloadProject\Waitlist\Eventsにあり、それぞれがモデルを読み取り専用プロパティ($event->entry、$event->waitlist)として保持します。 - ライフサイクルイベントはモデルから発生します(
WaitlistEntryAddedは作成時の$dispatchesEventsを介し、その他はmarkAsInvited()/markAsRejected()/markAsVerified()から発生します)。そのため、ファサードをバイパスするコードに対してもリスナーは実行されます。 - テストでは
Event::fake([SpecificEvent::class])を使用し、Event::fake()をそのまま使用しないでください。無差別なフェイクはWaitlistEntryAddedも飲み込み、メーリングリストリスナーが実行されなくなります。
メーリングリストの同期
- これを有効にするには
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にフォールバックします。 - 「サインアップ時に購読する」独自のリスナーを構築しないでください。購読は自動的であり、確認設定に従います。
waitlist.verification.enabledがオフの場合、エントリーはadd()時に同期され、オンの場合、エントリーはWaitlist::verify()後に同期されます。この順序は意図的です。未確認のアドレスがニュースレターに到達してはなりません。 - 招待時のタグ付け、拒否時の削除、リスト間の移動など、購読以外の処理については、ライフサイクルイベントをリッスンし、
MailingList::tagEntry($entry, [...])またはWaitlist::unsubscribeFromMailingList($entry)を呼び出します。これらの副作用をホストアプリのコントローラ内に追加しないでください。 - 同期はキュー化されたジョブ(
SyncEntryToMailingList、UnsubscribeEntryFromMailingList)を介して実行されます。プロダクションではmailing_list.queue.enabledをオンのままにして、サインアップがプロバイダのAPIをブロックしないようにします。既存の行は、php artisan waitlist:sync-mailing-list [slug] [--all] [--force]またはWaitlist::for($slug)->syncMailingList()でバックフィルします。 OffloadProject\Waitlist\Contracts\MailingListDriverを実装し、サービスプロバイダからMailingList::extend('name', fn (array $config) => new YourDriver(...))で登録することで、パッケージが提供していないサービスを追加します。出荷されているドライバーをフォークしないでください。- テストでは
MailingList::fake()を使用します。これにより、インメモリのArrayDriverが差し替えられ、同期がインラインで実行され、hasSubscriber()、subscribers()、およびtagsFor()が公開されます。実際のドライバーが送信する正確なリクエストをアサートする場合にのみHttp::fake()に手を伸ばしてください。
禁止事項
- 直接の
update()呼び出し($entry->update(['status' => 'invited']))でライフサイクル変更を実行しないでください。markAsInvited()/markAsRejected()/markAsVerified()を使用して、キャスト、副作用(タイムスタンプ、トークンクリア)、およびイベントの一貫性を保ってください。さらに良いのは、すべてをファサードを介して駆動することです。 - 公開されたマイグレーションを編集してカラムを追加しないでください。ホストアプリでフォローアップマイグレーションを作成してください。パッケージは将来のリリースでカラムを追加する可能性があり、公開されたスキーマを想定します。
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;
// …