Replace Laravel query callback with function - laravel

I have the following base query I use for retrieving collections with their relations, scopes, sorting, filtering,... This base query is used on almost all my models by extending the class this base query is in.
return $this->runSortQuery(
$query->when($request->has('filter'),
function($query) use ($request) {
return $query->search($request->filter);
}
)->when($request->has('with'),
function($query) use ($request) {
return $query->with(
explode(',', $request->with)
);
}
)->when($request->has('scopes'),
function($query) use ($request) {
return $query->scopes(
json_decode($request->scopes, true)
);
}
) /* and so on... */, $request
)->paginate((isset($request->paginate)) ? $request->paginate : 15);
Is it possible to replace the callback in every when with a custom function call? The reason I want this is because this base function is getting really long and I would like the method in the callback in it's own function, for readability and to keep this maintainable.
I tried this, but this obviously does not work.
$query->when($request->has('filter'), $this->filter($query, $request->filter))
->when($request/* and so on... */);
Can this be done in another way or what would be a good pattern or method to do this?

Yes, you can use the callables like so:
$query->when($request->has('filter'), [$this, 'someMethodNameInClass'])
->...
Check out this SO thread to learn more

Related

Laravel, eloquent, query: problem with retriving right data from DB

I'm trying to get the right data from the database, I'm retriving a model with a media relation via eloquent, but I want to return a photo that contains the 'main' tag stored in JSON, if this tag is missing, then I would like to return the first photo assigned to this model.
how i assign tags to media
I had 3 ideas:
Use orWhere() method, but i want more likely 'xor' than 'or'
$models = Model::with(['media' => function ($query) {
$query->whereJsonContains('custom_properties->tags', 'main')->orWhere();
}]);
return $models->paginate(self::PER_PAGE);
Raw SQL, but i don't really know how to do this i tried something with JSON_EXTRACT and IF/ELSE statement, but it was to hard for me and it was a disaster
Last idea was to make 2 queries and just add media from second query if there is no tag 'main'
$models = Model::with(['media' => function ($query) {
$query->whereJsonContains('custom_properties->tags', 'main');
}]);
$models_all_media = Model:: with(['media']);
return $models->paginate(self::PER_PAGE);
but i tried something like
for($i=0; $i<count($models); $i++) {
$models->media = $models_all_media
}
but i can't do this without get() method, beacuse i don't know how to change this to LengthAwarePaginator class after using get()
try using whereHas https://laravel.com/docs/9.x/eloquent-relationships
Model::with('media')
->whereHas('media',fn($media)=>$media->whereJsonContains('custom_properties->tags', 'main'))
->paginate(self::PER_PAGE);
as per your comment you can use
$models = Model::with(['media' => function ($query) {
$query->whereJsonContains('custom_properties->tags', 'main');
}])
->leftJoin('media', function ($join) {
$join->on('models.id', '=', 'media.model_id')
->whereNull('media.custom_properties->tags->main');
})
->groupBy('models.id')
->paginate(self::PER_PAGE);
return $models;

How to improve multiple eloquent conditions?

Here is a default eloquent condition:
if($request->has("parent")) => {
Specializatio::where('id', $request->id)
->when($request->parentid, function($query) use ($request){
return $query->where('parent_id', $request->parent_id);
})
}
It grows up so fast into long list of conditions .when based request object. Is there any flexibale mechanism to manage it, maybe devide in another class, helper?
Maybe query scopes and short hand functions will make your code a bit cleaner.
class Specialization extends Model
{
public function scopeParent($query, $parentId)
{
return $query->where('parent_id', $parentId);
}
}
Specialization::whereKey($request->id)
->when($request->filled('parent_id'), fn ($query) => $query->parent($request->parent_id));

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

Return keyword in query callback

Is it necessary to write return keyword while using whereHas in laravel.
For eg. return $query->where('status', 1);
$posts = App\Post::whereHas('comments', function ($query) {
$query->whereHas('content', function ($query){
$query->where('status', 1);
});
})->get();
Do we need to write return in every query callback?
No, you're modifying the query builder instance passed to your closure by calling $query->where('status', 1);. Since objects are passed by reference and where() mutates this instance, there's no need to return anything.
There is no need. Also you can use whereHas with nested relationship.
Example:
$posts = App\Post::whereHas('comments.content', function ($query) {
$query->where('status', 1);
})->get();
No. Because you have the same object (and modify it) inside every function. No need to return.

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