Laravel eloquent query with several layers of model relationships - php

I'm having some trouble doing eloquent database queries in Laravel. I have searched around a lot, but I don't know exactly what to search for to get my answer.
I'm making a web page in Laravel for kindergardens, and I need to get all children connected to a kindergarden in a single collection/array. The problem is that children are only connected to the kindergarden through their group(s). So the query needs to be able to get all children that are part of any group connected to the kindergarden.
The model relationships are like this:
A kindergarden has many groups, and groups belong to a kindergarden.
A group has many children, and children have many groups.
The best I've managed to do so far is this:
public function getChildren(){
$kid = Session::get('kid');
$k = Kindergarden::find($kid);
$groups = $k->group;
$children;
foreach($groups as $g){
$children = $children->merge($g->children);
}
$children = $children->toArray();
return Response::json($children);
}
But this makes duplicates when children are in several groups in the same kindergarden. It also seems like an unnecessary complicated way to do it.
For a while I also tried to get the hasManyThrough-relationship to work, but it doesn't seem to work when there'a a many-to-many relationship and a pivot table involved.
I tried with this:
class Kindergarden extends Eloquent {
public function children()
{
return $this->hasManyThrough('Children', 'Group', 'kid', 'gid');
}
}
and then tried to call
Kindergarden::find(1)->children;
I'm sure there is a really simple way to do this, but I'm totally new to laravel and not really that great at sql, so I haven't been able to find anything to help me figure this out.
Edit:
Managed to find a way to do it using Fluent:
$children = DB::table('children')
->join('children_group', 'children.chiid', '=', 'children_group.chiid')
->join('group', 'group.gid', '=', 'children_group.gid')
->where('group.kid', '=', $kid)
->groupby('children.chiid')
->get();
Still want to be able to do it using Eloquent, though.

In your Kindergarden class:
public function groups()
{
$this->hasMany('Groups', 'group_id', 'id');
}
In your Group class:
public function children()
{
$this->hasMany('Children', 'child_id', 'id')
}
Then try:
$children = Kindergarden::with('groups.children')->get();
This way you could go through each group in Kindergarden, and display each child that is apart of each group.
Also just a suggestion: In your database tables, I would name your related id's (such as a group ID in your Kindergarden table) group_id, instead of gid. Makes it much more readable and you know exactly what it's related to. Same with your child id, name it 'child_id' instead of 'chiid'.
EDIT: Since I don't know your table structure, take the functions above with a grain of salt. I hope what I suggested gets you on the right path! Let me know if it helps

Related

Using nestedset in Laravel to design a nested treant tree

I'm trying to figure out how these nestedsets in Laravel works. I've an many to many relation between organizations and departments. An organization can have many departments. An department can have many departments. For this I'm using Nestedsets.
What I'm trying to do, is retrieving all organizations from a user. On this query I'd like to retrieve all departments attached to these organizations. I'd like the structure, so I've a infinite parent -> child relation on my departments, so I'm able to build a structuretree using treant.js.
I'm pretty sure I've everything build correctly in my database, so my first thought were to use with. However it seems like I'm only getting the first children. Here is an example:
$currentUser->organizations()->with(
'departments.children',
'departments.commodities',
'departments.children.commodities',
)->get()
I've to include children.[model] for every nested department. So if I've two levels, I've to add departments.children.children.commodities, and so on. This seems pretty retarded!
I've been trying pretty many different approches to get a proper solution, but the one below is my best solution for now. I just feel like I'm using the nestedset-library wrong.
public function getUserDepartmentTree() {
foreach ( $this->organizations()->get() as $organization ) {
$dep[] = $organization->departments()->get()->toTree();
}
return $dep;
}
So my question is, how should I get all relational data from my departments tree-structured?
For this you'll want to use descendants instead of children as children will only return the direct/first child models whereas descendants will return everything under a certain node.
Since this will add the relationship as descendants instead of children you'll need to tweak it slightly i.e. change the name of the relationship and then use the toTree() method:
$organizations = $currentUser->organizations()
->with('departments.commodities', 'departments.descendants.commodities')
->get()
->map(function ($organization) {
$organization->departments->map(function ($department) {
return $department->setRelation('children', $department->descendants->toTree())->unsetRelation('descendants');
});
return $organization;
});

Laravel Conditional Relationship

