Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Ticket Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

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

  1. config/laravel-ticket.php — model bindings and status/priority definitions.
  2. The published migrations — they tell you exactly which columns (user_id, title, status, priority, pivot tables for categories/labels, media table) exist, and therefore what you can query.
  3. 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.

Implementation Patterns

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

  1. Create App\Models\Ticket extends Coderflex\LaravelTicket\Models\Ticket.
  2. Point ticket_model in the config at your class.
  3. Re-publish/duplicate the migration or add a supplementary migration for new columns.
  4. If you change 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))).

Gotchas and Tips

  • Publish config before migrations. The config drives which model classes the package resolves; if you migrate with defaults and later change user_model, the foreign keys still point at users.
  • Enum vs string values. Status and priority are stored as strings but filtered through the package's enum/config constants. Validate incoming values with Rule::enum(TicketStatus::class) (or Rule::in(config(...))) — a typo like in-progress vs in_progress will silently exclude tickets from every scope.
  • No authorization, no rate limiting, no spam protection. These are yours. Write a 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.
  • No assignee concept by default. "Assign to agent" requires your own 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.
  • Media library is a hard dependency. If you don't need attachments, you still get the tables and the 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.
  • N+1 everywhere. Ticket listings almost always render labels, categories, and the last message. Eager load (with(['labels','categories','messages' => fn($q) => $q->latest()->limit(1)])) or use withCount('messages') for the inbox badge.
  • Scopes and helper methods differ between versions. 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.
  • Factory coverage is thin. Write your own TicketFactory, CategoryFactory, LabelFactory for tests. Use RefreshDatabase and assert on pivot tables (assertDatabaseHas('label_ticket', [...])) since the relationship helpers hide the pivot name.
  • Debugging query behaviour: 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 caching bites. After editing 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.
  • Extension points to lean on: model config bindings, the 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.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin