Best way to query model relationship in Eloquent - laravel

I am working within a controller in a Laravel application. I am returning a table to the view. The table is based on my PlanSubmission model. I am receiving parameters through a GET request and using those parameters to return a filtered set of rows to my view.
The first part of my controller looks like this and is working fine:
public function index()
{
//Used for filter. The request is received in the URL
if (request()->has('status')) {
$plans = PlanSubmission::where('status', request('status'))->paginate(25)->appends('status', request('status'));
}
elseif (request('employer_name')) {
$plans = PlanSubmission::where('employer_name', request('employer_name'))->paginate(25)->appends('employer_name', request('employer_name'));
}
I have run into a problem because now I need to use a model relationship in the controller. I am receiving 'advisor_name' from the request. The 'advisor_id" column is the foreign key on the PlanSubmission model. The 'advisor_name' column exists in the Advisor model. I have a function on my PlanSubmission model that looks like this:
public function advisor()
{
return $this->belongsTo(Advisor::class);
}
Initially, I thought there was a way I could do this easily with something like:
$plans = PlanSubmission::where(advisor->name, request('advisor_name'))->paginate(25)->appends('advisor_name', request('advisor_name'));
Of course, this will not work because I cannot enter a relationship into the first parameter in the Where Clause.
I do not know where to go from here. My other thought is to return all the advisors first from the Advisor model like this:
$advisors = Advisor::where('name', request('advisor_name'));
Then, I imagine I would have to somehow loop through that and get the id (primary key) for each of the objects in $advisors and somehow get that into the PlanSubmission where clause. I'm totally lost.

Like Victor mentions in his answer you can use whereHas like so:
PlanSubmission::whereHas('advisor', function ($query) {
$query->where('name', request('advisor_name'));
});
You didn't asked this directly, but I noticed that you use conditionals to make different queries. Eloquent provides a few way to make this a bit nicer to deal with.
The first which is kind of obvious is that that whatever method you call a builder (query) is returned that you can just add on to. It could be there were some common restrictions in your two cases:
public function index()
{
$query = PlanSubmission::where('something', 42);
if (request()->has('status')) {
$query = $query->where('status', request('status'));
} elseif (..) {
...
}
return $query->paginate(25);
}
Another way to do conditional queries in Laravel is using when. E.g. for status:
$query = $query->when(request->has('status'), function ($query) {
// note that you don't have to return the query
$query->where('status', request('status'));
});
// or PlanSubmission::>when(..)
In your example you cannot both filter by status AND advisor_name, but lets assume that would be okay, then you can combine everything like so:
public function index()
{
return PlanSubmission::query()
//->where('something', 42)
->when(request->has('status'), function ($query) {
$query->where('status', request('status'));
})
->when(request->has('advisor_name'), function ($query) {
$query->whereHas('advisor', function ($query) {
$query->where('name', request('advisor_name'));
});
})->paginate(25);
}
This approach may seem verbose for simple queries and then it is fine to use if conditions, but for complex queries when can be useful. Also the idea of "building up a query" also works nice in those situation. You can pass the query builder around and continuously build it up.

You can use whereHas for that
docs

Related

Laravel Model request with several joins

While I am able to make simple requests with Model, I can't say the same for more complicated ones.
I know I don't necessarily have to use Model and can use DB facade but still, I want to know how it's supposed to be done.
Here's a request I made using DB :
DB::table('relationships')
->Join('users','users.id','=','relationships.user_id')
->Join('roles','roles.id','=','relationships.role_id')
->Join('bundles','bundles.id','=','relationships.related_id')
->Join('pools','bundles.id','=','pools.bundle_id')
->whereIn('pools.name',$pools)
->whereIn('roles.name',$roles)
->select('users.first_name','users.last_name','users.mail_address','roles.name AS role_name','bundles.name AS bundle_name', 'pools.name AS pool_name')
->get();
On a first attempt, I tried this:
User::whereHas('relationships', function($req) use($roles) {
$req->whereHas('bundle', function($req){
$req->whereIn('name', $pools);
});
$req->whereHas('role', function ($req){
$req->whereIn('name', $roles);
});
})
->with('relationships', 'relationships.role:id,name', 'relationships.bundle:id,name')
->get();
}
Problem is, using "with" just select everything unconditionally, ignoring previous tests you made earlier (whereHas, whereIn).
So I'd have to again filter on each table in the with statement.
Then I ended up doing this:
$pools = request()->input('pools.*.name');
return $prepReq = User::whereHas('relationships', function($req) use($pools, $roles) {
$req->whereHas('bundle', function($req) use ($pools){
$req->whereHas('pools', function($req) use ($pools){
$req->whereIn('name', $pools);
});
});
$req->whereHas('role', function ($req){
$req->whereIn('name', $roles);
});
})
->with(['relationships' => function ($query) use($pools, $roles){
$query->whereHas('role', function ($query){
$query->whereIn('name', $roles)
->select('id','name');
})->select('id','name');
}])
->get(['id', 'first_name', 'last_name', 'mail_address']);
Then I got lost into this and gave up.
Another thing that made me sweat is that when you go nested using "with", you can select columns only on the last table.
For example: "relations.bundle.pools" => I can select columns on pools but not on relationships or bundles, does that mean i have to imbricate with statements for each table ?
As you can see, I am a bit clueless on how things are supposed to be done
I would like any advice or help regarding this matter
Thanks in advance for your time
When using Laravel, you should be setting up the eloquent relationships for each model
Based on your select statement from above, and assuming you want to get the user, bundle, role, and pool, then I would do the following, may be off depending on how your actual DB and models are set up
// Relationship.php
public function user()
{
return $this->belongsTo(User::class);
}
public function role()
{
return $this->belongsTo(Role::class);
}
public function bundle()
{
return $this->belongsTo(Bundle::class, 'id', 'related_id');
}
// Bundle.php
public function relationship()
{
return $this->hasOne(Relationship::class, 'related_id');
}
// Role.php
public function relationship()
{
return $this->hasOne(Relationship::class);
}
// Pool.php
public function bundle()
{
return $this->belongsTo(Bundle::class);
}
Then you could do something like
Pool::with('bundle.relationship.user')->whereIn('name', $pools);
Role::with('relationship.user')->whereIn('name', $roles);

