Merge 'with' and 'whereHas' in Laravel 5 - php

I have this code in Laravel 5, using Eloquent, which is working perfectly:
$filterTask = function($query) use ($id) {
$query->where('taskid', $id);
};
User::whereHas('submissions', $filterTask)->with(['submissions' => $filterTask])->get();
Basically the goal is to get only those users with their filtered submissions, which has any of them.
However, it seems wasting to run both whereHas and with methods with the same callback function. Is there a way to simplify it?
Thanks.

In terms of performance you can't really optimize anything here (except if you were to move from eloquent relations to joins). With or without whereHas, two queries will be run. One to select all users another one to load the related models. When you add the whereHas condition a subquery is added, but it's still two queries.
However, syntactically you could optimize this a bit by adding a query scope to your model (or even a base model if you want to use this more often):
public function scopeWithAndWhereHas($query, $relation, $constraint){
return $query->whereHas($relation, $constraint)
->with([$relation => $constraint]);
}
Usage:
User::withAndWhereHas('submissions', function($query) use ($id){
$query->where('taskid', $id);
})->get();

The 'macroable' way (Laravel 5.4+)
Add this inside a service provider's boot() method.
\Illuminate\Database\Eloquent\Builder\Eloquent::macro('withAndWhereHas', function($relation, $constraint){
return $this->whereHas($relation, $constraint)->with([$relation => $constraint]);
});

I want to extend the answer from #lukasgeiter using static functions.
public static function withAndWhereHas($relation, $constraint){
return (new static)->whereHas($relation, $constraint)
->with([$relation => $constraint]);
}
Usage is the same
User::withAndWhereHas('submissions', function($query) use ($id){
$query->where('taskid', $id);
})->get();

You can now achieve that In Laravel 9.17
Example:
use App\Models\User;
$users = User::withWhereHas('posts', function ($query) {
$query->where('featured', true);
})->get();
Check out the documentation for more information

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

Using Model Relationship in a Single Query

Consider the following:
$posts = $this->model->newQuery()
->whereIn('user_id', $user->following) // specifically this line
->orWhere('user_id', $user->id)
->get();
The problem with the above is that there are two queries:
Get following: $user->following
Get posts: Above
This would be much more efficient with the use of a subquery, however, I cannot actually remember the correct way to do it...
I have tried all of the following:
// This was a long-shot...
...->whereIn('user_id', function ($query) use ($user) {
$query->raw($user->following()->toSql());
});
// This works but pretty sure it can be done better with eloquent...
...->whereIn('user_id', function ($query) use ($user) {
$query->select('follow_id')
->from('user_follows')
->where('user_id', $user->id);
});
Is there a way that this can be achieved by using the previously defined relationship $user->following() instead of manually defining the relationship query like the last example above?
Reference
The following relationship is defined as follows:
/**
* Get the users that the user follows.
*/
public function following()
{
return $this->belongsToMany('SomeApp\User\Models\User', 'user_follows', 'user_id', 'follow_id')
->withTimestamps();
}
Use this:
->whereIn('user_id', $user->following()->getQuery()->select('id'))

Laravel Eloquent, multiple schema query

I have the below query (simplified):
$q = ModelOne::with('relation_one', 'relation_two')
->whereHas('relation_three', function ($q) {
$q->where('object', 'Obj1');
})
->whereHas('relation_four', function ($q) {
$q->where('object', 'Obj2');
})
->get();`
It loads the relation_one and relation_two relationships fine, I also need to load another relationship per row, either relation_three or relation_four depending on the value of ModelOne->object.
The issue I am having is that ModelOne is from schema1 and the tables used in relation_three & relation_four are from schema2.
Both models are set up correct with their individual protected $connection and protected $table variables.
The error I am recieving is that the tables for relationship_three or relationship_four does not exist as the sub-query is checking the wrong schema.
Can anyone suggest how to fix this? Have had a look through the docs but couldn't find a solution.
Maybe not the most elegant solution but got this working by calling relationships and joining as follows:
$q = ModelOne::with('relation_one', 'relation_two')
->with(['relation_three' => function ($q) {
$q->leftJoin(
'schema1.model_one',
'table_three.id',
'=',
'model_one.object_id'
)
->where('object', 'Obj1');
}])
->with(['relation_four' => function ($q) {
$q->leftJoin(
'schema1.model_one',
'table_four.id',
'=',
'model_one.object_id'
)
->where('object', 'Obj2');
}])
->get();`
If anyone can suggest some improvements or a more efficient way to do this please let me know
I would suggest separating the different databases relations to different fields, at least. This way you can then load both (as suggested in comments) and differentiate the logic within controller/model code.
Also, I guess you'll need to define the connection name on the Model level, if not done yet:
class Model_Two_Relation {
protected $connection = 'your-database-name-from-config';
}
You also might want to specify the connection within the relation join condition:
$q = ModelOne::with('relation_one', 'relation_two')
->whereHas('relation_three', function ($q) {
$q->from('resources.one')->where('object', 'Obj1');
})
->whereHas('relation_four', function ($q) {
$q->from('resources.two')->where('object', 'Obj2');
})
->get();
Links: http://fideloper.com/laravel-multiple-database-connections

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 - is there a way to combine whereHas and with

I'm currently facing a small problem. I want to return a model only if a relation with certain conditions exists. That's working fine with the whereHas()-method.
$m = Model
::whereHas(
'programs',
function($q) {
$q->active();
}
);
However, calling the relation as a property like this will give me all (not filtered results).
$m->programs;
So basically what I'm doing now is this:
$m = Model
::whereHas(
'programs',
function($q) {
$q->active();
}
)
->with(array('programs' => function($q) {
$q->active();
}))
;
That's working fine but I feel very bad about doing the same thing again. That can't be the right way. How can I achieve this without kind of duplicating the code?
If a concept of an "active program" is important in your application, consider creating a separate relation just for active programs (in this case I'm presuming you have a HasMany relation):
class Model
{
public function activePrograms()
{
return $this->hasMany(Program::class)->active();
}
}
Then you can simplify your query to:
Model::with('activePrograms')->has('activePrograms')->get();
In Laravel 9.17 withWhereHas
Example:
$a= Model::withWhereHas('programs', function ($query) {
$query->active();
})->get();
Check out the documentation for more information.

Categories