---
title: "Filing and editing tickets — Tickets for Laravel | Roundly"
description: "File tickets with the builder or CreateTicketData, fill defaults from categories, then edit, recategorise, hand over, delete and restore them."
url: https://roundly-consulting.com/open-source/docs/tickets-for-laravel/filing-tickets
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)

# Filing and editing tickets

Tickets::make() returns a TicketBuilder; create() files the ticket and returns it. Only the subject and a requester (a host model or guest fields) are required:

```php
$ticket = Tickets::make()
    ->subject('Printer on floor 3 is jammed')               // required
    ->description('Paper jam error E-102.')
    ->requester($user)                                      // a host model…
    ->guest(['name' => 'Jo', 'email' => 'jo@acme.test'])    // …or guest fields (GuestData|array)
    ->category($hardware)                                   // model, id or null
    ->priority('high')                                      // value or enum case
    ->project($helpdesk)                                    // a TicketProject
    ->assignee($agent)
    ->due(now()->addDay())
    ->channel('web')
    ->fields(['asset_tag' => 'PRN-0042'])
    ->tags(['printer'])                                     // names or TicketTag models
    ->watchers($colleague)
    ->attach($request->file('photo'))                       // UploadedFile or a local path — never user input as a path
    ->meta(['crm_id' => 'C-1'])                             // host-owned metadata; top-level keys merge
    ->by($agent)                                            // the actor (default: the auth user, with actor.from_auth)
    ->create();
```

asSystem() files it with no actor; data() returns the CreateTicketData instead of creating. An inactive category is refused for a new ticket, and the channel must be one of tickets.channels. From PHP, attachments take uploads or local paths — never pass user input as a path.

## Defaults at creation

- Values the caller leaves out are filled once, at creation: the nearest ancestor category that sets one, then config — priority (tickets.priority.default), assignee (Contracts\\AssignmentStrategy), due date (Contracts\\DueDateResolver) and channel (tickets.default\_channel). Custom field defaults are filled too.
- Every contract sees the stored ticket: the assignee and the due date are picked right after the row is inserted, in the same transaction — the ticket has its key, reference, number and initial status, its requester, project, category, priority and channel, but not yet its custom field values, tags, watchers or attachments.
- Changing a ticket’s category later re-applies nothing.

## The DTO form

```php
use RoundlyConsulting\Tickets\DataTransferObjects\CreateTicketData;

$ticket = Tickets::create(new CreateTicketData(
    subject: 'Printer on floor 3 is jammed',
    requester: $customer,
    category: $hardware,
    priority: 'high',
    channel: 'web',
    fields: ['asset_tag' => 'PRN-0042'],
));
```

## From the requester or the project

```php
// Concerns\RequestsTickets on your user model — the requester is set
$ticket = $user->openTicket()->subject('VPN drops every hour')->create();

// Concerns\HasTickets on your project model — numbered in the project's sequence
$ticket = $project->openTicket()->subject('…')->requester($user)->create();   // SUP-1
```

## Finding tickets

```php
Tickets::find('SUP-42');                        // ?Ticket — live tickets only
Tickets::find('SUP-42', withTrashed: true);     // a soft-deleted one too
Tickets::findOrFail('SUP-42');                  // TicketNotFoundException otherwise
Tickets::for('SUP-42');                         // a handle; an unknown or soft-deleted reference throws
```

## Editing a ticket

```php
use RoundlyConsulting\Tickets\DataTransferObjects\UpdateTicketData;

// Only what is given changes; null clears a nullable value
Tickets::for($ticket)->by($agent)->update(new UpdateTicketData(subject: 'Printer on floor 3 jams on A3', description: null));

Tickets::for($ticket)->by($agent)->prioritize('urgent');         // TicketPriorityChanged
Tickets::for($ticket)->by($agent)->categorize($printers);        // TicketCategoryChanged; defaults are not re-applied
Tickets::for($ticket)->by($agent)->changeRequester($colleague);  // a model or GuestData
```

- UpdateTicketData fields default to the Support\\Optional sentinel (not given), so only what you pass changes and null can clear a nullable value. TicketUpdated fires with the changed keys (subject, description, meta).
- categorize() does not re-apply category defaults, but a newly required field of the target category needs a value.
- changeRequester() takes the ticket away from the previous requester: their automatic watch is removed silently (a watch an agent added on purpose stays), and when the previous requester was a guest every guest link to the ticket is revoked (GuestAccessRevoked).
- Setting the same priority again fires nothing.

## Deleting and restoring

```php
Tickets::for($ticket)->by($manager)->delete();                               // soft delete
Tickets::for(Tickets::findOrFail('SUP-42', withTrashed: true))->restore();   // bring it back
```

A soft-deleted ticket keeps its number, and every mutation of it throws TicketTrashedException until it is restored. In the API, delete and restore need the manage ability held by an agent for the ticket (see Policy and agent scope).

## The actor

by($actor) attributes the following calls; by(null) falls back to the authenticated user (tickets.actor.from\_auth on tickets.actor.guard); asSystem() means nobody. The actor is what activity entries, events and notifications credit, and what the domain rules judge — an agent role, a requester, a participant.

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