Back to Course |
Laravel User Timezones Project: Convert, Display, Send Notifications

Cleaning up Controllers: Events/Listeners

As you may have noticed, our Controller looks big and has a lot of repetitive code. Let's clean it up a bit.


The Plan

Our plan for the cleanup will be moving things around and extracting some code into separate methods. This includes:

  • Introducing Events and Listeners
  • Moving the code that creates the Notifications into the Booking Model (and creating a method for it)

Creating Events

To create the events, we can run the following commands:

php artisan make:event BookingCreatedEvent
php artisan make:event BookingUpdatedEvent
php artisan make:event BookingDeletedEvent

And once we have the files, we can add the Booking parameter to the constructor:

app/Events/BookingCreatedEvent.php

use App\Models\Booking;
use Illuminate\Foundation\Events\Dispatchable;
 
class BookingCreatedEvent
{
use Dispatchable;
 
public function __construct(public Booking $booking)
{
}
}

app/Events/BookingUpdatedEvent.php

use App\Models\Booking;
use Illuminate\Foundation\Events\Dispatchable;
 
class BookingUpdatedEvent
{
use Dispatchable;
 
public function __construct(public Booking $booking)
{
}
}

app/Events/BookingDeletedEvent.php

use App\Models\Booking;
use Illuminate\Foundation\Events\Dispatchable;
 
class BookingDeletedEvent
{
use Dispatchable;
 
public function __construct(public Booking $booking)
{
}
}

Keep in mind that we are using PHP 8.1 features here, so if you are using an older version, you will need to change the constructor to:

public Booking $booking;
 
public function __construct(Booking $booking)
{
$this->booking = $booking;
}

Instead of:

public function __construct(public Booking $booking)
{
}

Creating Listeners

Creating listeners is similar to creating events:

php artisan make:listener BookingCreatedListener --event=BookingCreatedEvent
php artisan make:listener BookingUpdatedListener --event=BookingUpdatedEvent
php artisan make:listener BookingDeletedListener --event=BookingDeletedEvent

Let's modify our Listeners to look like this:

app/Listeners/BookingCreatedListener.php

use App\Events\BookingCreatedEvent;
use Carbon\CarbonImmutable;
 
class BookingCreatedListener
{
public function __construct()
{
}
 
public function handle(BookingCreatedEvent $event): void
{
$booking = $event->booking;
$booking->load('user');
$startTime = CarbonImmutable::parse(toUserDateTime($booking->start, $booking->user), $booking->user->timezone);
 
$booking->createReminderNotifications($booking, $startTime);
}
}

app/Listeners/BookingUpdatedListener.php

use App\Events\BookingUpdatedEvent;
use App\Models\Booking;
use App\Models\ScheduledNotification;
use Carbon\CarbonImmutable;
 
class BookingUpdatedListener
{
public function __construct()
{
}
 
public function handle(BookingUpdatedEvent $event): void
{
$booking = $event->booking;
$booking->load('user');
$startTime = CarbonImmutable::parse(toUserDateTime($booking->start, $booking->user), $booking->user->timezone);
 
$hasScheduledNotifications = ScheduledNotification::query()
->where('notifiable_id', $booking->id)
->where('notifiable_type', Booking::class)
->where('user_id', $booking->user_id)
->exists();
 
// First we need to check if there are any already scheduled notifications
if ($hasScheduledNotifications) {
// Then in this example, we simply delete them. You can however update them if you want.
$booking->scheduledNotifications()
->where('user_id', $booking->user_id)
->delete();
}
 
// Since we are clearing the scheduled notifications, we need to create them again for the new date
$booking->createReminderNotifications($booking, $startTime);
}
}

app/Listeners/BookingDeletedListener.php

use App\Events\BookingDeletedEvent;
 
class BookingDeletedListener
{
public function __construct()
{
}
 
public function handle(BookingDeletedEvent $event): void
{
$event->booking->scheduledNotifications()
->where('user_id', $event->booking->user_id)
->delete();
}
}

Next, we'll introduce the createReminderNotifications method in the Booking Model.


Using Model to Create Notifications

Let's create a method in the Booking Model:

app/Models/Booking.php

use App\Notifications\BookingReminder1H;
use App\Notifications\BookingReminder2H;
use App\Notifications\BookingReminder5MIN;
use App\Notifications\BookingStartedNotification;
use Carbon\CarbonImmutable;
 
// ...
 
