Where to write search logic in laravel - laravel

I am writing a search script and want to return results based on the user's query.
Following is my route :-
Route::get('/search/{city}/{searchquery}', 'SearchController#search');
Controller
public function search($city, $query){
strtolower($query);
$commonWords = array('a','able','about','above','abroad'.....);
$cleanQuery = preg_replace('/\b('.implode('|',$commonWords).')\b/','',$query);
$cleanQuery = $s = preg_replace('/[^a-z0-9]+/i', ' ', $cleanQuery);
$queryarray = explode(' ',$cleanQuery);
$queryarray = array_filter( $queryarray );
$queryarray = array_slice( $queryarray, 0 );
//code to match each query word with MySQL fields such as title, description
return $result;
}
I believe, all this logic and code should not be written in the controller. What can I use to write logic and use controller to only return result

The simplest way would be to use models for that. Just write a method that gets keywords and parses it into sql query. You could use scope methods to allow for more flexibility.
public function scopeSearch($q, $keywords)
{
// ... $keywords processing here
$keywordsArray = explode(',', $keywords);
return $query->whereIn('keyword', $keywordsArray);
// OR maybe something like this?
return $query->where('keywords', 'LIKE', '%'. $keywords .'%');
}
You could use this method in a controller like that:
$city = new City();
$results = $city->search($keywords)->where(/* additional conditions could be added here */)->get();
Even if you decide not to use scope methods, I would still advise to return query builder objects instead of collections as to allow for better flexibility, such as implementing pagination or additional manipulation in controllers where needed.
You could also use Repositories if you are not afraid to add more complexify to gain more flexibility. More on that here: https://laracasts.com/lessons/repositories-simplified

Related

Using WhereRaw on collection ignores relation

I made a route to search a particular collection - Customers.
Customer Model
public function location() {
return $this->belongsTo('App\Location');
}
Location Model
public function customers() {
return $this->hasMany('App\Customer');
}
On the index page, I'm simply showing the customers with the data from $location->customers()
$location comes from the model route binding.
This is my search controller:
if ($request->input('search') != null) {
$customers = $location->customers();
$search = strtoupper($request->input('search'));
$searchQuery = 'UPPER(email) LIKE (?) OR CONCAT(UPPER(first_name), " ", UPPER(last_name)) LIKE (?)';
$customers = $location->customers()->whereRaw($searchQuery, ["%{$search}%", "%{$search}%"]);
$customers = $customers->paginate();
}
return response()->json(['results' => $customers], 200);
When a search is executed, I get 10 times as many results in some cases because it's grabbing all customers rather than the specific location relationship.
How can I make whereRaw use the relation?
It is because the OR condition messes up with the query. To avoid that wrap those two query parts with brackets ().
$searchQuery = '( UPPER(email) LIKE (?) OR CONCAT(UPPER(first_name), " ", UPPER(last_name)) LIKE (?) )';
Eloquent builder approach:
$customers = $location->customers()->where(function($query) use ($search) {
$query->whereRaw('UPPER(email) LIKE (?)', ["%{$search}%"]);
$query->orWhereRaw('CONCAT(UPPER(first_name), " ", UPPER(last_name)) LIKE (?)', ["%{$search}%"]);
});
Explanation
Example logical expression:
firstCondition && secondCondition || thirdCondition
In above example expression thirdCondition does not even care about firstCondition or secondCondition because of the ||.
So if you want to check this thirdCondition with secondCondition, then you have to explicitly wrap this two conditions with brackets ().
firstCondition && ( secondCondition || thirdCondition )
This same logic applies for mysql queries too.

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

Replace Laravel query callback with function

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

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.

filtering a paginated eloquent collection