Here is the situation: I have 4 models.
Invoices
Creditors
Debtors
Timelines
These objects are related as such:
An invoice has a Creditor, a Debtor, and may have a Timeline.
A Debtor also may have a Timeline.
A Creditor has a Timeline.
I am trying to write the timeline() relationship on Invoice to take into account the order of priority of these objects. If the Debtor has a Timeline, it is used. Else, if the Invoice has a Timeline, it is used. Lastly, the Creditor's Timeline is used if neither of the above were found.
I have looked around quite a bit, and I am struggling to find a solution to this. In my head, it would be simple, though it is proving not so.
Approaches I have taken to no avail...
Putting a raw select statement into the relationship where the select will return the _rowid of the timeline desired:
return $this->hasOne('Timeline', '_rowid', 'SELECT ...')
Same idea, using DB::raw():
return $this->hasOne('Timeline', '_rowid', DB::raw('SELECT ...'))
Trying to load using selects and joins to the relationship query:
return $this->hasOne('Timeline', '_rowid', 'timeline_rowid')
->select('Timelines_Country.*')
->join(DB::raw('(select if(Debitor.timeline_rowid is not null, Debitor.timeline_rowid, if(Invoice.timeline_rowid is not null, Invoice.timeline_rowid, Creditor.timeline_rowid)) as timeline_rowid from Invoice join Debitor on Invoice.unique_string_cd = Debitor.unique_string_cd join Creditor on Creditor.id = Invoice.creditor_id where Debitor.timeline_rowid is not null and Invoice.timeline_rowid is not null) temp'), function($join){
$join->on('Timelines_Country._rowid', '=', 'temp.timeline_rowid');
})
->whereNotNull('temp.timeline_rowid');
The final solution seemed the closest, but it still included the standard relationship query of "WHERE IN (?,?,?)" where the list was the timeline_rowids from the collection of Invoices, which caused certain timelines to still not be found.
Thanks in advance for any help!
Ok. I guess I fundamentally misunderstood how relationships work. Because they are loaded after the model is already instantiated, the array for the wherein part of the relationship query is built from accessing attributes of the model. Once I discovered that, it was actually an easy fix.
I wrote this function, which returns the timeline_rowid based on the relationships:
public function getDerivedTimelineAttribute()
{
if ($this->debtor && $this->debtor->timeline_rowid)
{
return $this->debtor->timeline_rowid;
}
else if ($this->timeline_rowid)
{
return $this->timeline_rowid;
}
else if ($this->creditor->timeline_rowid)
{
return $this->creditor->timeline_rowid;
}
else
{
Log::Debug("can't find timeline", ['cdi' => $this->unique_string_cdi]);
return 0;
}
}
and then modified the relationship to read as:
public function timeline()
{
return $this->hasOne('Timeline', '_rowid', 'derivedTimeline');
}
I hope this helps someone else. Drove me nuts for a while figuring it out!

Fetching and filtering relations in Laravel Eloquent

I have the following models in Eloquent: groups, threads, comments and users. I want to find all comments in a specific group from a specific user.
This is my current approach:
$group->threads->each(function ($thread) use ($user_id)
{
$user_comments = $thread->comments->filter(function ($comment) use ($user_id)
{
return $comment->owner_id == $id;
});
});
This looks ugly as hell, is probably slow as hell, and I just want to get rid of it. What is the fastest and most elegant way in Eloquent to get to my result set?
If a group hasMany threads, and a thread hasMany comments, you can add another relationship to group: group hasMany comments through threads.
On the group:
public function comments() {
return $this->hasManyThrough('Comment', 'Thread');
}
Now, you can get the comments in a group by $group->comments;
From here, you can tack on the requirement for the user:
$user_comments = $group->comments()->where('owner_id', $user_id)->get();
If you want, you can extract the where out into a scope on the Comment.
patricus solution pointed me in the right direction. I cross posted my question to the laracasts Forum and got a lot of help from Jarek Tkaczyk who also frequently visits this site.
hasManyThrough() for the Group model is the way to go:
public function comments()
{
return $this->hasManyThrough('ThreadComment', 'Thread');
}
There a couple of caveats, though:
Use the relation object instead of a collection ($group->comments(), NOT $group->comments)
If you use Laravel’s soft deletes you can’t just change the get() to a delete(), because you’ll get an ambiguity error for the column updated_at. You can’t prefix it either, it’s just how Eloquent works.
If you want to delete all comments from a specific user in a specific group you’ll have to do it a little bit differently:
$commentsToDelete = $group->comments()
->where('threads_comments.owner_id', $id)
->select('threads_comments.id')
->lists('id');
ThreadComment::whereIn('id', $commentsToDelete)->delete();
You basically fetch all relevant IDs in the first query and then batch delete them in a second one.