public function createReminderNotifications(Booking $booking, CarbonImmutable $startTime): void
{
// Schedule 2H reminder
$twoHoursTime = fromUserDateTime($startTime->subHours(2), $booking->user);
if (now('UTC')->lessThan($twoHoursTime)) {
$booking->user->scheduledNotifications()->create([
'notification_class' => BookingReminder2H::class,
'notifiable_id' => $booking->id,
'notifiable_type' => __CLASS__,
'sent' => false,
'processing' => false,
'scheduled_at' => $twoHoursTime,
'sent_at' => null,
'tries' => 0,
]);
}
// Schedule 1H reminder
$oneHourTime = fromUserDateTime($startTime->subHour(), $booking->user);
if (now('UTC')->lessThan($oneHourTime)) {
$booking->user->scheduledNotifications()->create([
'notification_class' => BookingReminder1H::class,
'notifiable_id' => $booking->id,
'notifiable_type' => __CLASS__,
'sent' => false,
'processing' => false,
'scheduled_at' => $oneHourTime,
'sent_at' => null,
'tries' => 0,
]);
}
// Schedule 5 min reminder
$fiveMinutesTime = fromUserDateTime($startTime->subMinutes(5), $booking->user);
if (now('UTC')->lessThan($fiveMinutesTime)) {
$booking->user->scheduledNotifications()->create([
'notification_class' => BookingReminder5MIN::class,
'notifiable_id' => $booking->id,
'notifiable_type' => __CLASS__,
'sent' => false,
'processing' => false,
'scheduled_at' => $fiveMinutesTime,
'sent_at' => null,
'tries' => 0,
]);
}
// Schedule started reminder
$startingTime = fromUserDateTime($startTime, $booking->user);
if (now('UTC')->lessThan($startingTime)) {
$booking->user->scheduledNotifications()->create([
'notification_class' => BookingStartedNotification::class,
'notifiable_id' => $booking->id,
'notifiable_type' => __CLASS__,
'sent' => false,
'processing' => false,
'scheduled_at' => $startingTime,
'sent_at' => null,
'tries' => 0,
]);
}
}

This cleans our Controller quite a lot and makes our code reusable in creating or updating cases.


Registering Events and Listeners

The last step is registering them in the EventServiceProvider:

app/Providers/EventServiceProvider.php

use App\Events\BookingCreatedEvent;
use App\Events\BookingDeletedEvent;
use App\Events\BookingUpdatedEvent;
use App\Listeners\BookingCreatedListener;
use App\Listeners\BookingDeletedListener;
use App\Listeners\BookingUpdatedListener;
use Illuminate\Auth\Events\Registered;
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Event;
 
class EventServiceProvider extends ServiceProvider
{
/**
* The event to listener mappings for the application.
*
* @var array<class-string, array<int, class-string>>
*/
protected $listen = [
// ...
BookingCreatedEvent::class => [
BookingCreatedListener::class,
],
BookingUpdatedEvent::class => [
BookingUpdatedListener::class,
],
BookingDeletedEvent::class => [
BookingDeletedListener::class,
],
];
// ...
}

Using the Events

Lastly, we need to use the events. We will do that in the BookingController:

app/Http/Controllers/BookingController.php

 
use App\Events\BookingCreatedEvent;
use App\Events\BookingDeletedEvent;
use App\Events\BookingUpdatedEvent;
use App\Http\Requests\StoreBookingRequest;
use App\Http\Requests\UpdateBookingRequest;
use App\Models\Booking;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
 
