As you may have noticed, our Controller looks big and has a lot of repetitive code. Let's clean it up a bit.
Our plan for the cleanup will be moving things around and extracting some code into separate methods. This includes:
Booking Model (and creating a method for it)To create the events, we can run the following commands:
php artisan make:event BookingCreatedEventphp artisan make:event BookingUpdatedEventphp 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 is similar to creating events:
php artisan make:listener BookingCreatedListener --event=BookingCreatedEventphp artisan make:listener BookingUpdatedListener --event=BookingUpdatedEventphp 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.
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.
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,        ],    ];// ...}
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.