When determining wheter a relation is loaded on model, we call relationLoaded method, introduced in v5.1, which does not verify nested relations. Taking a look to the actual code executed, it just checks if the key provided is in relations array.
Suppose we have a Bar model that belongs to Foo model, and this one has many Baz models, just a hypothetical situation. We now need to return a list of Bar models, that can load the Baz models from Foo relation, if we would like to know wheter the Baz models are loaded in Bar model from Foo relation, this not working:
$bar = Bar::with(['foo.bazs']);
$bar->relationLoaded('foo.bazs'); // false
Even with the relation loaded, it can not figure out nested relations. As we can see, this is a simple stuff, I thing. As a suggestion, the relationLoaded method could be changed to checks nested keys using own helpers.
namespace Illuminate\Database\Eloquent\Concerns;
...
trait HasRelationships
{
...
/**
* Determine if the given relation is loaded.
*
* @param string $key
* @return bool
*/
public function relationLoaded($key)
{
return array_has($this->relations, $key);
}
}
That's not a trivial problem, especially with *Many relationships:
$user->relationLoaded('posts.category');
In this case, you would have to look at every single post and check whether the category relationship is loaded.
Can you provide some context on why you need this functionality?
Taking a better look at the problem now, I figured out that I could take another approach to achieve what I needed. And as @staudenmeir mentioned before, that's not a trivial problem; anyway this may be something to think about.
I need this functionality to determine if nested relation had been loaded, if this was loaded, we can return on response. I was using fractal transformers to build the json response, for a REST api.
When you want for example, add a nested relation on the data structure level.
public function includeComments(User $user)
{
if ($user->relationLoaded('posts.comments')) {
return $user->posts->comments;
}
}
In this case, the comments will only be added on response when the relation had been eager loaded. Also allow us to put nested relations at a higher structure level on response without using lazy loading.
In my initial post where a suggest a code to fix that, it is really wrong, the code fix anything lol. We would need to take @staudenmeir suggestion.
You should probably override relationLoaded() in your application and then call it recursively for each level.
What types of relationships are in your nested path? The implementation is a lot easier for BelongsTo/MorphTo. You could also make it simpler by only checking the first result of a *Many relationship (instead of all).
Yes I will try it and post here.
/**
* Determine if the given relation is loaded.
*
* @param string $key
* @return bool
*/
public function relationLoaded($key)
{
if (str_contains($key, '.')) {
$relationNames = explode('.', $key);
// Find the relation we are dealing with.
$upperRelation = array_shift($relationNames);
if (parent::relationLoaded($upperRelation)) {
// Get relation object from model.
$relation = $this->relations[$upperRelation];
// Build the new key to be verified on relation model instance.
$key = implode('.', $relationNames);
return $this->firstOrItem($relation)->relationLoaded($key);
}
return false;
}
return parent::relationLoaded($key);
}
/**
* Determine if the item is a collection and return the first item on it,
* otherwise just return the item.
*
* @param \Illuminate\Support\Collection|mixed $item
* @return mixed
*/
protected function firstOrItem($item)
{
if ($item instanceof Collection) {
return $item->first();
}
return $item;
}
I took the easy way, recursively iterating into relations and finding out whether the relation keys are present on relation model. Suggestions are needed.
Thanks for helping.
What do you mean by "Suggestions are needed."? Is it not working?
No, I meant suggestions are welcome. Yeah it's working, thanks!
Looks good! I see only two cases that cause problems: When a relationship value is null or an empty collection.
Yes, you are right. When dealing with an empty collection, we can assume the relation was loaded, but no records was found. Checking the first item for nested Many relations may fail when the relation is empty, because when we have to check nested relations depending on this empty one, we simply can not procede.
I wrote a similar helper method for myself and then came here to check if anyone else had done something similar and looks like @yanmarques and my solutions are quite close.
if (!function_exists('modelHasRelations')) {
/**
* @param \Illuminate\Database\Eloquent\Model $model
*
* @return bool
*/
function modelHasRelations(\Illuminate\Database\Eloquent\Model $model, $relations): bool {
if (is_string($relations)) {
$relations = explode('.', $relations);
}
$queue = array_reverse($relations);
while (count($queue)) {
$relation = $queue[count($queue) - 1];
if ($model instanceof \Illuminate\Support\Collection) {
if (
$model->contains(function ($item) use ($relation) {
return !$item->relationLoaded($relation);
})
) {
return false;
}
$model = $model->first();
} else
if (!$model->relationLoaded($relation)) {
return false;
}
$model = $model[$relation];
array_pop($queue);
}
return true;
}
}
The only real difference is that my function loops through every model in a to-many relation and checks if all the models have the relation loaded.
Since this is more of a feature request than an actual problem I suggest to open up an issue in https://github.com/laravel/ideas to gather support and then perhaps send in a PR.
Most helpful comment
I took the easy way, recursively iterating into relations and finding out whether the relation keys are present on relation model. Suggestions are needed.
Thanks for helping.