CakePHP 3 - How to match on indirect associations? [duplicate] - php

I have a belongsToMany association on Users and Contacts.
I would like to find the Contacts of the given User.
I would need something like
$this->Contacts->find()->contain(['Users' => ['Users.id' => 1]]);
The cookbook speaks about giving conditions to contain, custom finder methods and sing through association key, but I did not find out how to put these together.

Use Query::matching() or Query::innerJoinWith()
When querying from the Contacts table, then what you are looking for is Query::matching() or Query::innerJoinWith(), not (only) Query::contain().
Note that innerJoinWith() is usually preferred in order to avoid problems with strict grouping, as matching() will add the fields of the association to the select list, which can cause problems as they are usually not functionally dependent.
See Cookbook > Database Access & ORM > Query Builder > Filtering by Associated Data
Here's an example using your tables:
$this->Contacts
->find()
->innerJoinWith('Users', function(\Cake\ORM\Query $q) {
return $q->where(['Users.id' => 1]);
});
This will automatically add the required joins + conditions to the generated query, so that only those contacts are being retrieved that are associated to at least one user with the id 1.
Target the join table
In case you'd have a manually set up many to many association via hasMany and belongsTo, you can directly target the join table:
$this->Contacts
->find()
->innerJoinWith('ContactsUsers', function(\Cake\ORM\Query $q) {
return $q->where(['ContactsUsers.user_id' => 1]);
});
Including containments
In case you actually want to have all the associations returned in your results too, then just keep using contain() too:
$this->Contacts
->find()
->contain('Users')
->innerJoinWith('Users', function(\Cake\ORM\Query $q) {
return $q->where(['Users.id' => 1]);
});
That would contain all users that belong to a contact.
Restricting containments
In cases where you have multiple matches, and you'd wanted to contain only those matches, you'd have to filter the containment too. In this example it doesn't make much sense since there would be only one match, but in other situations it might be useful, say for example if you'd wanted to match all contacts that have active users, and retrieve the contacts including only the active associated users:
$this->Contacts
->find()
->contain(['Users' => function(\Cake\ORM\Query $q) {
return $q->where(['Users.active' => true]);
}])
->innerJoinWith('Users', function(\Cake\ORM\Query $q) {
return $q->where(['Users.active' => true]);
})
->group('Contacts.id');
Given that there could be duplicates, ie multiple active users for a single contact, you'll probably want to group things accordingly, in order to avoid retrieving duplicate contact records.
Deep associations
You can also target deeper associations that way, by using the dot notated path syntax known from Query::contain(). Say for example you had a Users hasOne Profiles association, and you want to match only on those users that want to receive notifications, that could look something like this:
->innerJoinWith('Users.Profiles', function(\Cake\ORM\Query $q) {
return $q->where(['Profiles.receive_notifications' => true]);
})
This will automatically create all the required additional joins.
Select from the other table instead
With these associations and your simple requirements, you could also easily query from the other side, ie via the Users table and use just Query::contain() to include the associated contacts, like
$this->Users
->find()
->contain('Contacts')
->where([
'Users.id' => 1
])
->first();
All the contacts can then be found in the entities contacts property.

