Problems with eagerly loading constraints in Laravel 4 - a workaround? - laravel

I have 2 tables, Places and Users. Places can have many Users.
// in Place.php
public function users()
{
return $this->hasMany('User');
}
//in User.php
public function place()
{
return $this->belongsTo('Place');
}
and am trying to get only the Places that have at least one active and enabled User associated through a static method Place::locationsWithPeople().
public static function locationsWithPeople()
{
return Place::with(array('users' => function($query)
{
$query->where('enabled', '=', 1)->where('active', '=', 1);
}))->get();
}
This yields an HTTP 500. Removing the two wheres does not help.
This works, but of course it does not contain the two wheres:
return Place::has('users')->get();
Anyone can help? It seems totally analogous to the example in Laravel's documentation.

This works for me.
$place = new Place;
$array = $place->with(array('users' => function($query)
{
$query->where('enabled', 1)->where('active', 1);
}))->get();
var_dump($array->toArray());
Maybe because you are trying to use non-static methods statically?

It seems that the problem was caused by excessive resource consumption, leading the page to HTTP500 over resource exhaustion. Fixed by rewriting it through join statements.

Can you try this?
return Place::with(array('users' => function($query)
{
$query->where('enabled', '=', 1)->where('active', '=', 1);
}))->hasWith('users')->get();
Method hasWithis a scope that written by me. You can get scopeHasWith() from http://paste.laravel.com/13jb. This will work same as has() method but it will call eagerload closure before attach WHERE to query builder.
I think this can resolve your problem that used resource too much and you can WHERE them from eagerload closure. I had same problem as you that I want to query with relation but it send all records that match WHEREIN too and cause memory problem.

Related

Best way to query model relationship in Eloquent

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

Laravel Route with Multiple Parameters - How to get correct database result

I am a couple of weeks in to learning Laravel and have come across a problem which I can not find the answer to by myself, or online.
I am building a directory website with urls structured like:
directory.co.uk/parks
directory.co.uk/parks/{county-name}
directory.co.uk/parks/{county-name}/{park-name}
As {park-name} is not unique, I am struggling to return the page for an individual park. The controller needs to look up the county.id using the county.slug and then the park.id using the park.county_id and the park.slug.
I have routes in the web.php file such as:
Route::get('/parks','ParksController#index')->name('parks');
Route::get('/parks/{county}/{park}','ParksController#show')->name('park');
I have Parks and Counties models and (belongsTo and hasMany relationships set up between the two).
I have this is in both models:
public function getRouteKeyName()
{
return 'seo_url';
}
Then in my ParksController, I am at a loss. I currently have:
public function show(Counties $county, Parks $park)
{
//return $park;
//dd($park);
return view('parks.park', ['park'=>$park]);
}
I have also tried the non-Eloquent way:
public function show($county_slug,$park_slug)
{
$county = DB::table('counties')->where('seo_url',$county_slug)->get();
$county_id = $county->pluck('id');
$park = DB::table('parks')->where('county_id', $county_id)->where('seo_url', $park_slug)->get();
//dd($county_id);
//return $park;
return view('parks.park', ['park'=>$park]);
}
This returns a 404 error. Any help would be much appreciated. (I have done a lot of reading on Route model binding, but can not see any examples like mine.)
Laravel has an undocumented feature in its explicit model binding, where the callback can be given the current Route the binding is for. This can allow you to access the other parameters and use them to add conditionals.
Router::bind('park', static function ($value, Route $route) {
$query = Parks::where('seo_url', '=', $value);
if ($route->hasParameter('county')) {
$county = $route->parameter('county');
$query->where('county_id', '=', $county instanceof Counties ? $county->id : $county);
}
return $query->first() ?? abort(404);
});

Refactor whereHas to use via global scopes in Laravel

I've 2 models, CoreChallenge and Challenge. Below are relations between table.
I want to fetch Challenges which has core_challenges active. I tried to do putting global scope in CoreChallenge model, but when I'm getting null in relationship when Corechallenge is inactive.
I've done it this way
$challenges = Challenge::with('core_challenge')->whereHas('core_challenge', function($q){
$q->where('status', '=', 'active');
})->get();
I want to do it using global scopes
Global scope on CoreChallenge gives me null, but I want that it's parent (Challenge) should not load even, like in whereHas. Is there any way?
I have stumbled to the same approach, but when the table got bigger (core_challenges_table in your scenario), whereHas ended up being very slow (around 1min response time).
So I used a solution like this:
$ids = CoreChallenge::where('status', 'active')->pluck('id');
$challenges = Challenge::with('core_challenges')
->whereIn('core_challenge_id', $ids)
->get();
With this approach, my query reduced to 600~ms from 1min.
Which can be translated to Model scopes
class Challenge {
public function scopeActive($query) {
$activeIds = CoreChallenge::where('status', 'active')->pluck('id');
return $query->whereIn('core_challenge_id', $ids);
}
}
Challenge::with('core_challenges')->active()->get();

Laravel Eloquent Sort By Relationship Column

I have several relationships, one specifically that I would like to use for ordering a list, but I can't seem to find the right way to do it.
Below are my relationships:
public function date(){
return $this->hasOne(agent_billings_dates::class,'id','dateID');
}
public function carrier(){
return $this->hasOne(customer::class,'id','carrierID');
}
As well as two attributes which I have added as appends:
public function getItemCountAttribute(){
return $this->items->count();
}
public function getItemMissingAttribute(){
return $this->itemIssues->count();
}
public function getBatchSumAttribute(){
return $this->items->sum('amount');
These show up all fine when I have the following in my function:
$batches = agent_billings_batches::with(['date','carrier','carrier.carrierDetails'])
->where('status',$request->status)
->get();
But the attributes and the with's fall off when I do this (however the date is sorted appropriately):
$batches = agent_billings_batches::with(['carrier','carrier.carrierDetails'])
->join('agent_billings_dates', 'agent_billings_dates.id', '=', 'agent_billings_batches.dateID')
->orderBy('agent_billings_dates.date','desc')
->where('status',$request->status)
->get();
Am I doing something wrong? I'd appreciate any help anyone could give.
Thanks!
Eloquent does not use Joins when loading relationships. It loads them in a separate query, therefore you cannot order the main result using a relationship at query time, you need to do it after the data is collected:
$batches = agent_billings_batches::with(['date','carrier','carrier.carrierDetails'])
->where('status',$request->status)
->get()
->sortBy(function ($batch) {
return $batch->date->date;
});

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