Filtering Eloquent ORM query by relation - php

I need to filter my Eloquent query by relation. I have following relations:
User **has many** achievements
Game **has many** achievements
Now I need to filter achievements of user A to those gained in game B. This can be done this way:
$user->achievements()->whereGameId($game->id)
This is fine, but can I use $game object directly instead of filtering by ugly ID column?

Injecting $game object into a closure method and doing querying while eager loading is a way better approach in my opinion.
whereHas and orWhereHas is for limiting the result by querying relations.
$game = Game::find($gameID);
//magic happens here
$users = User::whereHas('achievements', function($query) use($game){
$query->where('gameId', $game->id);
})
->with('achievments') //if you want to have them inside collection
->get();
This limits both User (main collection result) along with achievments (relation).
Here's a detailed information (Querying Relations).
If you just want to filter achievments, but not User collection, you just need a closure function on eager loading:
$game = Game::find($gameID);
//magic happens here
$users = User::with(
array('achievements' => function($query) use($game){
$query->where('gameId', $game->id);
}))
->get();
This just limits achievments relation, but does not affect User (main collection)
Here's a detailed description (Eager Loading Constains)

edit:
So here's the PR https://github.com/laravel/framework/pull/4267 that lets you do this:
// 'game' being relation on the Achievement model
$user->achievements()->hasIn('game', $game);
// or equivalent dynamic call
$user->achievements()->hasInGame($game);
// of course you may use it like any other Builder method, for example:
User::hasInAchievements($achievement)->get();
No straightforward way to do this, unless the Game is related somehow to the User.
To be clear, say User and Game is related, then you can access achievements on the games Collection injecting Game model instead of 'ugly' $game->id:
$user->games->find($game)->achievements;

Related

Laravel eloquent multiple table join with filter

There are theree tables in my system.
Students
Articles
categories
Student can write many articles and a article is belong to just one student. And A Article can have only one category.
Controller
public function all_articles_by_student_by_category(Request $request){
$students_id = $request->students_id;
$categories_id = $request->categories_id;
$article_list = Students::find($students_id)->articles->all();
//This return Something like, Select All Articles Written by Damith
}
Model
class Students extends Model
{
protected $fillable = ['id','first_name', 'last_name', 'age', 'created_at', 'updated_at'];
public function articles()
{
return $this->hasMany('App\Articles');
}
}
What I am try to get
Something like, Select All Articles Written by Damith for Technology Category (Category Name should be there)
What I able to do so far
Something like, Select All Articles Written by Damith using $article_list = Students::find($students_id)->articles->all(); (You can find this code from controller)
What I want from you
How do I modify $article_list = Students::find($students_id)->articles->all(); to get, something like, Select All Articles Written by Damith for Technology Category. (Category name must be there in result and it is on category table, and for where condtion you can use the category_id which is i the article table )
First off with what you have done so far the ->all() method is not needed when getting the records for a relation on a model, this would return all of the articles linked to that student:
Students::find($students_id)->articles
Go through Articles Model
You could do something like:
Article::where('student_id', $students_id)
->where('category_id', $category_id)->get();
Which would acheive the result you are after.
Go through Students Model
If you want to go through Students Model you can constrain the relation using the with method.
$student = Students::with(['articles' => function($query) use ($category_id) {
$query->where('category_id', $category_id);
}])->find($student_id);
$filteredArticles = $student->articles
Useful Links
Laravel Docs 5.5 for Eager Loading : https://laravel.com/docs/5.5/eloquent-relationships#eager-loading
When accessing Eloquent relationships as properties, the relationship data is "lazy loaded". This means the relationship data is not actually loaded until you first access the property. However, Eloquent can "eager load" relationships at the time you query the parent model.
Laravel Docs 5.5 for Constraining Eager Loads: https://laravel.com/docs/5.5/eloquent-relationships#constraining-eager-loads
Sometimes you may wish to eager load a relationship, but also specify additional query constraints for the eager loading query.
Something like this should work:
$technologyArticles = Articles::where('student_id', '=', $students_id)->where('category_id', '=', $categories_id)->get();

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 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.

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