class BookingController extends Controller
{
// ...
 
public function store(StoreBookingRequest $request): RedirectResponse
{
$booking = $request->user()->bookings()->create([
'start' => fromUserDateTime($request->validated('start')),
'end' => fromUserDateTime($request->validated('end')),
]);
 
$startTime = CarbonImmutable::parse(toUserDateTime($booking->start, $booking->user), $booking->user->timezone);
 
// Schedule 2H reminder
$twoHoursTime = fromUserDateTime($startTime->subHours(2), $booking->user);
if (now('UTC')->lessThan($twoHoursTime)) {
$booking->user->scheduledNotifications()->create([
'notification_class' => BookingReminder2H::class,
'notifiable_id' => $booking->id,
'notifiable_type' => Booking::class,
'sent' => false,
'processing' => false,
'scheduled_at' => $twoHoursTime,
'sent_at' => null,
'tries' => 0,
]);
}
// Schedule 1H reminder
$oneHourTime = fromUserDateTime($startTime->subHour(), $booking->user);
if (now('UTC')->lessThan($oneHourTime)) {
$booking->user->scheduledNotifications()->create([
'notification_class' => BookingReminder1H::class,
'notifiable_id' => $booking->id,
'notifiable_type' => Booking::class,
'sent' => false,
'processing' => false,
'scheduled_at' => $oneHourTime,
'sent_at' => null,
'tries' => 0,
]);
}
// Schedule 5 min reminder
$fiveMinutesTime = fromUserDateTime($startTime->subMinutes(5), $booking->user);
if (now('UTC')->lessThan($fiveMinutesTime)) {
$booking->user->scheduledNotifications()->create([
'notification_class' => BookingReminder5MIN::class,
'notifiable_id' => $booking->id,
'notifiable_type' => Booking::class,
'sent' => false,
'processing' => false,
'scheduled_at' => $fiveMinutesTime,
'sent_at' => null,
'tries' => 0,
]);
}
// Schedule started reminder
$startingTime = fromUserDateTime($startTime, $booking->user);
if (now('UTC')->lessThan($startingTime)) {
$booking->user->scheduledNotifications()->create([
'notification_class' => BookingStartedNotification::class,
'notifiable_id' => $booking->id,
'notifiable_type' => Booking::class,
'sent' => false,
'processing' => false,
'scheduled_at' => $startingTime,
'sent_at' => null,
'tries' => 0,
]);
}
 
event(new BookingCreatedEvent($booking));
 
return redirect()->route('booking.index');
}
 
// ...
 
public function update(UpdateBookingRequest $request, Booking $booking): RedirectResponse
{
$booking->update([
'start' => fromUserDateTime($request->validated('start')),
'end' => fromUserDateTime($request->validated('end')),
]);
 
$startTime = CarbonImmutable::parse(toUserDateTime($booking->start, $booking->user), $booking->user->timezone);
 
$hasScheduledNotifications = ScheduledNotification::query()
->where('notifiable_id', $booking->id)
->where('notifiable_type', Booking::class)
->where('user_id', $booking->user_id)
->exists();
 
// First we need to check if there are any already scheduled notifications
if ($hasScheduledNotifications) {
// Then in this example, we simply delete them. You can however update them if you want.
$booking->scheduledNotifications()
->where('user_id', $booking->user_id)
->delete();
}
 
// Since we are clearing the scheduled notifications, we need to create them again for the new date
 
// Schedule 2H reminder
$twoHoursTime = fromUserDateTime($startTime->subHours(2), $booking->user);
if (now('UTC')->lessThan($twoHoursTime)) {
$booking->user->scheduledNotifications()->create([
'notification_class' => BookingReminder2H::class,
'notifiable_id' => $booking->id,
'notifiable_type' => Booking::class,
'sent' => false,
'processing' => false,
'scheduled_at' => $twoHoursTime,
'sent_at' => null,
'tries' => 0,
]);
}
// Schedule 1H reminder
$oneHourTime = fromUserDateTime($startTime->subHour(), $booking->user);
if (now('UTC')->lessThan($oneHourTime)) {
$booking->user->scheduledNotifications()->create([
'notification_class' => BookingReminder1H::class,
'notifiable_id' => $booking->id,
'notifiable_type' => Booking::class,
'sent' => false,
'processing' => false,
'scheduled_at' => $oneHourTime,
'sent_at' => null,
'tries' => 0,
]);
}
// Schedule 5 min reminder
$fiveMinutesTime = fromUserDateTime($startTime->subMinutes(5), $booking->user);
if (now('UTC')->lessThan($fiveMinutesTime)) {
$booking->user->scheduledNotifications()->create([
'notification_class' => BookingReminder5MIN::class,
'notifiable_id' => $booking->id,
'notifiable_type' => Booking::class,
'sent' => false,
'processing' => false,
'scheduled_at' => $fiveMinutesTime,
'sent_at' => null,
'tries' => 0,
]);
}
// Schedule started reminder
$startingTime = fromUserDateTime($startTime, $booking->user);
if (now('UTC')->lessThan($startingTime)) {
$booking->user->scheduledNotifications()->create([
'notification_class' => BookingStartedNotification::class,
'notifiable_id' => $booking->id,
'notifiable_type' => Booking::class,
'sent' => false,
'processing' => false,
'scheduled_at' => $startingTime,
'sent_at' => null,
'tries' => 0,
]);
}
 
event(new BookingUpdatedEvent($booking));
 
return redirect()->route('booking.index');
}
 
public function destroy(Request $request, Booking $booking): RedirectResponse
{
abort_unless($booking->user_id === $request->user()->id, 404);
 
$booking->delete();
 
$booking->scheduledNotifications()
->where('user_id', $booking->user_id)
->delete();
 
event(new BookingDeletedEvent($booking));
 
return redirect()->route('booking.index');
}
}

That's it! Our Controller looks much cleaner now and we gained the ability to trigger Booking Events where we need them.