Eloquent with nested whereHas - php

Currently I have this whereHas in a collection of my model:
$query = self::whereHas('club', function($q) use ($search)
{
$q->whereHas('owner', function($q) use ($search)
{
$q->where('name', 'LIKE', '%'. $search .'%');
});
});
I was under the impression the code above could be as such:
$query = self::whereHas('club.owner', function($q) use ($search)
{
$q->where('name', 'LIKE', '%'. $search .'%');
});
I'm aware this is already a lot of power, but even then, if I have a nested relationship 5 levels deep, things will get ugly.
Update:
As stated in the comments, I ended up not making my question clear, I apologize.
I will try to use a simple example, consider $owner->club->membership->product->package, now from owners I want to search a certain package, it would be something like this:
$query = self::whereHas('club', function($q) use ($search)
{
$q->whereHas('membership', function($q) use ($search)
{
$q->whereHas('product', function($q) use ($search)
{
$q->whereHas('package', function($q) use ($search)
{
$q->where('alias', 'LIKE', '%'. $search .'%');
});//package
});//product
});//membership
});//club
Is this correct? Is there a shortcut?

Update: the PR has been just merged to 4.2, so now it's possible to use dot nested notation in has methods ( ->has('relation1.relation2) ->whereHas('relation1.relation2, .. )
Your question remains a bit unclear or you misunderstand whereHas() method as it is used to filter models (users in this case) and get only those that have related models fitting search conditions.
It seems that you want to find Packages from the context of a given User, so no need to use whereHas method.
Anyway depending on the relations (1-1,1-m,m-m) this can be easy or pretty hard and not very efficient. As I stated, loading nested relations means that for every level of nesting comes another db query, so in this case you end up with 5 queries.
Regardless of the relations you can invert this chain like this, as it will be easier:
edit: This is not going to work atm as whereHas() doesn't process dot nested relations!
// given $user and $search:
$packages = Package::where('alias','like',"%$search%")
->whereHas('product.membership.club.user', function ($q) use ($user) {
$q->whereId($user->id);
})->get();
As you can see this is much more readable, still runs 5 queries.
Also this way you get $packages, which is a single Collection of the models you wanted.
While from the context of a user you would get something like this (depending on the relations again):
$user
|-club
| |-membership
| | |-product
| | | |-packages
| | |-anotherProduct
| | | |-packages
| | |-yetAnotherProduct
| | |-packages
| |-anotherMembership
.....
You get the point, don't you?
You could fetch the packages from the Collection, but it would be cumbersome. It's easier the other way around.
So the answer to your question would be simply joining the tables:
// Let's assume the relations are the easiest to handle: 1-many
$packages = Package::where('alias','like',"%$search%")
->join('products','packages.product_id','=','products.id')
->join('memberships','products.membership_id','=','memberships.id')
->join('clubs','memberships.club_id','=','clubs.id')
->where('clubs.user_id','=',$user->id)
->get(['packages.*']); // dont select anything but packages table
Of course you can wrap it in a nice method so you don't have to write this everytime you perform such search.
Performance of this query will be definitely much better than separate 5 queries shown above. Obviously this way you load only packages, without other related models.

Just building upon Jarek's answer one may add a scope utility to their model:
public function scopeWhereRelatedIs($query, $relation, $related) {
$query->whereHas(
$relation,
function ($query) use ($related) {
$query->where('id', '=', $related->id);
}
);
}
With this in place, the usage becomes really elegant, such as:
$packages = Package::where('alias','like',"%$search%")
->whereRelatedIs('product.membership.club.user', $user)
->get();

Related

Laravel / Eloquent: nested WhereHas

I've just started learning Laravel and stumbled upon one issue which I can't make work using the Eloquent relationships.
Let's assume that I have a Worker model, a Skill model and a pivot table skills_workers to keep the many-to-many relationship.
When I'm trying to get all the workers, who have following skill, then it goes without a problem using the following syntax:
$skill='carpenter';
$workers=Worker::whereHas('skills', function (Builder $query) use($skill){
$query->where('name','=',$skill);
})->get()();
The problem begins when I need to find all workers who have the set of skills. For example, carpenter-driver-chef (just for example). If the worker should have one of the skills, then I'd just use the whereIn function, but I need to find the worker who posess all of the skills in array.
I can't make the nested WhereHas as every time the user performs a search the skill set might be different. Sometimes it's just 1 skill, sometimes 2 and so on.
So the following construction:
$skills=['carpenter','driver'];
$workers=Worker::whereHas('skills', function (Builder $query) use($skills){
$query->where('name','=',$skills[0]);
})->whereHas('skills', function (Builder $query) use($skills){
$query->where('name','=',$skills[1]);
})->get();
is not an option.
Is there a way to use whereHas inside of a foreach loop, for example? Or, maybe, there is a more elegant way of performing such queries? None of the other posts on StackOverflow that I've found, helped...
I'd really like to avoid using the raw SQL queries, if possible.
Thank you in advance
As your $skills variable appears to be an array, you could use the Eloquent whereIn function.
$workers = Worker::whereHas('skills', function (Builder $query) use ($skills) {
$query->whereIn('name', $skills);
})->get();
Update
The following should get you a collection of Workers that have all the Skills.
$workers = Worker::whereHas('skills');
foreach ($skills as $skill) {
$workers->whereHas('skills', function (Builder $query) use ($skill) {
$query->where('name', $skill);
})->get();
}
$workers->get();
I think you can use foreach for skills to get multiple matching condition
$workers=Worker::whereHas('skills', function (Builder $query) use($skills){
foreach($skills as $value){
$query->where('name',$value);
}
})->get();
You can start with getting all skills and after that you can use whereIn like this
$skills=['carpenter','driver'];
$skills_id = Skill::whereIn(['name',$skills])->pluck('id');
By using pluck the query will return an array of IDs [1,3,...] not model.
$workers = Worker::whereHas('skills', function(Builder $query) use ($skills_id) {
$query->whereIn('id', $skills_id);
})->get();

Easiest way to query data where relationship is in a pivot table?

I've two tables which are in relation by a standard pivot table.
Now, i only want to those articles which belongs to a specific category_id
I found solutions like:
Article::whereHas('categories', function($q) use ($cat_id) { $q->where('category_id', $cat_id); })->get();
Is this the best and easiest way in Laravel 5 with Eloquent?
In general - yes, this is the way, but you could create also scope for this for cleaner usage especially in case you use it multiple times.
You can add scope into Article model like so:
public function scopeHavingCategory($query, $cat_id)
{
return $query->whereHas('categories', function($q) use ($cat_id) {
$q->where('category_id', $cat_id);
});
}
and now you can use it like so:
Article::havingCategory($cat_id)->get();

Laravel ajax search with relations

I wanted to search the records after getting the relations. I tried with Laravel colletions. didnt work tough.
public function search($key){
$products = Product::with('unit', 'category', 'brand')->get();
$allproducts = collect($products);
$result = $allproducts->search($key);
Here I want the search to be done also based on category and brand.
If I cant you collection then how to do it the standard way.
I think using a query is the best approach since you dont need to load all products-categories-etc from the database and then try to find the matching ones.
Example query
Product::where('product_name', 'LIKE', "%$keyword%")
->orWhereHas('category', function ($q) use ($keyword) {
$q->where('category_name', 'LIKE', "%$keyword%");
//everything else you need to check
})->get();

Select all records where a relationship count is zero using Eloquent?

In plain English: I have three tables. subscription_type which has many email_subscriptions which has many emails.
I'm trying to select all email_subscription records that have a particular subscription_type, that also don't have any associated email records that have a status of Held.
The particular bit I am stuck on is only returning email_subscriptions which have zero emails (with an additional where clause stacked in there described above).
Using Eloquent, I've been able to get a bit of the way, but I don't have any idea how to select all the records that have a relationship count of zero:
$subscriptionsWithNoCorrespondingHeldEmail = EmailSubscriptions::whereHas('subscriptionType', function($q) {
$q->where('name', 'New Mission');
})-; // What do I chain here to complete my query?
Additionally, is this even possible with Eloquent or will I need to use Fluent syntax instead?
You can use the has() method to query the relationship existence:
has('emails', '=', 0)
Eg:
$tooLong = EmailSubscriptions::whereHas('subscriptionType', function($q) {
$q->where('name', 'New Mission');
})->has('emails', '=', 0)->get();
Edit
You can do more advanced queries with the whereHas() and whereDoesntHave() methods:
$tooLong = EmailSubscriptions::whereHas('subscriptionType', function($q) {
$q->where('name', 'New Mission');
})
->whereDoesntHave('emails', function ($query) {
$query->where('status', '=', 'whatever');
})->get();
OK what I have under stand from your Question is you would like to have a All Emails which have
specific subscription_type, Zero(0) association and status = status
If yes so you canuse array in where statement.
Like:
$q->->where(array('status' => 'status','subscription_type'=>'what ever you want));

More than one way to invert an Eloquent query?

Consider the following query:
$tickets = Account::with('tickets')->where('name','LIKE',"%{$search}%")->paginate(10);
This is how I inverted it:
$tickets = Ticket::whereHas('account', function($q) use ($search)
{
$q->where('name', 'LIKE', '%'. $search .'%');
})->paginate(10);
Is there another way to invert the first query?
I was looking for a way to execute the first query and receive only results from the tickets table. As it is now of course I will receive the tickets as a property of each account returned.
Answering your comment: yes, you can fetch tickets from 1st query. This is how to do it, though pagination is run in the context of accounts, so we will skip it and retrieve all the accounts:
// returns Eloquent Collection of Account models
$accounts = Account::with('tickets')->where('name','LIKE',"%{$search}%")->get();
// returns array of Ticket arrays (not models here)
$tickets = $accounts->fetch('tickets')->collapse();
As you can see it's possible but a bit cumbersome. Of course you can always prepare a helper method for this, but still you get arrays instead of models etc
So in fact I suggest using the 2nd solution, but here I'll give you something to make it nice and easy:
// Ticket model
public function scopeHasCategoryNameLike($query, $search)
{
$query->whereHas('categories', function($q) use ($search) {
$q->where('name', 'like', "%{$search}%");
});
}
Then it's as simple as this:
$tickets = Ticket::hasCategoryNameLike($search)->paginate(10);

Categories