9

I'm building a simple timetracking App using Laravel 5 where I have two Laravel models, one called "User" and one called "Week"

The relationships are pretty simple:
Week.php:

public function user()
{
    return $this->belongsTo('App\User');
}

User.php:

function weeks()
{
    return $this->hasMany('App\Week');
}

Now, the User.php file also has a simple helper / novelty function called "getCurrentWeek()" that, as the name suggests, returns the current week

function getCurrentWeek()
{
    $possibleWeek = $this->weeks->sortByDesc('starts')->all();
    if(count($possibleWeek) != 0)
    {
        return $possibleWeek[0];
    }
    else
    {
        return null;
    }
}

My problem is: If I create the very first week for a user and attach / relate it to the user like so:

$week = new Week;

//Omitted: Setting so attributes here ...

$week->user_id = $user->id;
$weeks->save()
$user->weeks()->save($week);

And then call the $user->getCurrentWeek(); method, that method returns null, although a new week has been created, is related to the user and has been saved to the database. In my mind, the expected behaviour would be for getCurrentWeek() to return the newly created week.
What am I misunderstanding about Eloquent here / doing just plain wrong?

Skye Ewers
  • 358
  • 1
  • 4
  • 16

3 Answers3

25

Relationship attributes are lazy loaded the first time they are accessed. Once loaded, they are not automatically refreshed with records that are added or removed from the relationship.

Since your getCurrentWeek() function uses the relationship attribute, this code will work:

$week = new Week;
// setup week ...
$user->weeks()->save($week);

// $user->weeks attribute has not been accessed yet, so it will be loaded
// by the first access inside the getCurrentWeek method
dd($user->getCurrentWeek());

But, this code will not work:

// accessing $user->weeks lazy loads the relationship
echo count($user->weeks);

// relate a new record
$week = new Week;
// setup week ...
$user->weeks()->save($week);

// $user->weeks inside the method will not contain the newly related record,
// as it has already been lazy loaded from the access above.
dd($user->getCurrentWeek());

You can either modify your getCurrentWeek() method to use the relationship method ($this->weeks()) instead of the attribute ($this->weeks), which will always hit the database, or you can reload the relationship (using the load() method) after adding or removing records.

Change the getCurrentWeek() method to use the relationship method weeks() (updated method provided by @Bjorn)

function getCurrentWeek()
{
    return $this->weeks()->orderBy('starts', 'desc')->first();
}

Or, refresh the relationship using the load() method:

// accessing $user->weeks lazy loads the relationship
echo count($user->weeks);

// relate a new record
$week = new Week;
// setup week ...
$user->weeks()->save($week);

// reload the weeks relationship attribute
$user->load('weeks');

// this will now work since $user->weeks was reloaded by the load() method
dd($user->getCurrentWeek());
patricus
  • 59,488
  • 15
  • 143
  • 145
  • `$thing->load('relationshipName')` was exactly what I needed. I was using lazy loaded relationships in a PDF that is generated directly after `save()` is called. Thanks! – hutch Jan 26 '22 at 04:30
4

Just to add to @patricus answer...

After save/create/update, you can also empty all the cached relation data like so: $user->setRelations([]);

Or selectively: $user->setRelation('weeks',[]);

After that, data will be lazy loaded again only when needed: $user->weeks

That way you can continue using lazy loading.

Isometriq
  • 371
  • 3
  • 8
0

$user->weeks()->save($week); is not needed because you manually attached the week to the user by using $week->user_id = $user->id; and saving it.

You could actually rewrite the whole function to:

function getCurrentWeek()
{
    return $this->weeks->sortByDesc('starts')->first();
}

Or

function getCurrentWeek()
{
    return $this->weeks()->orderBy('starts', 'desc')->first();
}

Edit:

I made a little proof of concept and it works fine like this:

App\User.php

function weeks()
{
    return $this->hasMany('App\Week');
}

function getCurrentWeek()
{
    return $this->weeks->sortByDesc('starts')->first();
}

App\Week.php

public function user()
{
    return $this->belongsTo('App\User');
}

routes/web.php or App/Http/routes.php

Route::get('poc', function () {
    $user = App\User::find(1);
    $week = new App\Week;
    $week->user_id = $user->id;
    $week->starts = Carbon\Carbon::now();
    $week->save();
    return $user->getCurrentWeek();
});

Result:

{
    id: 1,
    user_id: "1",
    starts: "2016-09-15 21:42:19",
    created_at: "2016-09-15 21:42:19",
    updated_at: "2016-09-15 21:42:19"
}
Björn
  • 5,696
  • 1
  • 24
  • 34
  • `$user->getCurrentWeek()` gets called directly after `$user->weeks()->save($week);` from my code above. I tried your method but now I'm getting an error. `BadMethodCallException in Builder.php line 2405: Call to undefined method Illuminate\Database\Query\Builder::all()`. The error occurs at exactly that changed line. What exactly am I doing wrong here? I copy and pasted your exact code. – Skye Ewers Sep 15 '16 at 21:21
  • Hi @SvenE. I made a little proof of concept and both your way and my way worked fine. I attached all the code so you can compare it to yours. See my edit. – Björn Sep 15 '16 at 21:49
  • Try using get() instead of all() – pseudoanime Sep 16 '16 at 00:52
  • Thanks so much @Björn, you poc-code helped me debug the problem :) – Skye Ewers Sep 16 '16 at 12:04