Laravel eloquent relationship and pivot tables - php

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.

Related

Laravel Eloquent ORM: get value from passing through multiple one to many relationships

I have three tables which are users, loans, statuses
The relationship is like this:
A user can have many loans. a loan has many status steps. in the statuses table I have a column called status, basically it telsl this step yes, no. pending sort of situation.
the table structure look like this:
users table
->id
->...
loans table
->id
->...
->user_id (it is the foreign key ->references('id')->on('users');
statuses table
->id
->...
->status (can be "yes", "no", "pending")
->...
->loan_id (it is the foreign key ->references('id')->on('loans');
the models look like this:
in the User model :
public function loans(){
return $this->belongsToMany('App\Loan');
}
in the Loan model:
public function users(){
return $this->belongsToMany('App\User');
}
public function statuses(){
return $this->hasMany('App\Status');
}
in the Status model:
public function loan(){
return $this->belongsTo('App\Loan');
}
My question is how to get the status yes number for each user. say I have five users, each user have multiple loans. each loan have, say 20 steps. but different loan many have different yes steps . I would like to use laravel eloquent ORM to get a array tell me each user get how many yes at certain time. So I would be able to loop through this array in my front end blade file to display users progress. thanks!
Laravel's Collection, which you get when you use Eloquent, is great for this kind of operation. Say you want to get the 'yes' statuses of one user:
//yesStatuses is itself a Collection, but it can be used in a foreach like an array
$yesStatuses = $user->loans
->map(function ($loan) {
return $loan->statuses;
})
->filter(function ($status) {
return $status->status === 'yes';
});
//Number of statuses === 'yes'
$yesStatuses->count();
//If you need it as a regular array
$yesStatuses->toArray();
When you query your users table you should take care of loading your loans and statuses eagerly, otherwise you'll be performing many queries for this operation. Something like this:
$users = App\User::with('loans.statuses')->get();
More on this:
Eloquent Collections
Eager loading of Eloquent Models
Thanks for your help, SrThompson!
I think map() function in the collection really helps. since my goal is to loop through the users to show each user's attributes plus count the "yes" status for that user. So I added my 'statuscount' into the user collection as:
$user['statuscount']=$yesStatusesCount;
my working code block like this:
$users=User::with('loans')->with('loans.statuses')
->map(function ($user){
$yesStatuses = $user->loans->map(function ($loan) {
return $loan->statuses->where('status','yes')->count();
});
$yesStatusesCount=array_sum($yesStatuses->toArray());
$user['statuscount']=$yesStatusesCount;
return $user;
});
Then, in my balde file, I would be able to display the number of "yes" status of the user in the #foreach loop. Thanks again!

Applying hasManyThrough to deeper relationships

The Laravel docs seem to indicate that the hasManyThrough declaration can only be used for relationships that are two levels "deep". What about more complex relationships? For example, a User has many Subjects, each of which has many Decks, each of which has many Cards. It's simple to get all Decks belonging to a User using the hasManyThrough declaration, but what about all Cards belonging to a User?
I created a HasManyThrough relationship with unlimited levels: Repository on GitHub
After the installation, you can use it like this:
class User extends Model {
use \Staudenmeir\EloquentHasManyDeep\HasRelationships;
public function cards() {
return $this->hasManyDeep(Card::class, [Subject::class, Deck::class]);
}
}
As stated in the comments, hasManyThrough doesn't support this level of specificity. One of the things you can do is return a query builder instance going the opposite direction:
//App\User;
public function cards()
{
Card::whereHas('decks', function($q){
return $q->whereHas('subjects', function($q){
return $q->where('user_id', $this->id);
});
});
}
We're going from Cards -> Decks -> Subjects. The subjects should have a user_id column that we can then latch onto.
When called from the user model, it would be done thussly:
$user->cards()->get();
Well, actually the best solution will be put the extra column to Card table - user_id, if you have so frequent needs to get all cards for the user.
Laravel provides Has-Many-Through relations for 2-depth relation because this is very widely often used relation.
For the relations Laravel does not support, you need to figure out the best table relationship yourself.
Any way, for your purpose, you can use following code snap to grab all cards for the user, with your current relation model.
Assumption
User has hasManyThough relationship to Deck,
So Project model will have following code:
public function decks()
{
return $this->hasManyThrough('Deck', 'Subject');
}
Deck has hasMany relationship to Card
Code
$deck_with_cards = $user->decks()->with("cards")->get();
$cards = [];
foreach($deck_with_cards AS $deck) {
foreach ($deck->cards as $c) {
$cards[] = $c->toArray();
}
}
Now $cards has all cards for the $user.

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();

Schema.org + Laravel = way too complicated?

I recently completed my first Laravel project (I really liked the look of the framework and wanted to give it a try) and was reflecting on what I think I did well, what I did poorly, and what I want to do differently next time.
One of the biggest issues I noticed I had was that in a gross effort to avoid duplication of data I had some pretty hideous work to do in order to extract meaningful information from my database.
At the outset of the project I used schema.org as a rough basis for designing my models. For instance, when I wanted to store Employees and Customers:
Create a Person model
A Person hasMany Roles
A Customer is a (morphs) Role
An Employee is a (morphs) Role
An Organization hasMany Employees
An Organization hasMany Customers
At first this seems nice, because a person can be both an employee and a customer (you can shop at WalMart and work for WalMart). Also since a person can have many roles, they can be multiple customers and multiple employees -- if they shop at more than one store or if they have two jobs.
However I ran into issues when I wanted to do something like "find all employees for organization X whose name is John":
$employees = $organization->employees;
$employeesNamedJohn = new Collection();
foreach( $employees as $employee ) {
if( $employee->role->person->name == 'John' )
$employeesNamedJohn->add( $employee );
}
This seems awfully complicated (not to mention inefficient), especially considering if I hadn't invented the messed up schema it would just be a one-liner:
$employees = $organization->employees()->where('name', 'John')->get();
So am I just doing something wrong? Does Laravel have a simple way to handle complex relationships like this, or is the answer to simply never let your relationships get this complicated?
Adding query scopes on your models could really help:
class Role extends Model
{
public function scopeNamed($query, $name)
{
$query->whereHas('person', function ($query) use ($name) {
$query->where('name', $name);
});
}
}
class Employee extends Model
{
public function scopeNamed($query, $name)
{
$query->whereHas('role', function ($query) use ($name) {
$query->named($name);
});
}
}
Then you could do this:
$employees = $organization->employees()->named('John')->get();
Much better.
As an extra,
I did implement Josheph Silber his answer in
https://github.com/noud/seo
Look at the Entity-Relationship Diagram below in the readme.md
Entity role having fields
roleable_id (the id to where the foreign key points)
roleable_type (the entity the foreign key points to)
In Laravel this is
Role model
trait for Employee, Customer, ...
so you can fetch Employees named $name

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