跳到正文
cccskills
免费在 GitHub 公开

Laravel Invite Only

Conventions and APIs for the offload-project/laravel-invite-only package — polymorphic invitations, token acceptance, bulk invites, scheduled reminders, and event-driven hooks.

中文简介正在准备中。当前显示原文说明。

查看安装方法

包含的文件(1)

  • SKILL.md11.4 KB

SKILL.md(原文)

安装前可以查看交给 AI 代理的指令内容。

Context

offload-project/laravel-invite-only is a Laravel 11/12/13 package (PHP 8.2+) for managing user invitations against any model via polymorphic relationships. It ships:

  • An Invitation Eloquent model with status lifecycle (pending, accepted, declined, expired, cancelled) backed by an InvitationStatus enum.
  • Two traits: HasInvitations (for models that issue invitations — Team, Organization, Project) and CanBeInvited (for the User model).
  • An InviteOnly facade that wraps token generation, event dispatch, and notification sending.
  • Events for every lifecycle transition: InvitationCreated, InvitationAccepted, InvitationDeclined, InvitationCancelled, InvitationExpired.
  • Structured exceptions: InvalidInvitationException, InvitationAlreadyAcceptedException, InvitationExpiredException.
  • A invite-only:send-reminders Artisan command for scheduled reminder emails and expiration sweeps.

Apply this skill when working in a Laravel app that has offload-project/laravel-invite-only in composer.json, or when the user asks for help with InviteOnly, HasInvitations, CanBeInvited, the Invitation model, or invitation flows in this package.

Rules

Trait usage

  1. Apply HasInvitations to any model that can issue invitations (Team, Organization, Project, Account). Apply CanBeInvited to the User model that receives them.
  2. If a single model both sends and receives invitations (e.g. user-to-user friend invites), use both traits with PHP trait conflict resolution — the two traits each define an acceptedInvitations() method that collides. See the example below.
  3. Prefer getAcceptedInvitations() over the deprecated acceptedInvitations() helper on HasInvitations. The deprecated method will be removed in v3.0.

Status & enum

  1. Use the InvitationStatus enum (InvitationStatus::Pending, Accepted, Declined, Expired, Cancelled). Do not use the deprecated Invitation::STATUS_* string constants; they are kept only for backwards compatibility and will be removed.
  2. Check terminal states via $status->isTerminal() rather than chaining || against individual cases.

Creating invitations

  1. Create invitations through $invitable->invite() / $invitable->inviteMany() (preferred) or the InviteOnly facade. Do not call Invitation::create() directly — the facade handles token generation, expiration defaults, the InvitationCreated event, and the outbound notification.
  2. Pass the invitable model via the trait method ($team->invite(...)), or as the second argument to InviteOnly::invite($email, $invitable, $options). For invitations not tied to any model (e.g. open-platform signup), pass null.
  3. For bulk invites, use inviteMany() and inspect the returned BulkInvitationResult — $result->successful (Collection of Invitation) and $result->failed (Collection of ['email' => ..., 'reason' => ...]). It supports partial failure; do not wrap it in a try/catch expecting an exception.
  4. inviteMany() deduplicates against existing pending invitations by default. Pass 'skip_duplicates' => false only when you intentionally want duplicate pending invites.

Accepting / declining / cancelling

  1. Accept by calling InviteOnly::accept($token, $user) (or Invitation::accept via facade). Always pass the authenticated User so accepted_by is recorded.
  2. Catch the typed exceptions individually when handling user-facing flows — InvitationExpiredException, InvitationAlreadyAcceptedException, InvalidInvitationException — to produce specific error messages. Do not catch the bare base InvitationException unless you intentionally want to collapse all failure modes.
  3. Wire the actual "do something on acceptance" logic (attaching a user to a team, granting a role, etc.) in an InvitationAccepted event listener, not inline at every call site. The facade fires the event for you.

Configuration & customization

  1. Customize notifications by overriding the invite-only.notifications.{invitation,reminder,cancelled,accepted} config entries. Setting any of them to null disables that notification. Do not edit the package's notification classes directly.
  2. Adjust expiration window via invite-only.expiration.days (default 7). Set invite-only.expiration.enabled to false for non-expiring invitations.
  3. Configure reminders via invite-only.reminders.after_days (e.g. [3, 5]) and max_reminders. Reminders only fire if reminders.enabled is true.
  4. The default routes are mounted at /invitations with ['web', 'throttle:60,1'] middleware. Keep the throttle (or stricter) — invitation tokens are otherwise susceptible to brute force. Disable the package routes (routes.enabled => false) only if you're providing your own.

Scheduling

  1. Schedule the bundled command to run daily so reminders go out and expired invitations get marked:

    Schedule::command('invite-only:send-reminders --mark-expired')->daily();
    

    Without --mark-expired, pending invitations past expires_at stay in pending status until something else marks them.

Mass assignment / model

  1. The Invitation model is final. To extend behavior, listen to events or wrap calls — do not try to subclass it.
  2. All migration columns are in $fillable. Setting lifecycle fields (accepted_at, accepted_by, declined_at, cancelled_at, last_sent_at, reminder_count) directly via update() is allowed but discouraged — prefer the markAs*() helpers so casts and side effects stay consistent.
  3. Use $invitation->isValid() (pending and not expired) when gating "can this token still be used" checks. isPending() alone is not sufficient.

