---
title: "DI, actions & DTOs — Teams for Laravel | Roundly"
description: "Inject TeamsManager instead of the facade, or call the 17 single-purpose actions with their typed DTOs from your own jobs and commands."
url: https://roundly-consulting.com/open-source/docs/teams-for-laravel/actions-and-dtos
language: en
---

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

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

# DI, actions & DTOs

The facade is the recommended default, not the only way in. The package offers three equivalent entry points that run the same code:

- The Teams facade — the shortest; use it in everyday application code.
- The manager, RoundlyConsulting\\Teams\\TeamsManager — the facade root and a container singleton. Inject it through the constructor for the same API with an explicit dependency and no static calls.
- The actions in RoundlyConsulting\\Teams\\Actions — final readonly, single-purpose classes with execute(), for composing the raw use case into your own actions, jobs or commands.

## Injecting the manager

```php
use RoundlyConsulting\Teams\Models\Invite;
use RoundlyConsulting\Teams\Models\Team;
use RoundlyConsulting\Teams\TeamsManager;

final class InviteController
{
    public function __construct(private TeamsManager $teams) {}

    public function store(Team $team): Invite
    {
        return $this->teams->for($team)->invites()->create(role: 'member');
    }
}
```

Teams::fake() swaps the TeamsManager container binding too, so a constructor-injected manager receives the fake in tests.

## Calling an action

```php
use RoundlyConsulting\Teams\Actions\AddMemberAction;
use RoundlyConsulting\Teams\Actions\CreateInviteAction;
use RoundlyConsulting\Teams\DataTransferObjects\AddMemberData;
use RoundlyConsulting\Teams\DataTransferObjects\CreateInviteData;

// The raw actions — the behaviour the manager and facade both run.
$invite = app(CreateInviteAction::class)->execute($team, new CreateInviteData(
    role: 'admin',
    email: 'jane@example.com',
    invitedBy: $currentUser,
));

app(AddMemberAction::class)->execute($team, new AddMemberData(
    member: $user,
    role: 'admin',
    meta: ['invited_via' => 'import'],
    expiresAt: now()->addYear(),
));
```

The manager resolves every action from the container, so a binding you register for an action class applies to the facade too. Calling an action directly skips the team-scope checks the handles perform and isn’t seen by Teams::fake() — prefer the manager in application code. All 17 actions are host-facing; none is internal.

## Facade method → action

| Facade method | Action | execute(…) | Dispatches |
| --- | --- | --- | --- |
| `Teams::create()` | `CreateTeamAction` | `CreateTeamData $data` | — (adds the owner via AddMemberAction) |
| `for($t)->members()->add()` | `AddMemberAction` | `Team $team, AddMemberData $data` | TeamMemberAdded / TeamMemberRoleChanged |
| `for($t)->members()->remove()` | `RemoveMemberAction` | `Team $team, Model $member` | TeamMemberDeleted |
| `for($t)->members()->changeRole()` | `ChangeMemberRoleAction` | `Team $team, Model $member, string $role` | TeamMemberRoleChanged |
| `for($t)->transferOwnershipTo()` | `TransferOwnershipAction` | `Team $team, Model $newOwner` | TeamOwnershipTransferred |
| `for($t)->invites()->create()` | `CreateInviteAction` | `Team $team, CreateInviteData $data` | InviteCreated |
| `Teams::invites()->accept()` | `AcceptInviteAction` | `Invite $invite, AcceptInviteData $data` | InviteAccepted |
| `for($t)->invites()->resend()` | `ResendInviteAction` | `Invite $invite` | InviteResent |
| `for($t)->invites()->revoke()` | `RevokeInviteAction` | `Invite $invite` | InviteRevoked |
| `Teams::invites()->prune()` | `PruneInvitesAction` | — | — |
| `for($t)->roles()->define()` | `DefineTeamRoleAction` | `DefineTeamRoleData $data` | — |
| `for($t)->joinRequests()->open()` | `RequestToJoinAction` | `RequestToJoinData $data` | JoinRequestCreated (+ JoinRequestApproved) |
| `for($t)->joinRequests()->approve()` | `ApproveJoinRequestAction` | `JoinRequest $request, RespondToJoinRequestData $data` | JoinRequestApproved |
| `for($t)->joinRequests()->deny()` | `DenyJoinRequestAction` | `JoinRequest $request, RespondToJoinRequestData $data` | JoinRequestDenied |
| `Teams::joinRequests()->expire()` | `ExpireJoinRequestsAction` | — | JoinRequestExpired (per row) |
| `Teams::members()->prune()` | `PruneExpiredMembersAction` | — | MembershipExpired (per row) |
| `Teams::members()->expiring() / notifyExpiring()` | `DispatchExpiringMembershipsAction` | `?int $withinDays = null, bool $notify = true` | MembershipExpiringSoon (per row, when $notify) |

## DTOs

The DTOs in RoundlyConsulting\\Teams\\DataTransferObjects are final readonly classes. The facade and manager build them from named arguments, so you only need them for direct action calls:

```php
new CreateTeamData(
    string $name,
    bool $isPublic = false,
    ?Model $owner = null,
    array $meta = [],
);

new AddMemberData(
    Model $member,
    string $role,
    array $meta = [],
    ?CarbonInterface $expiresAt = null,
    ?int $acceptedInviteId = null,   // set internally on invite acceptance
);

new CreateInviteData(
    string $role,
    ?CarbonInterface $expiresAt = null,   // null → invites.expires_after
    ?string $email = null,                // null → an open invite
    ?Model $invitedBy = null,
    array $meta = [],
    ?int $maxUses = 1,                    // null → unlimited seats
);

new AcceptInviteData(
    Model $member,
    ?string $email = null,   // must match an email-targeted invite
);

new DefineTeamRoleData(
    int $teamId,
    string $key,
    string $name,
    array $permissions = [],   // list<string|Permission>
    string $description = '',
);

new RequestToJoinData(
    Team $team,
    Model $requester,
    ?string $requestedRole = null,
    ?string $message = null,
    array $meta = [],
    ?CarbonInterface $expiresAt = null,       // null → never lapses
    array $approvers = [],                    // non-empty → route through approvals-for-laravel
    ?ApprovalRule $approvalRule = null,       // null → teams.approvals.rule
    ?int $approvalQuorum = null,              // null → teams.approvals.quorum
);

new RespondToJoinRequestData(
    Model $responder,
    ?string $role = null,   // overrides the requested/default role on approval
);
```

## 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/teams-for-laravel.md)
