NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages

Every connection carries its own set of permission strings — whatever names your app uses, such as view, edit or publish. The same two models can be connected with different privileges in different contexts. Sets are de-duplicated automatically.

Changing permissions

use RoundlyConsulting\Connections\Facades\Connections;

$permissions = Connections::between($user, $team)->permissions();

$permissions->grant('publish');          // Connection — creates the connection if absent
$permissions->revoke('publish');         // Connection — throws ConnectionNotFound if absent
$permissions->sync('view', 'edit');      // Connection — exact set, creates if absent
$permissions->clear();                   // Connection — remove every permission; throws ConnectionNotFound if absent

$permissions->all();                     // Collection<int, string> — what is stored
$permissions->has('edit');               // bool
$permissions->hasAny('view', 'edit');    // bool
$permissions->hasAll('view', 'edit');    // bool
  • grant() adds to the set and creates the connection when none exists.
  • revoke() removes from the set and throws ConnectionNotFound when there is no connection.
  • sync() replaces the set with exactly the given permissions and creates the connection when none exists.
  • clear() empties the set and, unlike sync(), never creates a connection — it throws ConnectionNotFound when there is none.
  • Pass the permissions explicitly — grant() and sync() don’t fall back to permissions staged with withPermissions().
  • ConnectionPermissionsChanged fires only when the set actually changes, carrying the previous and current lists.

The trait exposes the same edits as model methods — shorthand that runs through the same manager:

$user->grantThroughConnection($team, 'publish', 'archive');
$user->revokeThroughConnection($team, 'archive');
$user->syncConnectionPermissions($team, ['view', 'edit']);
$user->clearConnectionPermissions($team);

$user->permissionsThroughConnection($team);   // Collection<int, string> of the stored set

Checking permissions

use RoundlyConsulting\Connections\Facades\Connections;

Connections::between($user, $team)->permissions()->has('edit');              // bool
Connections::between($user, $team)->permissions()->hasAny('view', 'edit');   // bool
Connections::between($user, $team)->permissions()->hasAll('view', 'edit');   // bool

// Trait shorthand — the same checks from the connector's side.
$user->hasPermissionThroughConnection($team, 'edit');             // bool
$user->hasPermissionThroughConnection($team, 'edit', force: true);// bypass the cache
$user->hasAnyPermissionThroughConnection($team, 'view', 'edit');  // bool
$user->hasAllPermissionsThroughConnection($team, 'view', 'edit'); // bool

// On a Connection row you already hold.
$connection->hasPermission('edit');             // bool
$connection->hasAnyPermission('view', 'edit');  // bool
$connection->hasAllPermissions('view', 'edit'); // bool

Checks run from the connector to the connectable, and permissions()->has() is the same check as the trait’s hasPermissionThroughConnection(). While enforce_active_on_check is on (the default), a pending, blocked or expired connection grants nothing. Checks are memoised per pair for the request — pass force: true to the trait method to bypass the cache (see Caching).

permissions()->all() and the trait’s permissionsThroughConnection() return the stored set regardless of status or expiry — use the check methods for access decisions.

Wildcard permissions

A connection whose permission set contains * passes any permission check, and a segment wildcard like posts.* matches posts.edit. Sets without a wildcard behave exactly as before — this is purely opt-in by the stored data:

Connections::between($user, $team)->withPermissions('posts.*')->connect();
$user->hasPermissionThroughConnection($team, 'posts.edit');  // true
$user->hasPermissionThroughConnection($team, 'users.edit');  // false

Connections::between($admin, $team)->withPermissions('*')->connect();
$admin->hasPermissionThroughConnection($team, 'anything');   // true

The withPermission() scope and connectionsWithPermission() honour wildcards too: they match the exact permission, * and trailing segment wildcards such as posts.* or posts.comments.*. Any other pattern shape — posts.*.edit, for example — is honoured by hasPermission() and hasPermissionThroughConnection() only.

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 crypto

By 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.