---
title: "DI and actions — Connections for Laravel | Roundly"
description: "Inject ConnectionManager instead of the facade, or run single-purpose actions — with the full facade-to-action map, DTOs and exceptions."
url: https://roundly-consulting.com/open-source/docs/connections-for-laravel/actions
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Connections for Laravel](https://roundly-consulting.com/open-source/docs/connections-for-laravel.md)

# 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

```php
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:

```php
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:

```php
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

```php
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 on the connection model’s database. BulkConnect takes an optional trailing replaceMeta flag. |
| `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(); its mergePermissions flag (false by default) makes a write that lands on the pair’s live row add its permissions to the stored set instead of replacing it. PermissionSet is the immutable, de-duplicated permission list the actions work with. SyncTarget and SyncResult are covered in Bulk operations & sync.

```php
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'],
    mergePermissions: true,   // on a live row, add to the stored permissions instead of replacing them
);

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 crypto](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

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

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/connections-for-laravel.md)
