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().
| Scope | Meaning |
|---|---|
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| Type | Visibility |
|---|---|
created, status_changed, priority_changed, category_changed, assigned, unassigned, due_changed, attachment_added, attachment_removed, requester_changed, overdue | public |
merged, merged_from | public, 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 |
replied | public — 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_deleted | as 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 |
updated | public 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, restored | internal (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 cryptoBy 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.