Eager load ONE from Many to Many relationship - php

I have a many to many relationship for users and roles. A user can have multiple roles, but I only want with to grab the FIRST role.
Consider the following code:
User::with('roles')->get()
Works great for all roles, but I only want the first role.
I've set this up in my model but doesn't work:
public function role()
{
return $this->roles()->first();
}
How do I load with for only the first result?

You should be able to call first directly on the eager loaded relationship like this:
User::with(['roles' => function ($query) {
$query->first();
})->get();

first() actually executes the query and returns the results as a collection. Relationships must return a query builder, which can then be chained or executed, so using first() in a relationship won't work.
UPDATE
I realised you want to use role in with, so you need to create a relationship to do that. Create a new relationship on your User model (you can use any limit described in the docs, not just oldest()):
public function role()
{
return $this->hasOne('App\Role')->oldest();
}
And then you can use it in with:
$users = User::with('role')->get();

Related

Laravel with() works only for first record (only for record with id 1)

Imagin situation where in laravel User Eloquent model I have this kind of relationship functions:
public function serviceProvider(){
return $this->belongsTo('App\Http\Models\Users\ServiceProvider');
}
public function company(){
return $this->serviceProvider->company();
}
I have this kind of relationship in serviceProvider model:
public function company()
{
return $this->morphOne('App\Http\Models\Companies\Company', 'companyable');
}
Now in controller I write something like that:
User::where('status', '=', 1)->with('company')->get();
I know it sounds crazy but it only works for record where user's id = 1, for the rest of the record it just returns company : null
I have no idea what is going on here, Maybe I am doing something wrong? what is the problem?
If I write something like that: $user->company, it works, It works for each user, I mean for
$user = User::where('id', '=', "any id")->first;
so problem is with with()
I'm a little surprised this is working at all to be fair.
Since your User a relationship for serviceProvider set up and that has a relationship for company you don't need to have a company relationship in User.
If you want to eager/lazy load nested relationships you can just use dot notation e.g.
User::with('serviceProvider.company')->where('status', 1)->get();
Lazy load docs (scroll down to Nested Eager Loading)

Calling Laravel relationship with aggregate

If I have a Laravel 5.5 model called User that hasMany Posts and each Post hasMany Hits, is there an aggregate function I can call to get the total number of Hits for a User across all Posts, where the Hit was created in the last week?
It seems like there may be a clever way to do it besides doing something like
$hits = $user->posts()->hits()
and then looping over those hits to check created date.
In this case it seems like raw sql would be better, but I figured there may be an Eloquent way to handle a situation like this.
I think the right solution is just to use a HasManyThrough relationship to grab all the Hit rows, joined through the posts table.
So it'd look like this on the User model (roughly):
return $this->hasManyThrough(
Hit::class,
Post::class
// if you have non-standard key names you can specify them here-- see docs
);
Then when you have your User model you can just call $user->hits to get a collection of all the associated hits through all the user's Posts
You can add the code below to your Post model.
protected static function boot()
{
parent::boot();
static::addGlobalScope('hitCount', function ($builder) {
$builder->withCount('hits');
});
}
It automatically provides a field hits_count whenever you fetch a post.
$post = Post::first();
$hits = $post->hits_count; //Count hits that belongs to this post
You can read the documentation here to customize it to your need.
Set HasManyThrough relation in the User model:
public function hits()
{
return $this->hasManyThrough('App\Models\Hits','App\Models\Posts','user_id','post_id','id');
}
then you can do this:
$reults = $user->hits()->where('hits_table_name.created_at', '>=', Carbon::today()->subWeek())->count();
HasManyThrough Link
Use DB::enableQueryLog(); and DB::getQueryLog(); to see if executed SQL Query is correct;

How to query a relationship through a relationship collection

I have a user and a department, a user can belong to many departments. I'm trying to define a scope on the user model to only show users of departments that the current user is a part of:
public function scopeVisible($query) {
$user = Auth::user();
return $user->departments()->users();
}
I've looked at the documentation and can't seem to find a way to do this.
I tried mapping but got strange errors where a method call would be on a query builder instead of a collection even though the object is a collection.
As Laravel docs says you can use whereHas to add customized constraints for more specific queries:
If you need even more power, you may use the whereHas and orWhereHas
methods to put "where" conditions on your has queries. These methods
allow you to add customized constraints to a relationship constraint.
Then this should work:
public function scopeVisible($query) {
$user_id = Auth::user()->id;
return $query->whereHas('departaments', function($query) use ($user_id) {
$query->where('user_id', '=', $user_id);
});
}
The error you are getting about "the method call must be on a query builder instead of a collection" it's because on your code you are returning the result of $user->departments()->users(), which is a collection of Departments, you should return the query builder instead.

Laravel Eloquent query unexpected result

I've found some query result really unexpected.
It's Laravel 5.2
We have following entity:
User with method:
public function roles() : BelongsToMany
{
return $this->belongsToMany(Role::class)->withPivot('timestamp');
}
Each User can have many roles, so we have also Role entity (but it doesn't matter much in my question) and pivot table user_role with timestamp field (and ids of course), because we hold information about time, when User achieved specific role.
I want to get all Users with theirs last assigned Role
When I create query (in User context in some repository):
$this->with(['roles' => function($query) {
$query->orderBy('timestamp', 'desc');
}])->all();
the result will contain Users with Roles entities inside itself ordered by timestamp - it's ok. But I want to retrieve only one last role inside each User entity not all ordered.
So...
$this->with(['roles' => function($query) {
$query->orderBy('timestamp', 'desc')->limit(1);
}])->all();
And then I retrieve Users but only User which achieved some Role for the very last time contains it! All the other Users have their roles field containing empty array.
Why ordering was performed on each Users relation separately, but when I added limit it behaved like a global limit for all.
It drives me crazy...
Thanks for advices.
EDIT
I've created lastRoles() method to get all Roles ordered desc. But all, retrieving one is impossible.
public function lastRoles() : BelongsToMany
{
return $this->BelongsToMany(Roles::class)->withPivot('timestamp')->latest('timestamp');
}
And for testing:
$users = (new User())->with('lastRoles')->get();
But now I must iterate over Users and invoke lastRoles() on each one:
foreach ($users as $user) {
var_dump($user->lastRoles()->get()->first()->name);
}
Then I retrieve names of latest Roles assigned to each User.
So... There is no way to do it in one query? This is the only way?
For this to work, you would need a helper function:
public function latestRole()
{
return $this->hasOne(Role::class)->withPivot('timestamp')->orderBy('timestamp', 'DESC');
}
And then:
$this->with('latestRole')->get();
Credits to this awesome article.
When you eager load a relationship with query constraint(s), the query will be run once to load all relationships, not each one individually. This is the expected behavior. Think about it, eager loading exists to turn many queries into one query in order to optimize performance. There is only one query executed, so your limit constraint will limit the entire result set, rather than on a per model basis.
To circumvent this, you could try creating another belongsToMany method that adds the desired limit constraint. The following code is untested:
public function lastRole() : BelongstoMany
{
return $this->belongsToMany(Role::class)
->withPivot('timestamp')
->orderBy('timestamp', 'desc')
->limit(1);
}
Assuming this works, you can then simply change the relationship method from roles to lastRole and remove your query constraint:
$this->with('lastRole')->all();

Using withTrashed with relationships in Eloquent

Is there a way to use withTrashed with relationships in Eloquent.
What I need is this. I have table and model Mark and another table User. User has many Mark and Mark belongs to User. So I defined this in Eloquent models.
Now I need to get an instance of Mark that is soft deleted. This is not a problem if User isn't soft deleted, but if both Mark and User are soft deleted, I get an error Trying to get property of non-object, because
$mark->user
won't return actual user, cause it is soft deleted.
Is there a way that I can do something like
$mark->withTrashed()->user
to get this related user even if it is deleted?
Depending on your needs, you can define the relationship:
public function marks()
{
return $this->hasMany('Mark')->withTrashed();
}
// then just
$user->marks;
or use it on the fly:
$user->marks()->withTrashed()->get();
// or when lazy/eager loading
$user = User::with(['marks' => function ($q) {
$q->withTrashed();
}])->find($userId);
then your case would turn into:
$mark->user() // get relation object first
->withTrashed() // apply withTrashed on the relation query
->first(); // fetch the user
// alternatively you use getResults relation method
$mark->user()
->withTrashed()
->getResults(); // returns single model for belongsTo
$user->marks()->withTrashed()
->getResults(); // returns collection for hasMany
You can do that like this:
$mark->withTrashed()->first()->user->withTrashed()->first()

Categories