React to things that happen elsewhere in TruCMS, move slow work out of the request, and run housekeeping on a timer. All of it is wired from your plugin's service provider.
These are the events the current code actually fires. Listening to anything else in the codebase is not safe until you have checked that something dispatches it.
| Event | Payload | Fired when |
|---|---|---|
App\Events\AdminBooting | none | Every admin request, after the core sidebar and settings are registered. The hook for sidebar items, settings sections and menu-builder sources |
Illuminate\Auth\Events\Registered | $event->user | A visitor registers. The core listener creates their team and assigns the default role |
Konekt\User\Events\UserWasActivated / UserWasInactivated | the user | User::activate() / inactivate() |
App\Events\TicketOpenEvent | $event->ticket | A support ticket is created (fired after the transaction commits) |
App\Events\TicketMessageEvent | $event->ticket, the message | A reply is posted |
App\Events\TicketCloseEvent | $event->ticket | A ticket is closed. The core has no listener for it, so a closing notification is yours to add |
Vanilo\Payment\Events\PaymentCreated, PaymentCompleted, PaymentPartiallyReceived, PaymentDeclined, PaymentTimedOut | $event->getPayment() | TruPayments applies a gateway response or a manual status change. Check getPayable() to see whose payment it is |
Modules\TruPayments\Events\RefundCreated | $event->refund | A refund record is created (after commit) |
Modules\TruPayments\Events\RefundStatusChanged | refund, from, to (RefundStatus enums) | A refund moves between states (after commit) |
| Eloquent model events | the model | created, updated, deleted… on any model, core or module. The most reliable way to follow bookings, orders and products |
cache:cleared | store, tags | php artisan cache:clear. The core listens to flush the response cache |
Booking's own events are not dispatched
The Booking module declares BookingCreated, BookingUpdated, BookingStatusChanged, BookingCheckedIn and BookingPaymentStatusChanged and maps them to a listener, but nothing in the current code fires them: booking notifications are sent directly by NotificationService. To react to bookings, observe the Modules\Booking\Models\Booking model (created, updated, and wasChanged('status') inside updated), or listen to the payment events above for payment changes.
For a handful of listeners, register them in boot():
Modules/Testimonials/app/Providers/TestimonialsServiceProvider.php
use Illuminate\Support\Facades\Event;
use Modules\Booking\Models\Booking;
use Modules\Testimonials\Listeners\AskForTestimonial;
use Nwidart\Modules\Facades\Module;
public function boot(): void
{
parent::boot();
Event::listen(TicketCloseEvent::class, NotifyTicketClosed::class);
// Only touch another module's classes when that module is enabled.
if (Module::isEnabled('Booking')) {
Booking::updated(function (Booking $booking): void {
if ($booking->wasChanged('status') && $booking->status === 'completed') { // AppointmentStatus::Completed
app(AskForTestimonial::class)->handle($booking);
}
});
}
}For many listeners, add a child EventServiceProvider with a $listen map and list it in your provider's $providers array, as Booking does (Modules/Booking/app/Providers/EventServiceProvider.php).
Module::isEnabled() or class_exists(). If your plugin cannot work at all without one, declare it in requires instead.ShouldQueue. Keep them quick, or queue them.ShouldHandleEventsAfterCommit (or the event's own after-commit behaviour) when you react to data written in a transaction; the core ticket observer and TruPayments refunds do.Define plain event classes in app/Events with Dispatchable and SerializesModels, and dispatch them with MyEvent::dispatch($model). Document them for other plugin authors, and keep the payload stable once released.
Queues run synchronously by default
QUEUE_CONNECTION ships as sync, so a queued job runs inside the request that dispatched it. Site owners can choose Database, Redis, Beanstalkd or SQS in Settings → General → Queue, and must then run a worker. Write jobs that are correct in both modes.
Modules/Testimonials/app/Jobs/SendTestimonialRequest.php
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
class SendTestimonialRequest implements ShouldQueue
{
use Queueable;
public function __construct(public int $customerId) {} // pass ids, not models with big relations
public function handle(): void
{
// ...
}
}
// dispatching from a transaction: wait until it commits
SendTestimonialRequest::dispatch($customer->id)->afterCommit();SendBookingNotificationJob and SyncBookingIntegrationsJob take ids and are dispatched afterCommit().sync runs the job immediately, an exception in the job surfaces in the visitor's request. Catch and report failures that should not break the page.BROADCAST_DRIVER=log, no Echo or Pusher client). Do not build features that need real-time pushes without adding one.Override configureSchedules() in your module provider. The base provider calls it once the application has booted, so your tasks appear in php artisan schedule:list only while the plugin is enabled.
Modules/Testimonials/app/Providers/TestimonialsServiceProvider.php
use Illuminate\Console\Scheduling\Schedule;
use Modules\Testimonials\Console\PruneRejectedTestimonials;
protected array $commands = [
PruneRejectedTestimonials::class, // registered by parent::boot()
];
protected function configureSchedules(Schedule $schedule): void
{
$schedule->command('testimonials:prune-rejected')->dailyAt('04:10')->withoutOverlapping();
}Name commands {alias}:{verb}-{noun}, use withoutOverlapping() for anything that can run long, and pick a time that does not collide with the bundled tasks:
| Command | Owner | Schedule |
|---|---|---|
pages:refresh-bound | Core (routes/console.php) | Hourly |
booking:cleanup-payment-intents | Booking | Hourly |
booking:renew-calendar-watches | Booking | Daily 03:20 |
trucommerce:checkout-intents:expire, trucommerce:stock:release-holds | TruCommerce | Every 15 minutes |
trucommerce:cleanup, trucommerce:optimize, trucommerce:media:generate-missing-conversions | TruCommerce | Daily 02:10, 02:30, 03:00 |
None of this runs unless the server's cron calls php artisan schedule:run every minute; the end-user installation guide tells site owners how to set it up. dotartisan:tickets:autoclose exists but is not scheduled by default.
mail and database channels (the notifications table exists). Logged-in visitors see database notifications in their account area..env. A fresh site uses the log mailer until the owner configures SMTP, so never treat "mail sent" as "mail delivered".use Illuminate\Support\Facades\Bus;
use Illuminate\Support\Facades\Event;
it('queues a thank-you job when a testimonial is published', function (): void {
Bus::fake();
$testimonial = Testimonial::factory()->create(['is_published' => false]);
$testimonial->update(['is_published' => true]);
Bus::assertDispatched(SendThankYou::class);
});
it('registers the prune task', function (): void {
$this->artisan('schedule:list')->expectsOutputToContain('testimonials:prune-rejected');
});The test environment forces QUEUE_CONNECTION=sync, so jobs you do not fake run inline. Booking has no model factories; when your plugin reacts to bookings, create the rows you need in the test itself.