I am trying to filter a paginated eloquent collection, but whenever I use any of the collection methods, I lose the pagination.
$models = User::orderBy('first_name','asc')->paginate(20);
$models = $models->each(function($model) use ($filters) {
if(!is_null($filters['type'])) {
if($model->type == $filters['type'])
return $model;
}
if(!is_null($filters['state_id'])) {
if($model->profile->state_id == $filters['state_id'])
return $model;
}
if(!is_null($filters['city_id'])) {
if($model->profile->city_id == $filters['city_id'])
return $model;
}
});
return $models;
I am working with Laravel 4.2, is there any way to persist the pagination?
An answer to the titular question, which is possible in Laravel 5.2+:
How to filter the underlying collection of a Paginator without losing the Paginator object?
You can eject, modify and inject the collection as follows:
$someFilter = 5;
$collection = $paginator->getCollection();
$filteredCollection = $collection->filter(function($model) use ($someFilter) {
return $model->id == $someFilter;
});
$paginator->setCollection($filteredCollection);
The Paginator is built on an underlying collection, but indeed when you use any of the inherited Collection methods they return the underlying collection and not the full Paginator object: collection methods return collections for chaining together collection calls.
Expanding on mininoz's answer with your specific case:
//Start with creating your object, which will be used to query the database
$queryUser = User::query();
//Add sorting
$queryUser->orderBy('first_name','asc');
//Add Conditions
if(!is_null($filters['type'])) {
$queryUser->where('type','=',$filters['type']);
}
if(!is_null($filters['state_id'])) {
$queryUser->whereHas('profile',function($q) use ($filters){
return $q->where('state_id','=',$filters['state_id']);
});
}
if(!is_null($filters['city_id'])) {
$queryUser->whereHas('profile',function($q) use ($filters){
return $q->where('city_id','=',$filters['city_id']);
});
}
//Fetch list of results
$result = $queryUser->paginate(20);
By applying the proper conditions to your SQL query, you are limiting the amount of information that comes back to your PHP script, and hence speeding up the process.
Source: http://laravel.com/docs/4.2/eloquent#querying-relations
I have a scenario that requires that I filter on the collection, I cannot rely on the Query Builder.
My solution was to instantiate my own Paginator instance:
$records = Model::where(...)->get()->filter(...);
$page = Paginator::resolveCurrentPage() ?: 1;
$perPage = 30;
$records = new LengthAwarePaginator(
$records->forPage($page, $perPage), $records->count(), $perPage, $page, ['path' => Paginator::resolveCurrentPath()]
);
return view('index', compact('records'));
Then in my blade template:
{{ $records->links() }}
paginate() is function of Builder. If you already have Collection object then it does not have the paginate() function thus you cannot have it back easily.
One way to resolve is to have different builder where you build query so you do not need to filter it later. Eloquent query builder is quite powerful, maybe you can pull it off.
Other option is to build your own custom paginator yourself.
You can do some query on your model before do paginate.
I would like to give you some idea. I will get all users by type, sort them and do paginate at the end. The code will look like this.
$users = User::where('type', $filters['type'])->orderBy('first_name','asc')->paginate(20);
source: http://laravel.com/docs/4.2/pagination#usage
This was suitable for me;
$users = User::where('type', $filters['type'])->orderBy('first_name','asc')->paginate(20);
if($users->count() < 1){
return redirec($users->url(1));
}
A workaround when mixing search parameters and pagination, since default pagination won't keep the search parameters, using GET:
$urlSinPaginado = url()->full();
$pos=strrpos(url()->full(), 'page=');
if ($pos) {
$urlSinPaginado = substr(url()->full(), 0, $pos-1);
}
[...]
->paginate(5)
->withPath($urlSinPaginado);
sample generated pagination link: http://myhost/context/list?filtro_1=5&filtro_2=&filtro_3=&filtro_4=&filtro_5=&filtro_6&page=8
You can simply use this format in views
{!! str_replace('/?', '?', $data->appends(Input::except('page'))->render()) !!}
For newer versions of Laravel, you can use this:
$data->paginate(15)->withQueryString();

Resources