hackzilla/ticket-bundle
A simple ticketing/helpdesk bundle for Symfony apps. Users can create and reply to tickets with priorities, categories, statuses, attachments, and email notifications, while admins manage queues and respond from the backend. Integrates with existing user providers.
This is a Symfony bundle, not a Laravel package. There is no service provider, no Eloquent model, no Blade views, and no config/ticket.php. Everything you read below assumes a Symfony application (or a Symfony service inside a hybrid stack); a pure Laravel app cannot composer require this and get a working ticketing system — see Gotchas first if that is your situation.
Minimum steps in a Symfony app:
composer require hackzilla/ticket-bundle
config/bundles.php:
Hackzilla\Bundle\TicketBundle\HackzillaTicketBundle::class => ['all' => true],
config/packages/hackzilla_ticket.php is the format to copy from first; YAML still works if that is your house style (use one format per bundle, never both):
return static function (HackzillaTicketConfig $config): void {
$config->userClass(App\Entity\User::class);
// ticket/message/attachment/category/priority/status class overrides,
// templates, mailer + from_email
};
Whatever you copy, DependencyInjection/Configuration.php is the authoritative key list, not the README, and not a blog post.bin/console doctrine:schema:validate then doctrine:migrations:diff and inspect the generated ticket, ticket_message, ticket_attachment, ticket_category, ticket_priority, ticket_status tables before running them.Where to look first, in this order: Manager/TicketManagerInterface (the entire public API), Manager/UserManager (new findUserByUsername() in 3.8.0 — the supported way to resolve a user from a string, useful for assignment widgets and imports), Entity/TicketInterface + Entity/MessageInterface (your extension surface), Resources/views/ (what you will override), Event/ (the hooks). Ignore the rest until you need it.
First use case: an internal support inbox. Inject TicketManagerInterface, create a ticket from a form, list tickets by status, and render the bundle's Twig templates from your own controller. There is no bundled CRUD UI; the routing definitions shipped with the bundle were modernized in 3.8.0 and an XML variant was added, but treat those as reference resources and check Resources/config/routing* for actual route names before importing anything. Routing, authorization, and pagination are yours to build — roughly 100 lines of controller is the fastest path to a working prototype.
Always go through the manager, never the repository. TicketManager::createTicket(), updateTicket(), getTicket(), and the message/attachment helpers wrap persistence and dispatch events and trigger notifications. Persisting a Ticket with the EntityManager directly silently skips both, which is the number-one source of "why didn't the email go out" bugs.
public function __construct(private TicketManagerInterface $tickets) {}
public function open( User $user, string $subject, string $body ): TicketInterface
{
$ticket = $this->tickets->createTicket();
$ticket->setSubject($subject)
->setCreatedBy($user)
->setPriority($defaultPriority)
->setCategory($defaultCategory)
->setStatus($openStatus);
$this->tickets->updateTicket($ticket);
$this->tickets->createMessage($ticket, $user, $body); // initial body is a message
return $ticket;
}
Custom entities via interface + config. Subclass the bundle entities (or implement the interfaces and add #[ORM\Entity]), then point hackzilla_ticket.ticket_class etc. at yours. This is the supported extension point — do not fork the bundle. For the polymorphic createdBy/assignedTo relation, wire Doctrine's ResolveTargetEntityListener so user_class resolves at runtime; that is what makes the bundle reusable across apps, and it is entirely independent of which user provider you use (3.8.0 dropped the FOSUserBundle assumption — do not re-add it).
Resolving users by name. UserManager::findUserByUsername() (3.8.0) is the new, provider-agnostic entry point. Use it in admin "assign to" form types, CSV imports, or CLI commands instead of injecting your own UserRepository — it respects the configured user_class and keeps the bundle swappable.
Reuse the forms, don't rebuild them. Form/TicketType, TicketMessageType, and the filter form already encode the relation choices. Extend them with ->add() in your own form class; a from-scratch form usually forgets the status/priority EntityType query builders and produces empty dropdowns on fresh databases.
Override templates through config or convention. Either set paths under hackzilla_ticket.templates or use Symfony's standard templates/bundles/HackzillaTicketBundle/ override directory. The bundled markup is Bootstrap-3-ish and opinionated; most teams override the list, show, and form partials within a day.
Events are the integration seam. Subscribe to the ticket and ticket-message events to push to Slack, fire webhooks, or write an audit log. If you need email beyond the bundled notifier, disable the built-in mailer in config and handle it in a listener — you get retries, queuing, and testability that the synchronous built-in mailer does not offer.
Attachments need an owner. The attachment entity handles the uploaded file and move-on-persist via lifecycle callbacks. You must set upload_dir to a writable path and provide your own controller/route to serve files back with authorization checks — the bundle will not do that for you, and serving an unauthenticated upload directory is the classic leak here.
Multilingual, precisely: the bundle translates its interface through Symfony's translator catalogs, not user content. Add translations/HackzillaTicketBundle.<locale>.yaml for your locale. If you need genuinely per-locale ticket bodies, model that yourself as one message per locale or a translatable field — the package gives you the UI plumbing only.
Hybrid Laravel stacks: the only sane pattern is to run the Symfony app as the ticketing service and have Laravel call it over HTTP (or share the database read-only). Do not try to boot the bundle inside Laravel.
symfony/* and doctrine/* constraints that conflict with the versions Laravel already requires. Even when install succeeds, none of the wiring (DI container, event dispatcher, Twig) exists. Treat it as reference architecture, not a dependency.composer.json in the tag you install and run a real composer update --dry-run in a throwaway branch before committing to an upgrade. Budget for a fork if you need a version pair the constraints do not cover.hackzilla_ticket.yaml and hackzilla_ticket.php — the merge order will surprise you. Verify what actually loaded with bin/console debug:config hackzilla_ticket.UserLoad constructor fix (#201). If you were pinned to a 3.7.x patch and had subclassed or decorated the user loader, re-check your signature after upgrading; the argument list changed.migrations:diff. Regenerate and diff carefully after changing any *_class config key — table names and FKs shift underneath you.user_class must be a Doctrine entity. Laravel's Authenticatable is not one. In Symfony, the bundle assumes the security token's user; anonymous ticket creation needs an explicit fallback user or an early access_denied.updateTicket(). Disable it in test and dev, and wrap in production or replace it with a listener + queue.bin/console debug:container hackzilla, debug:config hackzilla_ticket, debug:event-dispatcher (filter on the ticket events), debug:router after importing any bundle routing resource, doctrine:mapping:info, and doctrine:schema:validate after any config change.TicketManagerInterface → fork. Reaching for the fork first is how these projects die; the interface decorator covers 90% of "I need behavior X on every ticket write."How can I help you explore Laravel packages today?