---
title: "DI and actions — Tickets for Laravel | Roundly"
description: "Skip the facade: inject TicketsManager or call a single-purpose action — every host-facing action with its execute() signature and facade path."
url: https://roundly-consulting.com/open-source/docs/tickets-for-laravel/without-facade
language: en
---

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

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

# DI and actions

The facade is the recommended default, not the only way in. Three entry points run the same code: the Tickets facade (shortest), the manager RoundlyConsulting\\Tickets\\TicketsManager — the facade’s root, a container singleton — injected through the constructor (same API, explicit dependency, no static calls), and actions (final readonly classes with one execute(), for composing into your own actions, jobs and commands):

```php
use RoundlyConsulting\Tickets\Actions\Assignment\AssignTicketAction;
use RoundlyConsulting\Tickets\Facades\Tickets;
use RoundlyConsulting\Tickets\TicketsManager;

// Facade
Tickets::for($ticket)->by($agent)->assign($user);

// Dependency injection: the same API
public function __construct(private TicketsManager $tickets) {}
$this->tickets->for($ticket)->by($agent)->assign($user);

// The raw action
app(AssignTicketAction::class)->execute($ticket, $user, actor: $agent);
```

## Inject the manager

```php
use Illuminate\Http\Request;
use RoundlyConsulting\Comments\Models\Comment;
use RoundlyConsulting\Tickets\TicketsManager;

final class TicketReplyController
{
    public function __construct(private TicketsManager $tickets) {}

    public function __invoke(Request $request, string $reference): Comment
    {
        return $this->tickets->for($reference)->by($request->user())->reply($request->input('body'));
    }
}
```

## Call an action

```php
use RoundlyConsulting\Tickets\Actions\Merge\MergeTicketsAction;

app(MergeTicketsAction::class)->execute($source, $target, reason: 'duplicate', actor: $agent);
```

?Model $actor = null, bool $system = false work the same everywhere: null falls back to the authenticated user (with tickets.actor.from\_auth, on tickets.actor.guard), system: true means nobody. ConvertGuestAction and DeleteCategoryAction take only $actor — the facade passes none, so the authenticated user is credited.

## Facade method → action

Namespaces are relative to RoundlyConsulting\\Tickets\\Actions:

