Laravel Conditional Relationship - php

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!

Related

Laravel eloquent relationship and pivot tables

Im experimenting with an Laravel application where I have users and teams. The tables looks a little bit like this (simplified):
users
id*
...
teams
id*
...
team_user
team_id*
user_id*
isLeader
confirmed
As you can see, a user can be part of a number of teams, and *he can also be appointed leader of a given team. A team can have multiple leaders.
The user model has the following relationships:
// Returns all the teams connected to the user and where the confirmed timestamp is set
public function teams()
{
return $this->belongsToMany(Team::class)->wherePivot('confirmed', '!=', null);
}
// Returns all the teams where the user is appointed team leader
public function teamleaderTeams()
{
return $this->belongsToMany(Team::class)->wherePivot('isLeader', '=', 1);
}
The team has:
public function confirmedUsers()
{
return $this->belongsToMany(User::class)->where('confirmed', '!=', null);
}
I need something that returns all the users that the user is team leader for. So if you are not a team leader the result would off course be empty, but if you are it should return all the users from all the teams where you are appointed leader.
Ive tried asking around, and have gotten some suggestions, but not really arrived at a solution.
I do kindof understand what I want (I think). Sooo... since you can tell which teams a user is team leader for through the teamleaderTeams() relation, I can loop through each and then ask to get all the confirmed users through the confirmedUsers() relation. But I've only managed to accomplish this in the controller and it just seems messy.
I.e. this only crashes the browser (it seems to be in an infinite loop or something, and I dont really understand why).
public function getLeaderForAttribute()
{
$users = collect();
foreach($this->teamleaderTeams as $team)
{
foreach ($team->confirmedUsers as $user) {
$users->add($user);
}
}
return $users->unique('id');
}
Anyway, anyone got a nice solution for a teamleaderUsers() relation (not really a good name for it), that returns all the users from all the teams that a given user is team leader for (thats a mouth full)?
I think this is a nice time to use Pivot models. You can define a pivot model by extending the Pivot class. Furturemore, you can define relationships in the pivot model. So, if you have users relationship in your pivot model, you can make a query like this:
TeamUser::with('users')->where('isLeader', 1); // If the pivot model is called TeamUser
Of course you can exclude a specific user as usual:
TeamUser::with(['users' => function($query) {
$query->where('id', '<>', 1); // If we want to exclude user with id 1
}])
->where('isLeader', 1);
Of course, you can also make an additional where clause in the relatonship:
public function teamLeaders()
{
return $this->hasMany('users')->where('isLeader', 1);
}
Please read more about it here and here is the API
Good luck!
First, you have a typo here:
$users->add($user);
It should be Collection::push:
$users->push($user);
Second, I think your approach is okay. However, if performance becomes your problem, you might want to write a custom query for optimization, rather than depending on Laravel ORM.
Third, you can name relations like this: leadingTeams instead of teamleaderTeams and leadingUsers instead of teamleaderUsers.

how to get none related objects of Many to Many relationship

I have user and news table and then a middle table called news_user, the middle table determines which news has been seen by the user. I can easily get objects that have been seen but, but now I need to show the user objects that have not been seen.
I did a workaround with putting all the seen news id in an array and look for news where id differs from the array. but I don't consider it as the healthiest solution.
This is what I did:
$seenNewses = DB::table('news_user')->where('user_id', Auth::id())
->pluck('news_id')->toArray();
$notSeenNewses = News::whereNotIn('id', $seenKeepers)
->orderBy('id', 'asc')->first();
Is there a way I can do this with a single query?
P.S: I have seen similar questions but they got little confused. Any answer is appreciated.
First in your News Model you need to make relationship like this
public function users()
{
return $this->belongsToMany(News::class,'news_user','news_id','user_id');
}
Below command will give you all the news which are not seen by any users yet
$notSeenNewses = News::doesntHave('users')->get();
If you want to put any condition then also check for
$notSeenNewses = News::whereDoesntHave('users', function ($query) {
$query->where('YOUR CONDITION');
})->get();
It's not tested but you can modify as per your need.

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 eloquent query with several layers of model relationships

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

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;

Categories