DI and actions
The facade is the recommended default, not the only way in. Three equivalent entry points run the same code:
- The Connections facade — the shortest form, used throughout these docs.
- The manager, RoundlyConsulting\Connections\ConnectionManager — the facade root, a container singleton, injected through the constructor. Same API, an explicit dependency and no static calls.
- Actions — single-purpose classes with execute(), for composing into your own actions, jobs and commands.
Inject the manager, or call an action
use RoundlyConsulting\Connections\Actions\GrantPermissions;
use RoundlyConsulting\Connections\ConnectionManager;
final class ShareTeam
{
public function __construct(private ConnectionManager $connections) {}
public function __invoke(User $user, Team $team): void
{
$this->connections->between($user, $team)->permissions()->grant('view');
}
}
// The raw action.
app(GrantPermissions::class)->execute($user, $team, 'view');Connections::fake() swaps the container binding as well as the facade, so an injected ConnectionManager — and every HasConnections trait write — is recorded in tests. An action you resolve and call yourself runs normally but isn’t recorded.
Facade method → action
| Facade method | Action |
|---|---|
between()->connect() · invite() | CreateConnection (execute / executeData) |
between()->accept() | AcceptConnection |
between()->block() | BlockConnection |
between()->disconnect() | DisconnectConnection |
between()->reconnect() · restore() | RestoreConnection |
between()->extend() | ExtendConnection |
between()->permissions()->grant() | GrantPermissions |
between()->permissions()->revoke() | RevokePermissions |
between()->permissions()->sync() | SyncPermissions |
between()->permissions()->clear() | ClearPermissions |
from()->toMany()->connectAll() | BulkConnect |
from()->toMany()->disconnectAll() | BulkDisconnect |
from()->toMany()->grantAll() · revokeAll() | GrantPermissions · RevokePermissions (per target) |
from()->sync() | SyncConnections |
Connections::prune() | PruneConnections |
exists(), find(), toggle(), expiring(), flushCache() and the permission reads (all(), has(), hasAny(), hasAll()) have no action of their own: they are queries, cache housekeeping or — for toggle() — a connect() or disconnect() chosen at call time.
Calling actions directly
Resolve an action from the container to run one operation:
use RoundlyConsulting\Connections\Actions\CreateConnection;
use RoundlyConsulting\Connections\Actions\GrantPermissions;
use RoundlyConsulting\Connections\Actions\RevokePermissions;
use RoundlyConsulting\Connections\Actions\SyncPermissions;
use RoundlyConsulting\Connections\Actions\ClearPermissions;
use RoundlyConsulting\Connections\Actions\ExtendConnection;
use RoundlyConsulting\Connections\Actions\DisconnectConnection;
use RoundlyConsulting\Connections\Actions\PruneConnections;
use RoundlyConsulting\Connections\Actions\SyncConnections;
use RoundlyConsulting\Connections\DataTransferObjects\SyncTarget;
app(CreateConnection::class)->execute($user, $team, collect(['view']), now()->addMonth());
app(GrantPermissions::class)->execute($user, $team, 'publish');
app(RevokePermissions::class)->execute($user, $team, 'publish');
app(SyncPermissions::class)->execute($user, $team, 'view', 'edit');
app(ClearPermissions::class)->execute($user, $team);
app(ExtendConnection::class)->execute($user, $team, now()->addYear());
app(DisconnectConnection::class)->execute($user, $team);
app(PruneConnections::class)->execute();
app(SyncConnections::class)->execute($user, [new SyncTarget($team)]);grant and sync auto-create the connection when none exists. disconnect, revoke, clear and extend throw ConnectionNotFound when there is no connection. CreateConnection::execute() takes the two models plus optional permissions, expiry, status, meta and a meta-replacement flag; executeData() takes the same input as a ConnectionData DTO. It follows the re-connect rules — a null argument keeps the stored value on an existing connection — and an explicit status it cannot move to (see canTransitionTo(), and never out of blocked) throws InvalidStatusTransition:
use RoundlyConsulting\Connections\Actions\CreateConnection;
use RoundlyConsulting\Connections\Enums\ConnectionStatus;
app(CreateConnection::class)->execute(
$user,
$team,
collect(['view']), // ?Collection — null: default_permissions when new, kept when existing
now()->addMonth(), // ?CarbonInterface — null: expiry.default when new, kept when existing
ConnectionStatus::Pending, // ?ConnectionStatus — null: default_status when new, kept when existing
['source' => 'import'], // ?array meta — null keeps the stored meta
replaceMeta: false, // true overwrites the stored meta instead of merging
);Status, restore, bulk and sync actions
use RoundlyConsulting\Connections\Actions\AcceptConnection;
use RoundlyConsulting\Connections\Actions\BlockConnection;
use RoundlyConsulting\Connections\Actions\RestoreConnection;
use RoundlyConsulting\Connections\Actions\BulkConnect;
use RoundlyConsulting\Connections\Actions\BulkDisconnect;
use RoundlyConsulting\Connections\Actions\SyncConnections;
use RoundlyConsulting\Connections\DataTransferObjects\SyncTarget;
app(AcceptConnection::class)->execute($user, $team); // Connection
app(BlockConnection::class)->execute($user, $team); // Connection
app(RestoreConnection::class)->execute($user, $team); // Connection
app(BulkConnect::class)->execute($user, [$teamA, $teamB], collect(['view'])); // Collection<Connection>
app(BulkDisconnect::class)->execute($user, [$teamA, $teamB]); // void
app(SyncConnections::class)->execute($user, [new SyncTarget($teamA, ['view'])]); // SyncResult| Action | Behaviour |
|---|---|
CreateConnection | Create the pair or change only what is supplied; revives a soft-deleted pair. Fires ConnectionCreated (+ ConnectionInvited when pending), ConnectionUpdated, or ConnectionRestored for a blocked pair. Throws InvalidStatusTransition. |
DisconnectConnection | Soft-delete; fires ConnectionRemoved. Throws ConnectionNotFound. |
GrantPermissions | Add permissions, creating the connection when absent. |
RevokePermissions | Remove permissions. Throws ConnectionNotFound. |
SyncPermissions | Replace the set, creating the connection when absent. |
ClearPermissions | Empty the set; never creates a connection. Throws ConnectionNotFound. |
ExtendConnection | Set or clear expires_at; fires ConnectionUpdated. Throws ConnectionNotFound. |
PruneConnections | Soft-delete every expired connection, drop the in-request cache; returns the count. |
AcceptConnection / BlockConnection | Change the status (no-op when unchanged); AcceptConnection is the one explicit unblock. Throws ConnectionNotFound. |
RestoreConnection | Restore the trashed row as it was (ConnectionRestored) or create a fresh one. |
BulkConnect / BulkDisconnect | Run CreateConnection / DisconnectConnection over a list in one transaction. |
SyncConnections | Reconcile to a list of SyncTarget in one transaction, reviving detached targets; returns SyncResult. |
Data transfer objects
ConnectionData carries a full connection write into CreateConnection::executeData(); PermissionSet is the immutable, de-duplicated permission list the actions work with. SyncTarget and SyncResult are covered in Bulk operations & sync.
use RoundlyConsulting\Connections\Actions\CreateConnection;
use RoundlyConsulting\Connections\DataTransferObjects\ConnectionData;
use RoundlyConsulting\Connections\DataTransferObjects\PermissionSet;
use RoundlyConsulting\Connections\Enums\ConnectionStatus;
$data = ConnectionData::fromModels(
$user,
$team,
permissions: PermissionSet::make('view', 'edit'),
expiresAt: now()->addMonth(),
status: ConnectionStatus::Pending,
meta: ['source' => 'import'],
);
app(CreateConnection::class)->executeData($data, $user, $team);
$set = PermissionSet::make('view')->add('edit', 'view')->remove('view');
$set->all(); // ['edit'] — always de-duplicated
$set->has('edit'); // true
$set->isEmpty(); // false
$set->toCollection(); // Collection<int, string>Exceptions
| Exception | Thrown when |
|---|---|
ConnectionsException | Abstract base of the package exceptions — catch it to handle all three below. |
ConnectionNotFound | disconnect, revoke, clear, extend, accept or block runs on a pair with no connection. |
InvalidStatusTransition | A status change canTransitionTo() refuses, or a connect-side verb would lift a block — e.g. invite() over an accepted or blocked connection. |
MissingConnectable | A terminal verb runs without a connectable, or a bulk verb without toMany() targets. |
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.