| Action | execute(…) | Returns | Facade path |
| --- | --- | --- | --- |
| `Tickets\CreateTicketAction` | `CreateTicketData $data` | `Ticket` | `Tickets::create(), Tickets::make()->…->create()` |
| `Tickets\UpdateTicketAction` | `Ticket $ticket, UpdateTicketData $data, ?Model $actor = null, bool $system = false` | `Ticket` | `->update()` |
| `Tickets\ChangePriorityAction` | `Ticket $ticket, BackedEnum\|string $priority, ?Model $actor = null, bool $system = false` | `Ticket` | `->prioritize()` |
| `Tickets\ChangeCategoryAction` | `Ticket $ticket, TicketCategory\|int\|null $category, ?Model $actor = null, bool $system = false` | `Ticket` | `->categorize()` |
| `Tickets\SetDueDateAction` | `Ticket $ticket, ?CarbonInterface $dueAt, ?Model $actor = null, bool $system = false` | `Ticket` | `->due()` |
| `Tickets\ChangeRequesterAction` | `Ticket $ticket, Model\|GuestData $requester, ?Model $actor = null, bool $system = false` | `Ticket` | `->changeRequester()` |
| `Tickets\DeleteTicketAction` | `Ticket $ticket, ?Model $actor = null, bool $system = false` | `Ticket` | `->delete()` |
| `Tickets\RestoreTicketAction` | `Ticket $ticket, ?Model $actor = null, bool $system = false` | `Ticket` | `->restore()` |
| `Tickets\TransitionTicketAction` | `Ticket $ticket, TransitionData $data, ?Model $actor = null, bool $system = false` | `TransitionResult` | `->transition() and the named conveniences` |
| `Conversation\PostReplyAction` | `Ticket $ticket, ReplyData $data, ?TransitionData $transition = null, ?Model $actor = null, bool $system = false` | `Comment` | `->reply(), $ticket->reply()` |
| `Conversation\PostNoteAction` | `Ticket $ticket, ReplyData $data, ?Model $actor = null, bool $system = false` | `Comment` | `->note(), $ticket->note()` |
| `Conversation\UpdateCommentAction` | `Ticket $ticket, Comment $comment, string $body, ?Model $actor = null, bool $system = false` | `Comment` | `->editComment()` |
| `Conversation\DeleteCommentAction` | `Ticket $ticket, Comment $comment, ?Model $actor = null, bool $system = false` | `Comment` | `->deleteComment()` |
| `Assignment\AssignTicketAction` | `Ticket $ticket, Model $assignee, ?Model $actor = null, bool $system = false` | `Ticket` | `->assign(), $ticket->assignTo()` |
| `Assignment\UnassignTicketAction` | `Ticket $ticket, ?Model $actor = null, bool $system = false` | `Ticket` | `->unassign()` |
| `Assignment\ClaimTicketAction` | `Ticket $ticket, ?Model $actor = null, bool $system = false` | `Ticket` | `->claim()` |
| `Watchers\AddWatcherAction` | `Ticket $ticket, Model $watcher, ?Model $actor = null, bool $system = false` | `Ticket` | `->watchers()->add() / watch()` |
| `Watchers\RemoveWatcherAction` | `Ticket $ticket, Model $watcher, ?Model $actor = null, bool $system = false` | `Ticket` | `->watchers()->remove() / unwatch()` |
| `Tags\AttachTagsAction` | `Ticket $ticket, array $tags, ?Model $actor = null, bool $system = false` | `Ticket` | `->tags()->attach()` |
| `Tags\DetachTagsAction` | `Ticket $ticket, array $tags, ?Model $actor = null, bool $system = false` | `Ticket` | `->tags()->detach()` |
| `Tags\SyncTagsAction` | `Ticket $ticket, array $tags, ?Model $actor = null, bool $system = false` | `Ticket` | `->tags()->sync()` |
| `Tags\CreateTagAction` | `TagData $data, bool $orExisting = false` | `TicketTag` | `Tickets::tags()->create() / findOrCreate()` |
| `Tags\UpdateTagAction` | `TicketTag $tag, UpdateTagData $data` | `TicketTag` | `Tickets::tags()->update() / rename() / recolor()` |
| `Tags\DeleteTagAction` | `TicketTag $tag` | `TicketTag` | `Tickets::tags()->delete()` |
| `Links\LinkTicketsAction` | `Ticket $ticket, Ticket $related, LinkType $type, ?Model $actor = null, bool $system = false` | `TicketLink` | `->links()->add()` |
| `Links\UnlinkTicketsAction` | `Ticket $ticket, Ticket $related, LinkType $type, ?Model $actor = null, bool $system = false` | `Ticket` | `->links()->remove()` |
| `Merge\MergeTicketsAction` | `Ticket $source, Ticket $target, ?string $reason = null, ?Model $actor = null, bool $system = false` | `MergeResult` | `->mergeInto()` |
| `Attachments\AddAttachmentAction` | `Ticket $ticket, UploadedFile\|string $file, ?Model $actor = null, bool $system = false` | `Media` | `->attachments()->add()` |
| `Attachments\RemoveAttachmentAction` | `Ticket $ticket, Media\|string $media, ?Model $actor = null, bool $system = false` | `Ticket` | `->attachments()->remove()` |
| `Fields\SetFieldValuesAction` | `Ticket $ticket, array $values, ?Model $actor = null, bool $system = false` | `Ticket` | `->fields()->set() / setMany()` |
| `Fields\ForgetFieldValuesAction` | `Ticket $ticket, array $keys, ?Model $actor = null, bool $system = false` | `Ticket` | `->fields()->forget()` |
| `Fields\DefineFieldAction` | `FieldData $data` | `TicketField` | `Tickets::fields()->define() / make()->…->define()` |
| `Fields\UpdateFieldAction` | `TicketField $field, UpdateFieldData $data` | `TicketField` | `Tickets::fields()->update()` |
| `Fields\DeleteFieldAction` | `TicketField $field` | `TicketField` | `Tickets::fields()->delete()` |
| `Fields\RestoreFieldAction` | `TicketField $field` | `TicketField` | `Tickets::fields()->restore()` |
| `Fields\PruneOrphanFieldValuesAction` | `bool $dryRun = false, int $chunk = 200` | `int` | `Tickets::fields()->prune(), tickets:prune-fields` |
| `Categories\CreateCategoryAction` | `CategoryData $data` | `TicketCategory` | `Tickets::categories()->create() / make()->…->create()` |
| `Categories\UpdateCategoryAction` | `TicketCategory $category, UpdateCategoryData $data` | `TicketCategory` | `Tickets::categories()->update()` |
| `Categories\MoveCategoryAction` | `TicketCategory $category, TicketCategory\|int\|null $parent, ?int $position = null` | `TicketCategory` | `Tickets::categories()->move()` |
| `Categories\DeleteCategoryAction` | `TicketCategory $category, TicketCategory\|int\|null $reassignTo = null, ?Model $actor = null` | `TicketCategory` | `Tickets::categories()->delete()` |
| `Categories\RestoreCategoryAction` | `TicketCategory $category` | `TicketCategory` | `Tickets::categories()->restore()` |
| `Guests\ResolveGuestAction` | `GuestData $data` | `TicketGuest` | `Tickets::guests()->resolve()` |
| `Guests\UpdateGuestAction` | `TicketGuest $guest, GuestData $data` | `TicketGuest` | `Tickets::guests()->update()` |
| `Guests\ConvertGuestAction` | `TicketGuest $guest, Model $user, ?Model $actor = null` | `int` | `Tickets::guests()->convert()` |
| `Guests\IssueGuestLinkAction` | `Ticket $ticket, ?Model $guest = null` | `string` | `->guestLink(), Tickets::guests()->linkFor()` |
| `Guests\RevokeGuestAccessAction` | `Ticket $ticket, ?Model $actor = null, bool $system = false` | `Ticket` | `->revokeGuestAccess()` |
| `Due\SweepDueDatesAction` | `—` | `SweepResult` | `Tickets::sweep(), tickets:sweep` |

- Handles and sub-accessors resolve each action from the container, so a host binding of an action applies to every path — facade, injected manager, handle, builder and model method.
- Tickets::fake() swaps the manager in the container too, so an injected TicketsManager is faked as well. Calling an action directly is not recorded — only manager calls are.
- The internal building blocks are not on the facade — don’t call them: Internal\\AllocateReferenceAction, RecordActivityAction, ApplyAutomationAction, AutoWatchAction, ResolveAuthorRoleAction, ResolveTagsAction, ChangeTicketTagsAction, WriteFieldValuesAction, WriteTicketCommentAction and BumpGuestAccessAction.

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