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.