Model updates not shown in related model's collection of the updated model relation in afterSave or afterUpdate methods

First of all, apologies for the title of this one - I spent more time trying to figure out what to put there, than I did writing all this out :sweat_smile:

I have 2 models. Course and Booking. A Course hasMany Bookings, and a Booking belongsTo a Course.

class Course extends Model
{
    public $hasMany = ['bookings', Booking::class];
}

class Booking extends Model
{
    public $belongsTo= ['course', Course::class];
}

A booking has a qty, and a status attribute that can be “Awaiting, Confirmed or Cancelled”

The course model has an attributes available_spaces and open_spaces. It has a method called getRemainingSpaces that is used to set the open spaces value. This method calls another method, getBookingsQty. This method loops through the related Bookings using the reduce collection method to calculate the total qty for bookings that don’t have a cancelled status.

// Course.php

public function getRemainingSpaces()
{
    return $this->available_spaces - $this->getBookingsQty();
}

public function getBookingsQty()
{
    $qty = $this->bookings->reduce(function ($total, $booking) {
        if ($booking->status == 'cancelled') {
            return $total;
        }

        return $total + $booking->qty;
    });

    return $qty ?? 0;
}

When a booking’s status is changed to cancelled, I want to update the open_spaces value on the Course model. So I’ve put some logic inside the Booking’s afterSave method to do this.

// Booking.php

public function afterSave()
{
    $course = $this->course;
    
    /*
     * This is still including the recently updated booking's qty even
     * though it's status has been changed to cancelled
     */
    $course->open_spaces = $course->getRemainingSpaces(); 
    $course->save();
}

The problem I’m having is that the Booking that I’ve just saved isn’t being reflected in it’s related Course’s related bookings in the afterSave() method.

Is this a bug, or is there a way around it?

You probably have eager loaded the bookings relation while loading the Course model. You’ll need to refresh the relation in getBookingsQty() to make sure you get the latest state from the DB.

public function getBookingsQty()
{
    $this->load(['bookings']);

    $qty = $this->bookings->reduce(function ($total, $booking) {
        if ($booking->status == 'cancelled') {
            return $total;
        }

        return $total + $booking->qty;
    });

    return $qty ?? 0;
}

I’m not a 100% sure, but I think you could sum the qty of ‘not-cancelled’ bookings in SQL. That would reduce your function to a single line:

public function getBookingsQty()
{
    $this->bookings()->where('status', '!=', 'cancelled')->sum('qty');
}

Other options:

  • create a ‘cancelledBookings’ relationship with a fixed scope and use the relation count
  • Move that function out into a “CourseManager” service class, so you cant count the bookings of a record with SQL, without loading and hydrating the model first.
1 Like