laravel eloquent 'has' method behaves in an unexpected way - laravel

I want to get the collection of a Section model if it has at least one User. from the docs the has() method does this, great. The collection retrieved does not have the the users relationship in it. Yet when I loop through the collection , I can get the properties of the users. Why?
class Section extends Model
{
protected $guarded = [];
public function users()
{
return $this->hasMany('App\User');
}
}
class User extends Authenticatable
{
protected $guarded = [];
public function section()
{
return $this->belongsTo('App\Section');
}
}
what I did is this:
$section = Section::where('id' , 1)->has('users')->get();
the collection is this:
Illuminate\Database\Eloquent\Collection {#3025
all: [
App\Section {#3015
id: 1,
class_id: 1,
section_name: "A",
created_at: "2019-12-14 18:26:01",
updated_at: "2019-12-14 18:26:01",
},
],
}
Now the weird part is that when I do the following, it gives the properties of users even though in the collection the users relationship is not present.
Why?
#foreach ($section as $section)
#foreach ($section->users as $student)
<p>{{$student->name}}</p>
#endforeach
#endforeach
solomon
uche
kene

This is just how Laravel works.
Accessing $model->{relationship}, in your case $section->users, is a magic function that checks if you have explicitly loaded the relationship via something like Section::with('users'), and if you haven't, loads it then.
The reason you're not seeing users when you run dd($section) is that you didn't explicitly load the relationship, but this does not mean that it is unavailable. If you included with('users') in your initial query, you'd see the following:
$section = Section::where('id' , 1)->has('users')->with('users')->get();
App\Section {#3015
id: 1,
class_id: 1,
section_name: "A",
created_at: "2019-12-14 18:26:01",
updated_at: "2019-12-14 18:26:01",
users: [
0 => App\User {#3016}
id: ...
name: ...
]
},
// Or similar
Basically, you weren't loading the relationship, so you aren't able to see it when using dd($section), but it is still available in PHP, due to Laravel's magic methods.
I should note as well, use the correct variable naming and closure (->get(), ->first(), etc) for your query.
$section is a poor name when using ->get(), as you're getting multiple records back from the database. Either use $sections, or change the closure to ->first(), and don't use a foreach() if you use ->first().

Looks like your first echo:
$section = Section::where('id' , 1)->has('users')->get();
Just prints the section where it has a user, but you're not specifically saying give me the users as well.
In the next loop you are looping over each section in the view, and specifically saying loop the relationship. this is visible by this line: #foreach ($section->users as $student)
Im reading the docs here: https://laravel.com/docs/6.x/eloquent-relationships#querying-relations
In your first echo when printing the section you can get the users like so: echo $section-> users()

Ok, now I understand your question.
First
The has method does not mean that it's going to include User. It means that will return all the sections that have at least one user. I think there is/are users for Section id ===1. So, in your code with has or without it, it does not make any difference.
if you want to load a relationship explicit you should use with
Section::where('id' , 1)->with('users')->get();. Then you should have users collection under per section.
Second
The reason why you can still access the user properties in your blade file, is because of lazy loading. even it's not included in the original DB query and result but when you are trying to access it laravel still tries to fetch those for you. And this may cause N+1 problem.

Related

Laravel : insert into pivot table

I'm trying to connect profiles with groups using a pivot table. What I want is to use tinker to add students profiles in study groups.
I don't know what I'm doing wrong but I can't make tinker write to the DB (I know how to do it with OneToOne and oneToMany relationships).
My Profile model has the following function :
public function isStudentInGroup()
{
return $this->belongsToMany(Group::class);
}
My Group model has the following function :
public function hasStudents()
{
return $this->belongsToMany(Profile::class);
}
In tinker, I start by grabbing a Profile and a group :
$profile = Profile::find(1);
$group = Group::find(1);
Then I want to add the group to the isStudentInGroup collection. How do I do it? Here's what I've tried until now. No matter what I do, everytime I use fresh() I see an empty collection.
$profile->isStudentInGroup->save($group);
$profile->isStudentInGroup->save($group->id);
$profile->isStudentInGroup->attach($group->id);
Profile::find(1)->isStudentInGroup->push(1)->update();
Profile::find(1)->isStudentInGroup->push(1)->save();
These all give BadMethodCallException errors.
A promising syntax was this one :
$profile->isStudentInGroup->push($group)
=> Illuminate\Database\Eloquent\Collection {#4646
all: [
App\Models\Group {#5012
id: "1",
start_time: "2022-09-05 18:10:00",
end_time: "2022-09-05 19:00:00",
created_at: null,
updated_at: null,
pivot: Illuminate\Database\Eloquent\Relations\Pivot {#5020
profile_id: "1",
group_id: "1",
},
},
],
}
But then, if I tried to $profile->save(); or $profile->update(); I would get the following
BadMethodCallException with message 'Method Illuminate\Database\Eloquent\Collection::update does not exist.'
What am I missing?
Thanks in advance.
EDIT :
Thanks to matiaslauriti I got the syntax right:
$profile->isStudentInGroup()->save($group);
I also went fishing for knowledge and found this related question with a helpful answer from McHobbes that helped me understand the logic behind the parentheses.
Why eloquent model relationships do not use parenthesis and how do they work?
Pardon me if I did not understand, but you want to make a relation between those relationships, so you have to run:
$profile->isStudentInGroup()->save($group->id);
That should trigger storing that group inside that relationship. If it does not work, I think this will should (or may also work):
$profile->isStudentInGroup()->attach($group->id);
But you must use the relationship, not the Collection. You are mixing relationships objects with Collections, that is why the error says:
Method Illuminate\Database\Eloquent\Collection::update does not exist.
A Collection does not have update or save as it is a collection, it can't be saved anywhere, but a relationship can.
Try this
App\Models\Profile::find(1)->isStudentInGroup;
i hope it was useful .

How to get the last record using Eloquent scope?

I have a Posts table structured like this:
Now I'm trying to get the latest post which is obviously the record with id = 2.
I have this in my theme page posts.htm:
[builderList postLatest]
modelClass = "Me\Articles\Models\Posts"
scope = "scopeLatest"
scopeValue = "{{ :scope }}"
displayColumn = "title"
noRecordsMessage = "No records found"
detailsPage = "-"
detailsUrlParameter = "slug"
pageNumber = "{{ :page }}"
in my model Posts I have:
public function scopeLatest($query)
{
return $query->orderBy('created_at','desc')->first();
}
But this one is returning both records. I also tried using latest()
return $query->latest();
And this one gives me the error:
Maximum function nesting level of '1000' reached, aborting!
Even tried passing parameter like latest('created_at') but got the same error.
Tried dumping dd($query->orderBy('created_at','desc')->first()->toSql()) and got select * from posts where posts.deleted_at is null
Why first() is not limiting to 1 record?
Why latest() is throwing me Maximum error? Is this version specific bug? It seems I cant find documentation related to it. I have Laravel 5.5.48.
I'm not sure now what can I use. Maybe I'm doing something wrong here. I just need to get the latest post.
The solution to your problem is using the ->take(1) method. There are more methods applicable to queries but here is the list of available methods you can run on a collection instance. I believe Builder expects scopes to return a query {[a collection of models]}. I took some time to do testing and in my opinion this might just be a flaw in the Builder Plugin construction.
Now before you look at my research below. I would say you should learn to build your own components like Hardik suggests in a comment. Hope this all helps.
So here is a scope I have for a plugin:
public function scopeFilterTypes($query) {
return $query->whereNotIn('slug', [
'range',
'close',
'all',
'single',
'three',
'five',
'self'
])->first();
}
Here is a picture of my backend form which correctly only sees one item in the relationship (this is why I am using a scope):
Now look at this image of my builder component You can see that it even ignores my ->whereNotIn([]) method on the query:
To prove my finding on this flaw in the builder component I do a find($id) method which would return a model instance not a collection of models. Using the find($id) method also gives me the correct result in the backend form but in the Builder list shows me all the records which is incorrect.
However when I change the scope to ->take(1):
public function scopeFilterTypes($query) {
return $query->whereNotIn('slug', [
'range',
'close',
'all',
'single',
'three',
'five',
'self'
])->orderBy('created_at','desc')->take(1);
}
I get this in the backend form; working correctly:
And I get this from the builder list:

Laravel eloquent query with sum of related table

I have a table users and posts with columns user_id and post_views.
In post_views I keep information how many times post was display.
And now, in query I would like to get user with sum of post_views all his posts.
I tried do something like this:
User::where(['id'=>$id])->with('posts')->get();
And in model I defined:
public function posts()
{
return $this->hasMany('App\Models\Post')->sum('post_views','AS','totalViews');
}
But without success.
How to do it?
Thank you
You can use a modified withCount():
public function posts()
{
return $this->hasMany('App\Models\Post');
}
$user = User::withCount(['posts as post_views' => function($query) {
$query->select(DB::raw('sum(post_views)'));
}])->find($id);
// $user->post_views
You can use
User::withCount('posts')->find($id)
to get the user with the id $id and a posts_count attribute in the response
I'm not fully sure what the intention of ->sum('game_plays','AS','totalVies'); is - you would need to add more context if you want this
Just something to add with regards to your shown code: No need to query by id using where + the get() at the end will make you query for a collection. If you want to get a single result use find when searching by id
As always laravel has a method for that : withSum (Since Laravel v8)
Note : I know that at the time of the message was posted, the method did not exist, but since I came across this page when I was looking for the same result, I though it might be interesting to share.
https://laravel.com/docs/9.x/eloquent-relationships#other-aggregate-functions
In your case it should be :
$user = User::withSum('posts as total_views', 'post_views')->find($id);
Then you can access to the result :
$user->total_views

Why does Eloquent change relationship names on toArray call?

I'm encountering an annoying problem with Laravel and I'm hoping someone knows a way to override it...
This is for a system that allows sales reps to see inventory in their territories. I'm building an editor to allow our sales manager to go in and update the store ACL so he can manage his reps.
I have two related models:
class Store extends Eloquent {
public function StoreACLEntries()
{
return $this->hasMany("StoreACLEntry", "store_id");
}
}
class StoreACLEntry extends Eloquent {
public function Store()
{
return $this->belongsTo("Store");
}
}
The idea here is that a Store can have many entries in the ACL table.
The problem is this: I built a page which interacts with the server via AJAX. The manager can search in a variety of different ways and see the stores and the current restrictions for each from the ACL. My controller performs the search and returns the data (via AJAX) like this:
$stores = Store::where("searchCondition", "=", "whatever")
->with("StoreACLEntries")
->get();
return Response::json(array('stores' => $stores->toArray()));
The response that the client receives looks like this:
{
id: "some ID value",
store_ac_lentries: [
created_at: "2014-10-14 08:13:20"
field: "salesrep"
id: "1"
store_id: "5152-USA"
updated_at: "2014-10-14 08:13:20"
value: "salesrep ID value"
]
}
The problem is with the way the StoreACLEntries name is mutilated: it becomes store_ac_lentries. I've done a little digging and discovered it's the toArray method that's inserting those funky underscores.
So I have two questions: "why?" and "how do I stop it from doing that?"
It has something in common with automatic changing camelCase into snake_case. You should try to change your function name from StoreACLEntries to storeaclentries (lowercase) to remove this effect.

Laravel collection eager loading based on nested relation field value

I'm trying to return a Laravel collection object with relations where the resulting collection is based on a nested criteria (ie. a nested model's field value). So it would look something like this:
User -> Category -> Post -> Comments where comment.active == 1
In this case, I want the result to include all of a specific user's categories => posts => comments, where the comment is active. If it is active, it would be nested in the proper hierarchy (Category->Post->Comment). If the comment is not active, any related post and potentially category (if there are no other posts with active comments) should not show up in the collection at all.
I've tried eager loading through with(), load() and filter() with no luck. They will continue to load the relations with empty comment relations. Looking for guidance as to where to research: joins? filters? advanced wheres with nesting?
One attempt:
$user->categories->filter(function($category) {
return $category->isActive();
});
In my model I have all the relationships setup appropriately, and in addition to that I have setup isActive() as follows:
// Category model
public function isActive() {
$active = $this->posts->filter(function($post) {
return $post->isActive();
}
}
// Post model
public function isActive() {
return (boolean) $this->comments()->where('active', 1)->count();
}
This works as expected, but it also includes eagerly loaded nested relationships where comments have an active field of 0. Obviously I'm doing this the wrong way but would appreciate any direction.
Another attempt:
User::with(['categories.posts.comments' => function($q) {
$q->where('active', 1);
}])->find(1);
Unfortunately, this also loads relations (categories and posts) that have no active comments. Replacing the relations with 'categories.posts.isActive' does not work either.
Still confusing because you didn't provide enough code but you may try something like this to get all the users with nested categories.posts.comments without any condition:
$users = User::with('categories.posts.comments')->get();
But it'll give you every thing even when you don't have any comments but to add condition you may try something like this:
// It should return all user models with `categories - posts - active comments`
$users = User::with('categories.posts.activeComments')->get();
Post model:
public function activeComments() {
return $this->hasMany('Comment')->where('active', 1);
}
You may also add more filters using constraints like:
$users = User::with(array('categories.posts.activeComments' => function($query){
$query->whereNull('comments.deleted_at');
}))->get();
But I'm not sure about it, don't know enough about your relationships, so just gave you an idea.

Resources