While #ndm answer got me where I needed to with a similar issue, solution needed a little tweak.
Problem was getting users filtered by data on the joinTable adding matching didn't return the users. So this is what I ended up with, hope it'll help someone.
$this->Contacts
->find()
->contain('Users', function(\Cake\ORM\Query $q) {
return $q->matching('ContactsUsers', function(\Cake\ORM\Query $q) {
return $q->where(['ContactsUsers.some_field' => 1]);
}
});
This got me Contacts with Users who has some_field set to 1 in association table. Or am I overcomplicating and there is a better solution?

Related

get a values using many to many relations on laravel 5 models

i'm really new working with laravel 5.0, so I got this problem when I try to retrieve a result using a model. I have a users table, with a list of users who can be a manager or not, they can have assigned one or more companies, or none, a company table with companies which can have one or many managers, and a pivot table that I called companies_managers. I set up the relations in every model like this:
/***User model***/
public function companies()
{
return $this->belongsToMany('App\Company', 'companies_managers','id', 'manager_id');
}
and the same in Company model
public function managers()
{
return $this->belongsToMany('App\User', 'companies_managers', 'id', 'company_id');
}
I want to get the managers assigned to a company using a company id to get it, but it just gave me an huge object without the values I looking for (the names of the managers assigned to that company). This is the code that I tried:
$managers = Company::find($id)->managers();
I would appreciate any help you can give me
Using ->managers() (with the brackets) doesn't actually return the associated managers, but rather a Builder instance (the "huge object"), which you can then chain with additional parameters before finally retrieving them with ->get() (or another closure, like ->first(), ->paginate(), etc)
Using ->managers (without the brackets), will attempt to access the associated managers, and execute any additional logic to retrieve them.
So, you have 2 options:
$company = Company::with(["managers"])->findOrFail($id);
$managers = $company->managers;
Or
$company = Company::findOrFail($id);
$managers = $company->managers()->get();
Both of those will perform the necessary logic to pull the managers. ->with() and no brackets is slightly more efficient, doing it all in a single query, so bear that in mind.
You just need to split out your code;
// this will find the company based on the id, or if it cannot find
// it will fail so will abort the application
$company = Company::findOrFail($id);
// this uses the active company record and gets the managers based
// on the current company
$managers = $company->managers;
Thank you for your help guys, I solved the issue fixing the relations in the models to this:
return $this->belongsToMany('App\Company', 'companies_managers', 'manager_id', 'company_id');
and this
return $this->belongsToMany('App\User', 'companies_managers', 'company_id', 'manager_id');
The IDs that I had set were not the correct ones for belongsToMany function
And this
$managers = Company::find($id)->managers();
was a problem too, was a dumb mistake of my part. I solved the return of Builder instance using just return instead of dd(), in that way I got the values I looking for. Thanks everyone!

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

Laravel - Dynamic relationship using hasManyThough() and unique merge

I can think of several ad-hoc ways to do this, but I'm really looking for a 'best practices' type of solution.
I have 3 tables involved
- users (user_id)
- usages ('user_id', 'provider_id', 'nurse_id', 'patient_id')
- usage_alerts ('usage_id')
Im trying to eager load alerts using hasManyThrough() based on a user's role.
The user_id field is agnostic, and can apply to any role, so merging and filtering needs to take place.
Using $this->hasManyThrough('UsageAlert', 'Usage')->get() will return a collection, making the ->merge() method available. However, when eager loading, on return, i get an error since it's a collection object.
Call to undefined method Illuminate\Database\Eloquent\Collection::addEagerConstraints()
For example, this is my current relation (returns the error above)
public function alerts()
{
$alerts = $this->hasManyThrough('UsageAlert', 'Usage')->get();
if(Sentry::getUser()->inGroup(Sentry::findGroupByName('provider')))
$alerts->merge($this->hasManyThrough('UsageAlert', 'Usage', 'provider_id'));
if(Sentry::getUser()->inGroup(Sentry::findGroupByName('patient')))
$alerts->merge($this->hasManyThrough('UsageAlert', 'Usage', 'patient_id'));
if(Sentry::getUser()->inGroup(Sentry::findGroupByName('nurse')))
$alerts->merge($this->hasManyThrough('UsageAlert', 'Usage', 'nurse_id'));
return $alerts;
}
Any suggestions? Pperhaps too much complexity for a relationship?
Best practice manipulates the relationship, though official documentation on how lacks. For your scenario, you can union the additional queries into the primary "agnostic" relationship:
$relation = $this->hasManyThrough('UsageAlert', 'Usage');
foreach (['provider','patient','nurse'] as $group) {
if (Sentry::getUser()->inGroup(Sentry::findGroupByName($group))) {
$relation->getBaseQuery()->union(
$this->
hasManyThrough('UsageAlert', 'Usage', $group . '_id')->
getBaseQuery()->
select('UsageAlert.*') // limits union to common needed columns
);
}
}
return $relation;
This approach returns a Relation, rather than a Collection, as would be expected by API users.

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;

Querying on related models using Laravel 4 and Eloquent

Using Laravel 4 I have the following models and relations: Event which hasMany Record which hasMany Item. What I would like to do is something like this
Item::where('events.event_type_id', 2)->paginate(50);
This of cause doesn't work as Eloquent doesn't JOIN the models together when retrieving the records. So how do I go about this without just writing the SQL myself (which I would like to avoid as I want to use pagination).
What you want is eager loading.
It works like this if you want to specify additional constraints:
Item::with(array('events' => function($query) {
return $query->where('event_type_id', 2);
}))->paginate(50);
There is a pull request pending here https://github.com/laravel/framework/pull/1951.
This will allow you to use a constraint on the has() method, something like this:
$results = Foo::has(array('bars' => function($query)
{
$query->where('title', 'LIKE', '%baz%');
}))
->with('bars')
->get();
The idea being you only return Foos that have related Bars that contain the string 'baz' in its title column.
It's also discussed here: https://github.com/laravel/framework/issues/1166. Hopefully it will be merged in soon. Works fine for me when I update my local copy of the Builder class with the updated code in the pull request.

Categories