coderflex/laravel-ticket
A complete ticketing system for Laravel: create and manage support tickets with categories, labels, priorities, statuses, assignments, comments, and attachments. Includes a ready-to-use dashboard, authorization policies, and customizable views.
Install and wire up (5 minutes)
composer require coderflex/laravel-ticket
php artisan vendor:publish --tag="laravel-ticket-config"
php artisan vendor:publish --tag="laravel-ticket-migrations"
php artisan migrate
Add the package trait to your User model so tickets can be scoped to their owner:
use Coderflex\LaravelTicket\Concerns\HasTickets;
class User extends Authenticatable
{
use HasTickets;
}
If your models live outside App\Models (or you want to extend the package models), edit config/laravel-ticket.php before running migrations — the config points at the user_model, ticket_model, category_model, label_model, and ticket-message model the package resolves at runtime.
Where to look first
config/laravel-ticket.php — model bindings and status/priority definitions.user_id, title, status, priority, pivot tables for categories/labels, media table) exist, and therefore what you can query.vendor/coderflex/laravel-ticket/src/Models/Ticket.php — scopes, casts, and helper methods (open(), close(), attachLabels(), etc.) are the real API surface; the README is thin.First use case: open a ticket from a support form
use Coderflex\LaravelTicket\Models\{Ticket, Category, Label};
$ticket = auth()->user()->tickets()->create([
'title' => 'Charged twice this month',
'message' => 'My card was billed on the 3rd and the 5th.',
'priority' => 'high',
]);
$ticket->categories()->attach($billingCategoryId);
$ticket->labels()->attach($refundLabelId);
$ticket->messages()->create([
'user_id' => auth()->id(),
'message' => 'Invoice attached.',
]);
That single flow — create, categorize, label, append a message — is 90% of what a support inbox does; everything else is presentation and authorization.
Treat the package as the data layer, not the app. There is no Livewire/Inertia UI, no controllers, no policies, no notifications. Expect to build your own resource controllers and TicketPolicy around the models.
Owner scoping in queries. Use the trait relationship for "my tickets" and explicit queries for agent views:
// Customer
auth()->user()->tickets()->with(['categories', 'labels'])->latest()->paginate();
// Agent / admin queue
Ticket::with(['user', 'labels'])
->whereIn('status', ['open', 'in_progress'])
->orderByRaw("FIELD(priority, 'urgent','high','normal','low')")
->paginate(25);
Status and priority transitions. Move tickets through explicit domain actions rather than mass-assigning a string from a request:
$ticket->update(['status' => 'closed']);
// or, where the package provides the helper:
$ticket->close();
Wrap transitions in a small service class (TicketWorkflow) so you can fire your own events, stamp closed_at, and notify the customer in one place. The package does not emit events for status changes — you must do it.
Conversation threading. A ticket is a container; the actual conversation lives in messages. Always attach messages via the relationship, and record the author so you can distinguish customer replies from agent replies in the UI:
$ticket->messages()->create([
'user_id' => auth()->id(),
'message' => $request->validated('message'),
]);
Consider an is_internal boolean (your own migration) for agent-only notes — the package has no concept of them.
File attachments. Attachments ride on spatie/laravel-medialibrary. Run php artisan storage:link, configure your media disk, and attach through the media collection the ticket model declares:
$ticket->addMediaFromRequest('attachment')->toMediaCollection('tickets');
If you need virus scanning, size limits, or private disks, handle it in a FormRequest plus a custom media disk — the package ships none of that.
Categories vs labels. Categories are usually structural ("Billing", "Bug"), labels are ad-hoc ("refund", "v2.3"). Seed both via a seeder so the form dropdowns are populated on first deploy; both use slug-based pivots.
Custom models. To add columns or behaviour (e.g. assigned_to, closed_at, soft deletes):
App\Models\Ticket extends Coderflex\LaravelTicket\Models\Ticket.ticket_model in the config at your class.user_model, update the HasTickets trait usage accordingly.Do this on day one if you expect growth — swapping models after data exists is painful.
Integration with the rest of the app. Use model observers (TicketObserver) for notification fan-out (TicketCreated, TicketReplied), and a scheduled command for stale-ticket reminders (Ticket::where('status','open')->where('updated_at','<',now()->subDays(7))).
user_model, the foreign keys still point at users.Rule::enum(TicketStatus::class) (or Rule::in(config(...))) — a typo like in-progress vs in_progress will silently exclude tickets from every scope.TicketPolicy (view, update, reply, close) keyed on $ticket->user_id === $user->id OR an is_agent role. Skipping this is the single most common security bug in ticket integrations.assigned_to column and a belongsTo(User::class, 'assigned_to') relation. Plan for it before launch; retrofitting means backfilling a nullable column and rewriting every query.HasMedia interface. Keep storage:link in your deploy script and verify the disk in config/filesystems.php — attachment links 404 silently when the symlink is missing.with(['labels','categories','messages' => fn($q) => $q->latest()->limit(1)])) or use withCount('messages') for the inbox badge.close(), open(), attachLabels(), syncCategories() have been renamed/added across releases. Grep the installed src/ rather than trusting blog posts, and pin the version in composer.json.TicketFactory, CategoryFactory, LabelFactory for tests. Use RefreshDatabase and assert on pivot tables (assertDatabaseHas('label_ticket', [...])) since the relationship helpers hide the pivot name.Ticket::toSql() and DB::listen() reveal that category/label filters need whereHas('categories', fn($q) => $q->where('slug',$slug)) — a naive where('category_id', …) won't exist because it's a many-to-many pivot.config/laravel-ticket.php in production, run php artisan config:clear (or config:cache in deploy). Stale config manifests as "class not found" on an extended model.HasTickets trait (override per-project), model observers, and the media collections. Everything else — routing, UI, authorization, notifications — is intentionally left to you, which is fine for a package this small but means the first sprint is mostly scaffolding.How can I help you explore Laravel packages today?