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

The Connections facade

The Connections facade is the package’s public API — every operation is reachable from it, and it is the recommended way in. Its root is RoundlyConsulting\Connections\ConnectionManager, a container singleton, and the HasConnections trait verbs delegate to the same manager (see DI and actions for the other entry points):

MethodReturnsPurpose
Connections::between($connector, $connectable)PendingConnectionFluent builder for one pair.
Connections::from($connector)PendingConnectionBuilder with the connectable set later (to(), toMany()) or a sync() reconcile.
Connections::expiring(int $days = 7)Builder<Connection>Every live connection expiring within the window, across all connectors.
Connections::prune()intSoft-delete expired connections, returning the count.
Connections::flushCache()voidDrop the in-request permission cache.
Connections::fake()ConnectionFakeRecording fake for tests — see Testing.

between() starts a builder for a pair; from() starts one for a connector and defers the connectable to to():

use RoundlyConsulting\Connections\DataTransferObjects\SyncTarget;
use RoundlyConsulting\Connections\Facades\Connections;

// Create the connection, or update the pair's existing one, with permissions and an expiry.
Connections::between($user, $team)
    ->withPermissions('view', 'edit')
    ->expiresIn(now()->addMonth())   // CarbonInterface, CarbonInterval, or seconds
    ->connect();

// Defer the connectable until later with from()->to().
Connections::from($user)->to($team)->withPermissions('view')->connect();

// Move the expiry, or clear it with null.
Connections::between($user, $team)->extend(now()->addYear());
Connections::between($user, $team)->extend(null);

// Look the pair up.
Connections::between($user, $team)->exists();   // bool — an active connection exists
Connections::between($user, $team)->find();     // ?Connection — in any status

// Remove the connection (soft delete).
Connections::between($user, $team)->disconnect();

// Reconcile a connector to exactly this set (connect missing, disconnect extras).
Connections::from($user)->sync([$teamA, new SyncTarget($teamB, permissions: ['read'])]); // SyncResult

// Connections expiring within 14 days, across every connector.
Connections::expiring(14)->with('connector')->get();

// Remove all expired connections, returning how many were removed.
$removed = Connections::prune();

// Drop the in-request permission cache mid-request (it already resets per request / job).
Connections::flushCache();

The cache is invalidated automatically on every write, so a check after a write reflects the change without any force flag.

Staging the connection

Modifiers stage state on the builder; nothing is written until a terminal verb runs:

use Carbon\CarbonInterval;
use RoundlyConsulting\Connections\Facades\Connections;

$connection = Connections::from($user)
    ->to($team)                              // the connectable (or start with between())
    ->withPermissions('view', 'edit')        // staged permissions, de-duplicated
    ->withPermissions('publish')             // repeated calls add to the staged set
    ->expiresIn(CarbonInterval::days(30))    // CarbonInterface, CarbonInterval or seconds
    ->withMeta(['source' => 'signup'])       // merged into the meta bag
    ->connect();                             // terminal verb: returns the Connection
  • to($connectable) — set the connectable when the builder started with from().
  • toMany($connectables) — stage several connectables for the bulk verbs (see Bulk operations & sync).
  • withPermissions(...$permissions) — stage permissions; repeated calls add to the set, duplicates are dropped.
  • expiresIn($value) — a CarbonInterface instant, a CarbonInterval from now, or an integer of seconds from now.
  • expiringAt(?$instant) — an exact expiry instant, or null for none.
  • withMeta($array) / replaceMeta() — stage metadata; merged into the stored meta unless replaceMeta() is called.
  • asPending() — create the connection in the pending state; invite() is sugar for asPending()->connect().

Terminal verbs

VerbReturnsDoes
connect()ConnectionCreate the pair, or change only what is staged on its existing connection; revives a soft-deleted pair.
invite()Connectionconnect() in the pending state; throws InvalidStatusTransition over an accepted or blocked connection.
accept() / block()ConnectionChange the status; throw ConnectionNotFound when there is no connection. accept() is the only way to lift a block.
disconnect()voidSoft-delete; throws ConnectionNotFound when there is no connection.
toggle()?ConnectionDisconnect an active connection (returns null), otherwise connect — never lifting a block or renewing an expiry.
reconnect() / restore()ConnectionRestore the soft-deleted connection with its permissions, expiry and meta, or connect afresh.
permissions()ConnectionPermissionsThe pair’s permission set: grant, revoke, sync, clear, all, has, hasAny, hasAll — see Permissions.
extend(?$expiresAt)ConnectionMove or clear the expiry; throws ConnectionNotFound.
exists()boolWhether the pair is connected (active only, while enforce_active_on_check is on).
find()?ConnectionThe pair’s connection in any status, or null when there is none (or it is soft-deleted).
sync($connectables)SyncResultReconcile the connector to exactly this set of Connectable or SyncTarget items — see Bulk operations & sync.
connectAll() / grantAll() / revokeAll()CollectionBulk verbs over the toMany() targets.
disconnectAll()voidBulk disconnect over the toMany() targets.

The permissions() sub-accessor

between()->permissions() is the permission set of one connection — grant(), revoke(), sync() and clear() write it; all(), has(), hasAny() and hasAll() read it (see 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

Re-connecting is safe

A pair has at most one connection row, and connect() on a pair that already has one only changes what you stage: its status, permissions, expiry and meta are kept unless restated (meta is merged — see Metadata). The config defaults — default_permissions, default_status and expiry.default — apply to new connections only. To clear an expiry use extend(null); to change one attribute, use its dedicated verb:

Connections::between($user, $team)
    ->withPermissions('view', 'edit')
    ->expiresIn(now()->addMonth())
    ->connect();

// Re-connecting only changes what you stage: status, permissions and expiry are kept.
Connections::between($user, $team)->withMeta(['note' => 'hi'])->connect();   // merge; perms + expiry kept

// Config defaults (default_permissions, default_status, expiry.default) apply to new
// connections only. To clear an expiry or change one attribute, use its dedicated verb:
Connections::between($user, $team)->extend(null);
Connections::between($user, $team)->permissions()->grant('publish');
Connections::between($user, $team)->accept();

After a disconnect() or a prune, connect() — and grant(), permissions()->sync(), invite(), toggle() and sync() — revives the pair as a fresh connection, except a blocked one, which stays blocked (see Invitations & status).

Terminal verbs need a connectable. Running one after from() without to() — or a bulk verb without toMany() — throws MissingConnectable:

Connections::from($user)->connect();       // throws MissingConnectable — call to() first
Connections::from($user)->connectAll();    // throws MissingConnectable — call toMany() first

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.