Search code examples
phplaraveleloquentlaravel-5.3

Why HasOneOrMany doesn't update relations on parent?


We are developing an e-commerce cart using Laravel 5.3, which has items. So our cart has a 1:n relationship to items.

When we add some product to cart using Illuminate\Database\Eloquent\Relations\HasOneOrMany::save() it doesn't push the new item to our $cart->items collection, it has to be done manually using Collection::push() method.

We are asking this because the Illuminate\Database\Eloquent\Relations\BelongsTo::associate() method does that, so we don't know if this is a bug.

Just for better understanding:

Now we need to do this:

$cart = Cart::first();

$cartItem = new CartItem();
$cartItem->quantity = 1;
$cartItem->base_price = 1;
$cartItem->paid_price = 1;

$cart->items()->save($cartItem);
print($cart->items->count()); # returns 0
$cart->items->push($cartItem);
print($cart->items->count()); # returns 1

What we want to do is this code above, and then be able to interate with our list (for example update order values), without calling other methods.

$cart = Cart::first();

$cartItem = new CartItem();
$cartItem->quantity = 1;
$cartItem->base_price = 1;
$cartItem->paid_price = 1;

$cart->items()->save($cartItem);
print($cart->items->count()); # returns 1

We are thinking on make a pull request because we saw the code and it can be done. But is it correct? Can we do that?

Duplicated from here: https://github.com/laravel/framework/issues/14719

Thanks


Solution

  • You may be interested in the discussion on this issue. I have copied the comments I made on that issue here, in case that link goes away at some point.

    Information on how relationship attributes are loaded:

    This is expected behavior. Once the relationship attribute for a model instance is loaded, it will not be reloaded unless explicitly reloaded.

    // relationship attribute lazy loaded here
    $blog->posts->count()
    
    $post = new Post(['title' => 'post title');
    
    // typo in OP; save() must be called on relationship method, not relationship attribute
    $blog->posts()->save($post);
    
    // relationship already loaded. Collection has not changed.
    $blog->posts->count(); // 0
    
    // however, call to database will reflect current count
    $blog->posts()->count(); // 1
    
    // reload the relationship attribute
    $blog->load('posts');
    
    // relationship collection refreshed; count of relationship attribute will reflect this
    $blog->posts->count(); // 1
    

    Why the relationship attribute Collection is not updated when an item is added to the relationship:

    I think there's too much uncertainty to attempt modifying the Collection. Even though you're relating the post to the blog through the posts() relationship, there is no guarantee that the new post should be loaded into the relationship attribute Collection in the first place.

    Imagine, for example, if your relationship were defined like this (silly example, but bear with me):

    // only get posts created before today
    public function posts() {
        return $this->hasMany(Post::class)->where('created_at', '<', date('Y-m-d'));
    }
    

    Given this relationship, since the new post is created today, it would be incorrect if it were simply injected into the existing lazy loaded $blog->posts collection, because it does not meet the added where condition. If you reload this relationship, the new post will still not be in it.

    Another example would be this:

    // get the posts, newest first
    public function posts() {
        return $this->hasMany(Post::class)->orderBy('created_at', 'DESC');
    }
    

    Given this relationship, if the new post is added to the end of the existing loaded $blog->posts collection, it will be in the incorrect order. You could argue that it could be added to the beginning, but then it would be in the incorrect order if the relationship were ordered ASC. You would have to parse the relationship query to try to figure out where to add the item.

    Now, those are just simple examples. I'm sure there are plenty of very complicated relationships out there where it would be impossible to parse all the constraints and conditions in order to properly insert the new item into the existing collection.

    The only real alternative to the existing functionality would be to always force a reload of an existing loaded relationship whenever the relationship is modified, however that could lead to some serious performance issues. It seems best to make sure the developer knows they need to explicitly reload the relationship if needed, rather than to implicitly reload it, whether it is needed or not.