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

Querying and the activity timeline

Plain Eloquent scopes on Ticket. The API’s list filters are built on them, so PHP and HTTP filter identically:

use RoundlyConsulting\Tickets\Enums\StatusCategory;
use RoundlyConsulting\Tickets\Models\Ticket;

Tickets::visibleTo($user)->unresolved()->orderByPriority()->paginate();
Ticket::query()->assignedTo($agent)->withStatusCategory(StatusCategory::Waiting)->get();
Ticket::query()->inCategory($hardware, descendants: true)->overdue()->count();
Ticket::query()->taggedWith('printer', 'vip')->createdFrom(now()->subWeek())->get();
Ticket::query()->search('TCK-42')->first();

In host code that must follow a model swap, query through Support\TicketModels::ticket()::query().

ScopeMeaning
visibleTo(Model $user)The tickets TicketPolicy::view allows (Tickets::visibleTo()).
assignedTo(Model ...$assignees) / unassigned()Assignee morph.
requestedBy(Model $requester)Requester morph (a user or a TicketGuest).
inProject(?Model $project)null = tickets without a project.
inCategory(TicketCategory|int $category, bool $descendants = false)With its subcategories (bounded by categories.max_depth).
withStatus(BackedEnum|string ...$states)Exact state values.
withStatusCategory(StatusCategory ...$categories)The states of the configured enum in those categories.
unresolved()Open, Active or Waiting.
withPriority(BackedEnum|string ...$priorities)Any of the priorities.
taggedWith(string|TicketTag ...$tags)Any of the tags, by slug or model.
watchedBy(Model $watcher)Tickets the model watches.
whereField(string $key, mixed $value) / whereFieldBetween(string $key, mixed $from, mixed $to)Custom field values, cast to the field’s type; a null bound of the range is open. An unknown, encrypted or multiselect key throws UnknownTicketFieldException; the range takes number, decimal, date and datetime fields only.
dueBefore / dueAfter(CarbonInterface), dueFrom / dueTo, overdue()Before / after are exclusive, from / to inclusive; overdue = past due and unresolved.
createdFrom / createdTo(CarbonInterface)Inclusive.
viaChannel(string ...$channels)Intake channel.
search(string $term)The reference exactly, or subject/description LIKE (wildcards matched literally).
merged(bool $merged = true)merged_into_id set / not set.
orderByPriority(string $direction = 'desc')By the enum weights; an unknown stored value sorts as 0.

The activity timeline

Every change — status, priority, owner, due date, tags, fields, merges, edits and deletions of messages — goes into an append-only timeline, written inside each action’s transaction (status changes inside lifecycle’s transition transaction):

Tickets::for($ticket)->activity();                 // public entries, oldest first — safe for a requester-facing page
Tickets::for($ticket)->activity(internal: true);   // every entry, for agents
TypeVisibility
created, status_changed, priority_changed, category_changed, assigned, unassigned, due_changed, attachment_added, attachment_removed, requester_changed, overduepublic
merged, merged_frompublic, counting the published messages only (comments), no reason — plus an internal entry of the same type with every message counted and the reason, when those differ
repliedpublic — internal for a reply the host’s moderation held (properties.held: true, plus properties.transition when it asked for one it did not get)
comment_edited, comment_deletedas visible as the message: public for a published public reply, internal for a note or a held reply; properties comment (id) and role — never a body
updatedpublic for subject / description; a change of the host meta is a separate internal entry
note_added, fields_changed, tagged, untagged, linked, unlinked, watcher_added, watcher_removed, guest_access_revoked, deleted, restoredinternal (agent-only)

Entries carry the actor, an is_system flag, the visibility and properties — ids, keys, from/to; never a note body or an encrypted value. An update or delete throws ActivityIsAppendOnlyException, and a force-deleted ticket cascades. tickets.activity.enabled off writes nothing and the endpoint returns []. The API filters entries by role.

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.