Laravel 4 - Eloquent way to attach a where clause to a relationship when building a collection

This may be a dupe but I've been trawling for some time looking for a proper answer to this and haven't found one yet.
So essentially all I want to do is join two tables and attach a where condition to the entire collection based on a field from the joined table.
So lets say I have two tables:
users:
-id
-name
-email
-password
-etc
user_addresses:
-address_line1
-address_line2
-town
-city
-etc
For the sake of argument (realising this may not be the best example) - lets assume a user can have multiple address entries. Now, laravel/eloquent gives us a nice way of wrapping up conditions on a collection in the form of scopes, so we'll use one of them to define the filter.
So, if I want to get all the users with an address in smallville, I may create a scope and relationships as follows:
Users.php (model)
class users extends Eloquent{
public function addresses(){
return $this->belongsToMany('Address');
}
public function scopeSmallvilleResidents($query){
return $query->join('user_addresses', function($join) {
$join->on('user.id', '=', 'user_addresses.user_id');
})->where('user_addresses.town', '=', 'Smallville');
}
}
This works but its a bit ugly and it messes up my eloquent objects, since I no longer have a nice dynamic attribute containing users addresses, everything is just crammed into the user object.
I have tried various other things to get this to work, for example using a closure on the relationship looked promising:
//this just filters at the point of attaching the relationship so will display all users but only pull in the address where it matches
User::with(array('Addresses' => function($query){
$query->where('town', '=', 'Smallville');
}));
//This doesnt work at all
User::with('Addresses')->where('user_addresses.town', '=', 'Smallville');
So is there an 'Eloquent' way of applying where clauses to relationships in a way that filters the main collection and keeps my eloquent objects in tact? Or have I like so many others been spoiled by the elegant syntax of Eloquent to the point where I'm asking too much?
Note: I am aware that you can usually get round this by defining relationships in the other direction (e.g. accessing the address table first) but this is not always ideal and not what i am asking.
Thanks in advance for any help.
At this point, there is no means by which you can filter primary model based on a constraint in the related models.
That means, you can't get only Users who have user_address.town = 'Smallwille' in one swipe.
Personally I hope that this will get implemented soon because I can see a lot of people asking for it (including myself here).
The current workaround is messy, but it works:
$products = array();
$categories = Category::where('type', 'fruit')->get();
foreach($categories as $category)
{
$products = array_merge($products, $category->products);
}
return $products;
As stated in the question there is a way to filter the adresses first and then use eager loading to load the related users object. As so:
$addressFilter = Addresses::with('Users')->where('town', $keyword)->first();
$users= $addressFilter->users;
of course bind with belongsTo in the model.
///* And in case anyone reading wants to also use pre-filtered Users data you can pass a closure to the 'with'
$usersFilter = Addresses::with(array('Users' => function($query) use ($keyword){
$query->where('somefield', $keyword);
}))->where('town', $keyword)->first();
$myUsers = $usersFilter->users;

Laravel, How to use where conditions for relation's column?

