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

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):

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

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

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:

Actionexecute(…)ReturnsFacade path
Tickets\CreateTicketActionCreateTicketData $dataTicketTickets::create(), Tickets::make()->…->create()
Tickets\UpdateTicketActionTicket $ticket, UpdateTicketData $data, ?Model $actor = null, bool $system = falseTicket->update()
Tickets\ChangePriorityActionTicket $ticket, BackedEnum|string $priority, ?Model $actor = null, bool $system = falseTicket->prioritize()
Tickets\ChangeCategoryActionTicket $ticket, TicketCategory|int|null $category, ?Model $actor = null, bool $system = falseTicket->categorize()
Tickets\SetDueDateActionTicket $ticket, ?CarbonInterface $dueAt, ?Model $actor = null, bool $system = falseTicket->due()
Tickets\ChangeRequesterActionTicket $ticket, Model|GuestData $requester, ?Model $actor = null, bool $system = falseTicket->changeRequester()
Tickets\DeleteTicketActionTicket $ticket, ?Model $actor = null, bool $system = falseTicket->delete()
Tickets\RestoreTicketActionTicket $ticket, ?Model $actor = null, bool $system = falseTicket->restore()
Tickets\TransitionTicketActionTicket $ticket, TransitionData $data, ?Model $actor = null, bool $system = falseTransitionResult->transition() and the named conveniences
Conversation\PostReplyActionTicket $ticket, ReplyData $data, ?TransitionData $transition = null, ?Model $actor = null, bool $system = falseComment->reply(), $ticket->reply()
Conversation\PostNoteActionTicket $ticket, ReplyData $data, ?Model $actor = null, bool $system = falseComment->note(), $ticket->note()
Conversation\UpdateCommentActionTicket $ticket, Comment $comment, string $body, ?Model $actor = null, bool $system = falseComment->editComment()
Conversation\DeleteCommentActionTicket $ticket, Comment $comment, ?Model $actor = null, bool $system = falseComment->deleteComment()
Assignment\AssignTicketActionTicket $ticket, Model $assignee, ?Model $actor = null, bool $system = falseTicket->assign(), $ticket->assignTo()
Assignment\UnassignTicketActionTicket $ticket, ?Model $actor = null, bool $system = falseTicket->unassign()
Assignment\ClaimTicketActionTicket $ticket, ?Model $actor = null, bool $system = falseTicket->claim()
Watchers\AddWatcherActionTicket $ticket, Model $watcher, ?Model $actor = null, bool $system = falseTicket->watchers()->add() / watch()
Watchers\RemoveWatcherActionTicket $ticket, Model $watcher, ?Model $actor = null, bool $system = falseTicket->watchers()->remove() / unwatch()
Tags\AttachTagsActionTicket $ticket, array $tags, ?Model $actor = null, bool $system = falseTicket->tags()->attach()
Tags\DetachTagsActionTicket $ticket, array $tags, ?Model $actor = null, bool $system = falseTicket->tags()->detach()
Tags\SyncTagsActionTicket $ticket, array $tags, ?Model $actor = null, bool $system = falseTicket->tags()->sync()
Tags\CreateTagActionTagData $data, bool $orExisting = falseTicketTagTickets::tags()->create() / findOrCreate()
Tags\UpdateTagActionTicketTag $tag, UpdateTagData $dataTicketTagTickets::tags()->update() / rename() / recolor()
Tags\DeleteTagActionTicketTag $tagTicketTagTickets::tags()->delete()
Links\LinkTicketsActionTicket $ticket, Ticket $related, LinkType $type, ?Model $actor = null, bool $system = falseTicketLink->links()->add()
Links\UnlinkTicketsActionTicket $ticket, Ticket $related, LinkType $type, ?Model $actor = null, bool $system = falseTicket->links()->remove()
Merge\MergeTicketsActionTicket $source, Ticket $target, ?string $reason = null, ?Model $actor = null, bool $system = falseMergeResult->mergeInto()
Attachments\AddAttachmentActionTicket $ticket, UploadedFile|string $file, ?Model $actor = null, bool $system = falseMedia->attachments()->add()
Attachments\RemoveAttachmentActionTicket $ticket, Media|string $media, ?Model $actor = null, bool $system = falseTicket->attachments()->remove()
Fields\SetFieldValuesActionTicket $ticket, array $values, ?Model $actor = null, bool $system = falseTicket->fields()->set() / setMany()
Fields\ForgetFieldValuesActionTicket $ticket, array $keys, ?Model $actor = null, bool $system = falseTicket->fields()->forget()
Fields\DefineFieldActionFieldData $dataTicketFieldTickets::fields()->define() / make()->…->define()
Fields\UpdateFieldActionTicketField $field, UpdateFieldData $dataTicketFieldTickets::fields()->update()
Fields\DeleteFieldActionTicketField $fieldTicketFieldTickets::fields()->delete()
Fields\RestoreFieldActionTicketField $fieldTicketFieldTickets::fields()->restore()
Fields\PruneOrphanFieldValuesActionbool $dryRun = false, int $chunk = 200intTickets::fields()->prune(), tickets:prune-fields
Categories\CreateCategoryActionCategoryData $dataTicketCategoryTickets::categories()->create() / make()->…->create()
Categories\UpdateCategoryActionTicketCategory $category, UpdateCategoryData $dataTicketCategoryTickets::categories()->update()
Categories\MoveCategoryActionTicketCategory $category, TicketCategory|int|null $parent, ?int $position = nullTicketCategoryTickets::categories()->move()
Categories\DeleteCategoryActionTicketCategory $category, TicketCategory|int|null $reassignTo = null, ?Model $actor = nullTicketCategoryTickets::categories()->delete()
Categories\RestoreCategoryActionTicketCategory $categoryTicketCategoryTickets::categories()->restore()
Guests\ResolveGuestActionGuestData $dataTicketGuestTickets::guests()->resolve()
Guests\UpdateGuestActionTicketGuest $guest, GuestData $dataTicketGuestTickets::guests()->update()
Guests\ConvertGuestActionTicketGuest $guest, Model $user, ?Model $actor = nullintTickets::guests()->convert()
Guests\IssueGuestLinkActionTicket $ticket, ?Model $guest = nullstring->guestLink(), Tickets::guests()->linkFor()
Guests\RevokeGuestAccessActionTicket $ticket, ?Model $actor = null, bool $system = falseTicket->revokeGuestAccess()
Due\SweepDueDatesAction—SweepResultTickets::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

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.