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

Invitations & status

Every connection has a status — pending, accepted or blocked. connect() creates accepted connections by default; invite() creates a pending one that the receiving side accepts or blocks:

// Connector side: send an invitation (a pending connection).
Connections::between($user, $team)->withPermissions('view')->invite();

// Receiving side accepts or blocks.
$team->acceptConnectionFrom($user);  // status → accepted (now active)
$team->blockConnectionFrom($user);   // status → blocked (grants nothing)

// Or drive it from the connector / builder.
Connections::between($user, $team)->accept();
Connections::between($user, $team)->block();
$user->inviteConnection($team);

// Model helpers.
$connection->isPending();
$connection->isAccepted();
$connection->isBlocked();
$connection->isActive();   // accepted AND not expired

Only active connections — accepted and not expired — grant permissions and count for isConnectedTo() while enforce_active_on_check is on (the default). A pending invitation or a blocked connection grants nothing.

  • acceptConnectionFrom() and blockConnectionFrom() are called on the receiving side with the connector as the argument.
  • accept() and block() throw ConnectionNotFound when the pair has no connection.
  • Both are no-ops without an event when the connection is already in that state; otherwise they fire ConnectionAccepted or ConnectionBlocked.
  • accept() is the one explicit way to lift a block — acceptConnectionFrom() on the receiving side.
  • Creating a pending connection fires ConnectionCreated followed by ConnectionInvited.

Status transitions are enforced

Status only moves the way ConnectionStatus::canTransitionTo() allows, and every action enforces it:

  • connect(), toggle(), connectAll() and sync() never change an existing connection’s status — re-connecting can’t self-accept a pending invitation, demote an accepted one or lift a block.
  • invite() over a pending invitation is fine; over an accepted or blocked connection it throws RoundlyConsulting\Connections\Exceptions\InvalidStatusTransition.
  • A block is lifted only by an explicit accept(). It also survives disconnect() and connections:prune: re-connecting a soft-deleted blocked pair restores it still blocked, firing ConnectionRestored.
// Re-connecting never changes an existing connection's status.
Connections::between($user, $team)->invite();    // pending
Connections::between($user, $team)->connect();   // still pending — no self-accept
Connections::between($user, $team)->invite();    // fine — re-inviting a pending invitation

$team->acceptConnectionFrom($user);              // accepted
Connections::between($user, $team)->invite();    // throws InvalidStatusTransition

// Only an explicit accept() lifts a block — it even survives disconnect() and a prune.
$team->blockConnectionFrom($user);
Connections::between($user, $team)->disconnect();
Connections::between($user, $team)->connect();   // restored still blocked (ConnectionRestored)
$team->acceptConnectionFrom($user);              // accepted — the block is lifted

Invitations by default

Set default_status to pending to make every new connection an invitation — connect() then creates pending connections and fires ConnectionInvited for each. For a single call, asPending() stages the pending state:

// config/connections.php — every new connection starts as an invitation
'default_status' => 'pending',

// Or for a single call: asPending() stages the pending state.
Connections::between($user, $team)->asPending()->withPermissions('view')->connect();
// same as ->invite()

The ConnectionStatus enum

The status column is cast to the string-backed Enums\ConnectionStatus enum. Its canTransitionTo() guard allows accepting from pending or blocked (accept() on a blocked link is the explicit unblock), blocking from any state, moving back to pending never, and a transition to the current state always. Every action enforces it, and the connect-side verbs are stricter still — they never lift a block. Use it in your own UI and validation too:

use RoundlyConsulting\Connections\Enums\ConnectionStatus;

$connection->status;   // ConnectionStatus::Accepted (cast from the status column)

ConnectionStatus::Pending->canTransitionTo(ConnectionStatus::Accepted);   // true
ConnectionStatus::Blocked->canTransitionTo(ConnectionStatus::Accepted);   // true — accept() is the explicit unblock
ConnectionStatus::Accepted->canTransitionTo(ConnectionStatus::Blocked);   // true — blocking is always allowed
ConnectionStatus::Accepted->canTransitionTo(ConnectionStatus::Pending);   // false

The enum uses the shared Helpers trait from enums-for-laravel, so it also ships labels, select options and a validation rule:

use RoundlyConsulting\Connections\Enums\ConnectionStatus;

ConnectionStatus::values();          // Collection: ['pending', 'accepted', 'blocked']
ConnectionStatus::labels();          // Collection: ['Pending', 'Accepted', 'Blocked']
ConnectionStatus::toOptions();       // Collection: ['pending' => 'Pending', ...] for <select>
ConnectionStatus::toArray();         // array: the plain-array form of toOptions()
ConnectionStatus::options();         // Collection<EnumOption> for JS/Inertia selects
ConnectionStatus::validationRule();  // 'in:pending,accepted,blocked'
ConnectionStatus::Accepted->readable();       // 'Accepted'
ConnectionStatus::tryFromLabel('Accepted');   // ConnectionStatus::Accepted

$request->validate([
    'status' => ['required', ConnectionStatus::validationRule()],
]);

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.