I'm using Laravel and having a small problem with Eloquent ORM.. I can get this working simply with SQL query using a JOIN but I can't seem to get it working with Eloquent!
This is what I want, I have two tabels. one is 'Restaurants' and other is 'Restaurant_Facilities'.
The tables are simple.. and One-To-One relations. like there is a restaurant table with id, name, slug, etc and another table called restaurant_facilities with id, restaurant_id, wifi, parking, etc
Now what I want to do is.. load all restaurants which have wifi = 1 or wifi = 0..
How can i do that with Eloquent ? I have tried eager loading, pivot tables, with(), collections() and nothing seems to work!
The same problem I have for a Many-To-Many relation for cuisines!
I have the same restaurant table and a cuisine table and a restaurant_cuisine_connection table..
but how do I load all restaurants inside a specific cuisine using it's ID ?
This works.
Cuisine::find(6)->restaurants()->get();
but I wanna load this from Restaurant:: model not from cuisines.. because I have many conditions chained together.. its for a search and filtering / browse page.
Any ideas or ways ? I've been struggling with this for 3 days and still no answer.
Example Models :
class Restaurant extends Eloquent {
protected $table = 'restaurants';
public function facilities() {
return $this->hasOne('Facilities');
}
}
class Facilities extends Eloquent {
protected $table = 'restaurants_facilities';
public function restaurant() {
return $this->belongsTo('Restaurant');
}
}
PS :
This seems to be working.. but this is not Eloquent way right ?
Restaurant::leftJoin(
'cuisine_restaurant',
'cuisine_restaurant.restaurant_id',
'=', 'restaurants.id'
)
->where('cuisine_id', 16)
->get();
Also what is the best method to find a count of restaurants which have specific column value without another query ? like.. i have to find the total of restaurants which have parking = 1 and wifi = 1 ?
Please help on this.
Thank you.
I don't see anything wrong with doing the left join here, if you have to load from the Restaurant model. I might abstract it away to a method on my Restaurant model, like so:
class Restaurant extends Eloquent {
protected $table = 'restaurants'; // will be default in latest L4 beta
public function facility()
{
return $this->hasOne('Facility');
}
// Or, better, make public, and inject instance to controller.
public static function withWifi()
{
return static::leftJoin(
'restaurant_facilities',
'restaurants.id', '=', 'restaurant_facilities.restaurant_id'
)->where('wifi', '=', 1);
}
}
And then, from your routes:
Route::get('/', function()
{
return Restaurant::withWifi()->get();
});
On the go - haven't tested that code, but I think it should work. You could instead use eager loading with a constraint, but that will only specify whether the facility object is null or not. It would still return all restaurants, unless you specify a where clause.
(P.S. I'd stick with the singular form of Facility. Notice how hasOne('Facilities') doesn't read correctly?)
I stumbled across this post while trying to improve my REST API methodology when building a new sharing paradigm. You want to use Eager Loading Constraints. Let's say you have an api route where your loading a shared item and it's collection of subitems such as this:
/api/shared/{share_id}/subitem/{subitem_id}
When hitting this route with a GET request, you want to load that specific subitem. Granted you could just load that model by that id, but what if we need to validate if the user has access to that shared item in the first place? One answer recommended loading the inversed relationship, but this could lead to a confusing and muddled controller very quickly. Using constraints on the eager load is a more 'eloquent' approach. So we'd load it like this:
$shared = Shared::where('id', $share_id)
->with([ 'subitems' => function($query) use ($subitem_id) {
$query->where('subitem_id', $subitem_id)
}]);
So where only want the subitem that has that id. Now we can check if it was found or not by doing something like this:
if ($shared->subitems->isEmpty())
Since subitems is a collection (array of subitems) we return the subitem[0] with this:
return $shared->subitems[0];
Use whereHas to filter by any relationship. It won't join the relation but it will filter the current model by a related property. Also look into local scopes to help with situations like this https://laravel.com/docs/5.3/eloquent#local-scopes
Your example would be:
Restaurant::whereHas('facilities', function($query) {
return $query->where('wifi', true);
})->get();
Restaurant::whereHas('cuisines', function($query) use ($cuisineId) {
return $query->where('id', $cuisineId);
})->get();
To achieve the same thing with local scopes:
class Restaurant extends Eloquent
{
// Relations here
public function scopeHasWifi($query)
{
return $query->whereHas('facilities', function($query) {
return $query->where('wifi', true);
});
}
public function scopeHasCuisine($query, $cuisineId)
{
return $query->whereHas('cuisines', function($query) use ($cuisineId) {
return $query->where('id', $cuisineId);
});
}
}
For local scopes you DO NOT want to define them as static methods on your model as this creates a new instance of the query builder and would prevent you from chaining the methods. Using a local scope will injects and returns the current instance of the query builder so you can chain as many scopes as you want like:
Restaurant::hasWifi()->hasCuisine(6)->get();
Local Scopes are defined with the prefix scope in the method name and called without scope in the method name as in the example abover.
Another solution starring whereHas() function:
$with_wifi = function ($query) {
$query->where('wifi', 1);
};
Facilities::whereHas('restaurant', $with_wifi)
Nice and tidy.
Do you absolutely have to load it from the Restaurant model? In order to solve the problem, I usually approach it inversely.
Facilities::with('restaurant')->where('wifi' ,'=', 0)->get();
This will get all the restaurant facilities that match your conditions, and eager load the restaurant.
You can chain more conditions and count the total like this..
Facilities::with('restaurant')
->where('wifi' ,'=', 1)
->where('parking','=', 1)
->count();
This will work with cuisine as well
Cuisine::with('restaurant')->where('id','=',1)->get();
This grabs the cuisine object with the id of 1 eager loaded with all the restaurants that have this cuisine

Categories