Add, remove and inspect members through the team handle — or the Team model shorthand, which runs the same manager call. Roles are plain string keys:
use RoundlyConsulting\Teams\Facades\Teams;
$members = Teams::for($team)->members();
$member = $members->add($user, 'member'); // Member
$member = $members->add($user, 'editor', meta: ['source' => 'import']);
$removed = $members->remove($user); // bool — soft-deletes; false when not a member
$members->find($user); // ?Member
$members->has($user); // bool
$members->all(); // Collection<Member> — expired memberships included
$team->memberHasRole($user, 'editor'); // bool
$team->memberHasPermission($user, 'posts.edit'); // bool
// Model shorthand — the same manager call:
$team->addMember($user, 'member');
$team->removeMember($user);Idempotent adds
A team holds one membership row per member — a unique index that covers soft-deleted rows too. Adding is idempotent: re-adding an active member returns the existing membership and updates the role only when it differs — no duplicate rows, no defensive checks in your code. An expiresAt is overwritten only when you pass one, and a membership’s original accepted invite is never rewritten.
Re-adding a member who was removed, or whose membership expired, revives that row as a fresh membership — role, meta and expiry come from the new call (no expiresAt means permanent) — and fires TeamMemberAdded. It takes a seat, so MaxSeats applies:
$members = Teams::for($team)->members();
$members->add($user, 'member'); // creates the membership — fires TeamMemberAdded
$members->add($user, 'member'); // no-op — returns the same Member
$members->add($user, 'admin'); // updates the role — fires TeamMemberRoleChanged
$members->remove($user); // soft-deletes the row
$members->add($user, 'member'); // revives that row as a fresh membership — fires TeamMemberAddedAdds run in a transaction that locks the team row, so simultaneous adds — a double submit, two people accepting the last seat — serialise: one row per member, and the seat cap holds.
Changing roles and removing
Teams::for($team)->members()->changeRole($bob, 'admin'); // Member — throws MemberNotFoundException for a non-member
Teams::for($team)->members()->remove($bob); // bool — soft-deletes the membership
$team->findMember($bob)?->removeFromTeam(); // bool — the same, from the Member sideA role change fires TeamMemberRoleChanged (with the previous role) only when the role actually changes; changing the role of a model that isn’t a member throws MemberNotFoundException. Removal fires TeamMemberDeleted and is a soft delete — the row stays for audits, and adding the member again revives it.
The Member model
$member = $team->findMember($user);
$member->role; // 'admin' — the stored role key
$member->role(); // ?Role — resolved (per-team aware); null once expired
$member->member; // the member model (MorphTo)
$member->team; // the Team
$member->meta; // Collection
$member->expires_at; // ?Carbon
$member->isExpired(); // bool
$member->acceptedInvite; // the Invite this member joined through, or nullSeat limits
When a team has a MaxSeats setting, adding a new member — or reviving a removed or expired one — throws TeamsException once the active membership count reaches the cap. Re-adding an active member never trips it — see Team settings.
Show your open-source love
This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.
More ways to support, including cryptoBy donating, you agree to our donation terms.
Want this built into your product?
We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.