Appearance
Roles and permissions
Three roles, and deliberately few. This is membership, not an ability system: anything finer grained than "runs the organization", "administers it" and "belongs to it" is what laraspring/permissions is for, and a fourth role here would be a permissions model with no vocabulary.
php
use Laraspring\Organizations\Support\Roles;
Roles::OWNER; // 'owner'
Roles::ADMIN; // 'admin'
Roles::MEMBER; // 'member'Who may do what
| owner | admin | member | |
|---|---|---|---|
| See the organization and its members | ✓ | ✓ | ✓ |
| Leave it | ✓* | ✓ | ✓ |
| Rename it | ✓ | ✓ | |
| Invite, revoke, resend | ✓ | ✓ | |
| Change a member's role | ✓ | ✓ | |
| Remove a member | ✓ | ✓ | |
| Change the slug | ✓ | ||
| Transfer ownership | ✓ | ||
| Delete the organization | ✓ |
* Unless they are the last owner. See below.
Two lines are worth explaining rather than memorising.
The slug is an owner's decision. It is in every URL anybody has bookmarked or scripted against, so changing it breaks links for people outside the organization. Renaming, which only changes what the team sees, is administrative.
Ownership is transferred, never granted. organizations.members.update accepts admin and member and nothing else. Handing ownership over moves two rows at once, in one transaction, and only the current owner may start it.
The invariant: an organization always has an owner
Every path that would leave an organization ownerless is refused:
- The last owner cannot be demoted.
- The last owner cannot be removed.
- The last owner cannot leave; they are told to transfer ownership first.
- Transferring runs both writes in one transaction, so the ownerless moment in between never exists.
This is not tidiness. An ownerless organization cannot be transferred, invited into or deleted by anybody, so it stops being a row your application can fix and becomes one only a database console can.
Every membership write goes through Laraspring\Organizations\Support\Members, which is the only place the invariant lives. A controller that attached or detached a row itself would be a second place free to break it.
php
use Laraspring\Organizations\Exceptions\LastOwnerException;
use Laraspring\Organizations\Support\Members;
try {
Members::remove($organization, $user);
} catch (LastOwnerException $e) {
// Refused. The organization would have had no owner.
}It raises rather than returning false on purpose: a caller who ignores a boolean leaves the organization ownerless and nothing later can tell.
Extending the policy
Authorisation lives in Laraspring\Organizations\Policies\OrganizationPolicy, registered by the package's service provider. Every method reads the role off the membership row fresh, so an expelled member is refused on their very next request rather than at their next login.
There is no before() granting a superuser anything. An application that wants staff impersonation builds it deliberately; it does not inherit it from a starter kit.
To change a rule, bind your own policy after the package's provider has booted:
php
// app/Providers/AppServiceProvider.php
public function boot(): void
{
Gate::policy(Organization::class, MyOrganizationPolicy::class);
}To add an ability of your own, prefer a new policy method over widening an existing one: manageMembers meaning three different things is how a permission check ends up in the wrong place.
Reading a role
php
use Laraspring\Organizations\Support\Organizations;
$organization = Organizations::current();
$organization->roleFor($user); // 'owner' | 'admin' | 'member' | null
$organization->hasMember($user); // bool
$organization->ownerCount(); // never 0roleFor() queries the pivot rather than trusting an already loaded relation, which is what stops a stale relation from keeping a removed member's access for one more request.