Laravel 5, get only relationship for eloquent model

I have a user and article models in my app. The relation between them is straightforward:
//article.php
use Taggable;
public function user()
{
return $this->belongsTo('App\User');
}
Article uses the Taggable lib which provides me with a variety of methods like Article::withAnyTags (returns all articles tagged with 'xyz' string). Now I'd like to get all users who posted an article/s tagged as 'xyz'. I know how to do this in two lines but something tells me that this is not right. Ideally I'd like to do sth like:
$users = Article::withAnyTags('xyz')->with('user')->select('user'); --pseudocode
Is it possible to do sth like this in eloquent?
Side note: I knot that I could do this with DB::table but this is not an option for me. Please note that there is no "->get()" at the end on my pseudocode. It's so, because I'm paginating the users' results set with lib which works only with eloquent queries.
You could use whereHas():
User::whereHas('articles', function ($query) {
$query->withAnyTags('xyz');
})->paginate();
Or if you need to pass a variable of the tags to the closure you could do:
$tag = 'xyz';
User::whereHas('articles', function ($query) use($tag) {
$query->withAnyTags($tag);
})->paginate();

How to sort a query by the results of a model's method in Laravel

I have a model that has a "hasMany" relationship with a second model which records "votes" for the former. I then have a method which can be called upon to simply count the number of votes, as follows:
class Car extends Model
{
public function votes()
{
return $this->hasMany(Vote::class);
}
public function score()
{
return $this->votes->count();
}
}
In my controller, I want to run a query that returns all() but sorts by score()... something along the lines of:
$cars = Car::all()->sortBy('score');
But that doesn't work obviously... I feel like I'm on the right track, but don't quite know how to make this work. Would appreciate some help. Thanks.
Try this solution:
$cars = Car::with('votes')->get()->sortBy(function($car)
{
return $car->votes->count();
});
Hope it help you :)
the code should be like this :
$products = Shop\Product::join('shop_products_options as po', 'po.product_id', '=', 'products.id')
->orderBy('po.pinned', 'desc')
->select('products.*') // just to avoid fetching anything from joined table
->with('options') // if you need options data anyway
->paginate(5);

How to add condition in connection tables?

I have two tables: Users and Images.
So, a user can have some images.
For this relationship I have additional function in model User:
public function images()
{
return $this->hasMany('App\Images', 'idElement', 'id');
}
And in controller I have:
$users = Users::where('id', $id)->with("images")->get();
How can I add additional condition in controller for images table that will be "where images.type = 1"?
Now this tables are connected only by primary keys, but I need to set a new condition yet.
You can filter your images with callback function, try this:
$users = Users::where('id', $id)->with(["images" => function ($query){
$query->where('type', 1);
}])->get();
For something like this, where you want to scope down a subset of images based on their type, you can add another method called something like public function scopedImages() and define it as such:
public function scopedImages() {
return $this->hasMany('App\Images', 'idElement', 'id')->where("images.type", "=", 1);
}
In your controller, you would access this function the same as you would the images() function on User:
$users = Users::where('id', $id)->with(["scopedImages"])->get();
Keep the function images() as well, so if you need to find all images attached to a User, but adding additional functions like this gives you flexibility on what you want to return and when.

Laravel 5 Eager Loading with parameters

I'm working on a project with a bit of a complex model that has joins in its relations and also requires a parameter. It all works pretty well, except for when I need to eager load the relationship, as I couldn't figure out if there is a way to pass a parameter/variable to it.
The Controller
$template = Template::find($request->input('id'));
$this->output = $template->zones()->with('widgets_with_selected')->get();
The Model
public function widgets_with_selected($banner_id)
{
return $this->belongsToMany('App\Models\Widget', 'zone_has_widgets')
->leftJoin('banner_has_widgets', function($join) use($banner_id) {
$join->on('widgets.id', '=', 'banner_has_widgets.widget_id')
->where('banner_has_widgets.banner_id', '=', $banner_id);
})
->select('widgets.*', 'banner_has_widgets.banner_id');
}
This is returning a Missing argument error as the variable is not being passed.
I have resolved the issue by moving the logic to the controller, but I want to know if there is a way to keep the relationship in the model and just call it with a parameter.
Looking at the laravel code I dont think this is possible as you'd like to do it. You simply cant pass parameters to a with() call.
A possible workaround is to have an attribute on your model for $banner_id.
$template = Template::find($request->input('id'));
$template->banner_id = 1;
$this->output = $template->zones()->with('widgets_with_selected')->get();
Then change your relationship
public function widgets_with_selected()
{
return $this>belongsToMany('App\Models\Widget','zone_has_widgets')
->leftJoin('banner_has_widgets', function($join) use($this->banner_id) {
$join->on('widgets.id', '=', 'banner_has_widgets.widget_id')
->where('banner_has_widgets.banner_id', '=', $banner_id);
})
->select('widgets.*', 'banner_has_widgets.banner_id');
}
You could perhaps alter it a bit by passing the banner_id through a method. Sortof like this in your model:
public function setBanner($id) {
$this->banner_id = $id;
return $this;
}
Then you can do:
$template->setBanner($banner_id)->zones()->with('widgets_with_selected')->get();
Not sure if this works, and it's not really a clean solution but a hack.

Resources