Examples

Basic setup

// app/Models/Team.php
use OffloadProject\InviteOnly\Traits\HasInvitations;

class Team extends Model
{
    use HasInvitations;
}
// app/Models/User.php
use OffloadProject\InviteOnly\Traits\CanBeInvited;

class User extends Authenticatable
{
    use CanBeInvited;
}

Model that both sends and receives invitations

use OffloadProject\InviteOnly\Traits\CanBeInvited;
use OffloadProject\InviteOnly\Traits\HasInvitations;

class User extends Authenticatable
{
    use HasInvitations, CanBeInvited {
        CanBeInvited::acceptedInvitations insteadof HasInvitations;
        HasInvitations::acceptedInvitations as acceptedInvitationsToModel;
    }
}

In v3.0 HasInvitations::acceptedInvitations() will be removed in favour of getAcceptedInvitations(), eliminating the conflict — at which point the insteadof / as clauses can be dropped.

Sending invitations

$invitation = $team->invite('user@example.com', [
    'role' => 'member',
    'invited_by' => auth()->user(),
    'metadata' => ['source' => 'team-settings'],
]);

Bulk invitations with partial-failure handling

$result = $team->inviteMany(
    ['one@example.com', 'two@example.com', 'bad-email'],
    ['role' => 'member', 'invited_by' => auth()->user()],
);

foreach ($result->successful as $invitation) {
    // send to UI, log, etc.
}

foreach ($result->failed as $failure) {
    Log::warning('Skipped invite', $failure); // ['email' => ..., 'reason' => ...]
}

Accepting an invitation

use OffloadProject\InviteOnly\Exceptions\InvalidInvitationException;
use OffloadProject\InviteOnly\Exceptions\InvitationAlreadyAcceptedException;
use OffloadProject\InviteOnly\Exceptions\InvitationExpiredException;
use OffloadProject\InviteOnly\Facades\InviteOnly;

try {
    $invitation = InviteOnly::accept($token, auth()->user());
} catch (InvitationExpiredException) {
    return redirect()->route('login')->withErrors(['invite' => 'This invitation has expired.']);
} catch (InvitationAlreadyAcceptedException) {
    return redirect()->route('dashboard');
} catch (InvalidInvitationException $e) {
    return redirect()->route('login')->withErrors(['invite' => $e->getMessage()]);
}

Wiring side effects via the event

use Illuminate\Support\Facades\Event;
use OffloadProject\InviteOnly\Events\InvitationAccepted;

Event::listen(InvitationAccepted::class, function (InvitationAccepted $event): void {
    $team = $event->invitation->invitable; // Team|Organization|null
    $user = $event->user;
    $role = $event->invitation->role;

    if ($team !== null && $user !== null) {
        $team->users()->attach($user->id, ['role' => $role]);
    }
});

Custom notification

// config/invite-only.php
'notifications' => [
    'invitation' => App\Notifications\TeamInvitationSent::class,
    'reminder'   => App\Notifications\TeamInvitationReminder::class,
    'cancelled'  => null, // disabled
    'accepted'   => App\Notifications\TeamInvitationAccepted::class,
],

Status checks with the enum

use OffloadProject\InviteOnly\Enums\InvitationStatus;

if ($invitation->status === InvitationStatus::Pending) { /* ... */ }

if ($invitation->status->isTerminal()) {
    // accepted | declined | expired | cancelled
}

Scheduled reminders + expiration sweep

// routes/console.php
use Illuminate\Support\Facades\Schedule;

Schedule::command('invite-only:send-reminders --mark-expired')->daily();

Anti-patterns

  • ❌ Invitation::create([...]) — bypasses token generation, the InvitationCreated event, and the outbound notification. Always go through the facade or trait method.
  • ❌ Using Invitation::STATUS_PENDING (and the other STATUS_* constants). Deprecated — use InvitationStatus::Pending etc.
  • ❌ Using acceptedInvitations() from HasInvitations. Deprecated — use getAcceptedInvitations().
  • ❌ Subclassing Invitation. The model is final; extend behavior via events or wrapper services.
  • ❌ Catching \Throwable or \Exception around InviteOnly::accept(). Catch the typed exceptions so each failure mode produces a tailored response.
  • ❌ Doing "attach user to team / grant role" work inline at the acceptance route. Move it to an InvitationAccepted listener so manual acceptance, console flows, and webhook acceptance all behave the same.
  • ❌ Disabling the throttle middleware on package routes. Invitation tokens are 64-char hex; without a throttle they are still brute-forceable at high rates.
  • ❌ Editing files inside vendor/offload-project/laravel-invite-only. All extension points (notifications, expiration, routes, redirects) are exposed via config/invite-only.php.

References

评价

还没有评价。欢迎分享你的使用感受。

评论

无需登录即可发表。欢迎分享使用感受、提问,或说明在哪些环境下无法运行。

还没有评论。来写第一条吧。

提交后会立即公开。请勿填写邮箱、电话等个人信息。URL 最多 2 个。

0 / 1000 字

此表单受 reCAPTCHA 保护,适用 Google 的隐私权政策和服务条款。

报告此技能的问题