Way to structure code for "Search Engine" - laravel

I'm currently putting together some code that allows for a user to search an activities table in multiple ways (i.e. if title checkbox is selected) I feel like my code looks a little messy so I wanted to come on stack overflow and ask everyone what would be the best way to make this code more elegant? I'm just looking for ways to improve, have more readable code, and have a better structure for it.
if (request('name')){
$name = request('name');
$user = User::where('name', $name)->firstOrFail();
if (request('title') == 1) {
$activities = Activity::with('activity')->where('user_id', $user->id)->whereHas('thread', function ($query) use ($search, $user) {
$query->where('threads.user_id', '=', $user->id)
->where('threads.title', 'LIKE', '%' . $search . '%');
})->get();
dd($activities);
} else {
$activities = Activity::with('activity')->where('user_id', $user->id)->whereHas('thread', function ($query) use ($search, $user) {
$query->where('threads.user_id', '=', $user->id)
->where('threads.title', 'LIKE', '%' . $search . '%')
->orWhere('threads.body', 'LIKE', '%' . $search . '%');
})->orWhereHas('reply', function ($query) use ($search, $user) {
$query->where('replies.user_id', '=', $user->id)
->where('replies.body', 'LIKE', '%' . $search . '%');
})->get();
dd($activities);
}
} else {
if (request('title') == 1) {
$activities = Activity::with('activity')->where('user_id', $user->id)->whereHas('thread', function ($query) use ($search, $user) {
$query->where('threads.title', 'LIKE', '%' . $search . '%');
})->get();
dd($activities);
} else {
$activities = Activity::with('activity')->whereHas('thread', function ($query) use ($search) {
$query->where('threads.body', 'LIKE', '%' . $search . '%')
->orWhere('threads.title', 'LIKE', '%' . $search . '%');
})->orWhereHas('reply', function ($query) use ($search) {
$query->where('replies.body', 'LIKE', '%' . $search . '%');
})->get();
}
}
Thank you!

You could use the query builder's when and unless methods as well as define some query scopes in your models so your end result could look something like this
$user = request('name') ? User::where('name', $name)->firstOrFail() : null;
$title = request('title') == 1;
$activities = Activity::with('activity')->search($search, $user, $title)->get();
# Activity model
public function scopeSearch(Builder $query, ?$search = null, ?User $user = null, bool $title = false)
{
if (!$search)
return $query;
else
return $query->when($user, fn($q) => $q->where('user_id', $user->id))
->whereHas('thread', fn($thread) => $thread->search($search, $user))
->unless($title, fn($q) => $q->orWhereHas('reply', fn($reply) => $reply->search($search, $user)));
}
# Thread model
public function scopeSearch(Builder $query, ?string $search = null, ?User $user = null)
{
if (!$search)
return $query;
else
return $query->when($user, fn($q) => $q->where('threads.user_id', $user->id))
->where(fn($q) => $q->where('threads.title', 'LIKE', "%$search%")
->orWhere('threads.body', 'LIKE', "%$search%"));
}
# Reply model
public function scopeSearch(Builder $query, ?string $search = null, ?User $user = null)
{
if (!$search)
return $query;
else
return $query->when($user, fn($q) => $q->where('replies.user_id', $user->id))
->where('replies.body', 'LIKE', "%$search%");
}
Scopes are basically reusable queries. You can define them at a global level (for all models) or at a local level (this is what I've done here).
Local Query Scopes
With them, I've moved nearly all the query related logic to the models but if you prefer, you could still write it all in the controller.
Using the scopes I defined,
$activities = Activity::with('activity')
// call Activity Model's search scope
->search($search, $user, $title)
->get();
translates to
$activities = Activity::with('activity')
->when($user, fn($q) => $q->where('user_id', $user->id))
// call Thread model's search scope in whereHas('thread', ...) closure
->whereHas('thread', fn($thread) => $thread->search($search, $user))
// call Reply model's search scope in whereHas('reply', ...) closure
->unless($title, fn($q) => $q->orWhereHas('reply', fn($reply) => $reply->search($search, $user)))
->get();
which in turn translates to
$activities = Activity::with('activity')
->when($user, fn($q) => $q->where('user_id', $user->id))
->whereHas('thread', function ($thread) use ($search, $user) {
$thread->when($user, fn($q) => $q->where('threads.user_id', $user->id))
->where(fn($q) => $q->where('threads.title', 'LIKE', "%$search%")
->orWhere('threads.body', 'LIKE', "%$search%"));
})
->unless($title, fn($q) => $q->orWhereHas('reply', function ($reply) use ($search, $user) {
$reply->when($user, fn($q) => $q->where('replies.user_id', $user->id))
->where('replies.body', 'LIKE', "%$search%");
}))
->get();
The $title variable could be inlined at this point. ->unless(request('title') == 1, ...)
Conditional Clauses: when(), unless().
Logical Grouping: where(fn($q) => ...).

Related

Laravel dynamic query

I have a GET form with three filters.
make
Year
country
I need to get all posts from db. But filter the results based on these three filters.
If a country is selected, get posts for that country only or all countries.
if a make is selected, get posts for that make only or all makes
if a year is selected, get posts for that year only or all years
how to write one query that filters all these three options. What I have done is used if and else statements and written different queries for each scenario. That's 9 queries to get one information. Can we make it dynamic and just have one query?
My Example query:
public function search(Request $request)
{
$search=$request->input('search');
if($request->input('country') == "all")
{
$posts = Post::where('status','Published')->orderBy('status_change','DESC')
->where('status','Published')
->where(function($query) use ($search){
$query->where('title','LIKE','%'.$search.'%');
$query->orWhere('model','LIKE','%'.$search.'%');
$query->orWhere('notes','LIKE','%'.$search.'%');
$query->orWhere('description','LIKE','%'.$search.'%');
})
->paginate(25);
}
else
{
$posts = Country::where('country_name', $request->input('country'))->first()->posts()->orderBy('status_change','DESC')
->where('status','Published')
->where(function($query) use ($search){
$query->where('title','LIKE','%'.$search.'%');
$query->orWhere('model','LIKE','%'.$search.'%');
$query->orWhere('notes','LIKE','%'.$search.'%');
$query->orWhere('description','LIKE','%'.$search.'%');
})
->paginate(25);
}
return view('welcome')
->with('published_posts',$posts)
;
}
I think something like this would work:
/**
* #param Request $request
*/
function search(Request $request)
{
$postsQuery = Post::where('status', 'Published');
if ($request->has('country')) {
$country = $request->country;
// assuming relationships are setup correclty
$postsQuery->whereHas('country', function ($query) use ($country) {
$query->where('country_name', 'LIKE', $country);
});
}
if ($request->has('search')) {
$postsQuery->where(function ($query) use ($search) {
$query->where('title', 'LIKE', '%' . $request->search . '%');
$query->orWhere('model', 'LIKE', '%' . $request->search . '%');
$query->orWhere('notes', 'LIKE', '%' . $request->search . '%');
$query->orWhere('description', 'LIKE', '%' . $request->search . '%');
});
}
$postsQuery->orderBy('status_change', 'DESC')->paginate(25);
return view('welcome')->with('published_posts', $result);
}
I used 'when' method.
$make = null;
$year = null;
$country = null;
if($request->filled('make')){
$make = $request->query('make');
}
if($request->filled('year')){
$year = $request->query('year');
}
if($request->filled('country')){
$country = $request->query('country');
}
$posts = DB::table('posts')
->when($make, function($query, $make){
return $query->where("make", "=", $make);
})
->when($year, function($query, $year){
return $query->whereYear("year", "=", $year);
})
->when($country, function($query, $country){
return $query->where('country', "like", $country);
})
->get();
Check out the Laravel Docs:
Check out an article here

Laravel join database table with where clause and like from $request

public function search(Request $request)
{
if ($request->get('search') != '') {
$franchise = DB::table('operators')
->join('franchises', function ($join) {
$join->on('operators.id', '=', 'franchises.operator_id')
->where('case_number', 'like', '%' . $request->get('search') . '%');
})
->get();
return view('franchise-home', compact('franchise'));
} else {
$franchise = DB::table('operators')
->join('franchises', 'operators.id', '=', 'franchises.operator_id')
->get();
return view('franchise-home', compact('franchise'));
}
}
how to make this work with both where clause and get a request from a user? Any help is much appreciated. thanks.
You need to add use() to access your variable, for example :
$search = $request->get('search'); // define it before query
...
->join('franchises', function ($join) use($search) { //use it inside
$join->on('operators.id', '=', 'franchises.operator_id')
->where('case_number', 'like', '%' . $search . '%');
})
Below is the sort code
public function search(Request $request) {
$franchise = DB::table('operators')
->join('franchises', 'operators.id', '=', 'franchises.operator_id')
->when($request->get('search'), function($query) use($request) {
$query->where('case_number', 'like', '%' . $request->get('search');
})
->get();
return view('franchise-home', compact('franchise'));
}

return all the posts where author.username equals $username

I have a posts model that has this relationship to the User:
public function author()
{
return $this->belongsTo(User::class, 'user_id');
}
i am building a search function and I have this code
public function index(Request $request)
{
$search = $request->search;
$filter = $request->filter;
$append = array();
$posts = Post::with('author')->with('categories')->latest();
if($search){
switch ($filter) {
case 'username':
$posts->author->where('username', 'LIKE', '%'. $search . '%');
break;
}
$append += array('search' => $search);
$append += array('filter' => $filter);
}
$posts = $posts->paginate(3);
$posts->appends($append);
return view('core.blog.posts.index', compact('posts'));
}
I get
Undefined property: Illuminate\Database\Eloquent\Builder::$author
How do I add where that looks for author based on his username? I must be able to add this condition in an if case
You want to use whereHas() to searched based on a relation. This will return only posts that have an author with the username.
switch ($filter) {
case 'username':
$posts->whereHas('author', function($q) use($search) {
$q->where('username', 'LIKE', '%'. $search . '%');
});
break;
}
UPDATE
If you need conditional where, you can do this:
$postSelector = Post::with('categories');
if($search){
switch ($filter) {
case 'username':
$postSelector->with(['author' => function($q) use($search) {
$q->where('username', 'LIKE', '%'. $search . '%');
}]);
break;
}
$posts = $postSelector->get();

Is there a way for Eloquent whereHas to only get() the matching results?

I have the following function
public function index(Request $request)
{
$results = Service::whereHas('conditions', function ($query) use ($request){
$query->whereIn('id', $request->conditions);
})
->whereHas('locations', function ($query) use ($request){
$ran = false;
foreach($request->locations as $location)
{
if(!$ran)
{
$query->Where('town', 'like', '%'.$location.'%')
->orWhere('city', 'like', '%'.$location.'%');
}
else
{
$query->orWhere('town', 'like', '%'.$location.'%')
->orWhere('city', 'like', '%'.$location.'%');
}
$ran = true;
}
})
->with('types', 'contacts', 'locations.datetimes')->toSql();
dd($results);
}
which produces
select * from `services` where exists
(select * from `conditions` inner join `condition_service` on `conditions`.`id` = `condition_service`.`condition_id` where `services`.`id` = `condition_service`.`service_id` and `id` in (13, 14) and `conditions`.`deleted_at` is null)
and exists
(select * from `service_locations` inner join `service_location_service` on `service_locations`.`id` = `service_location_service`.`service_location_id` where `services`.`id` = `service_location_service`.`service_id` and
(`town` like 'manchester' or `city` like 'manchester' or `town` like 'crewe' or `city` like 'crewe')
and `service_locations`.`deleted_at` is null) and `services`.`deleted_at` is null
The problem is the whereHas on locations brings back all results, whereas I only want the result where the town/city is a match. Whats the best way to achieve this?
i think you need to group your location query using the callback which wraps the query in () and just process the first location directly remove it from array and iterate over loop. so below code should work in your case.
$results = Service::whereHas('conditions', function ($query) use ($request){
$query->whereIn('id', $request->conditions);
})
->whereHas('locations', function ($query) use ($request){
$locations = $request->locations;
$query->where(function($builder) use($locations){
$builder->where('town', 'like', '%' . $locations[0] . '%')
->orWhere('city', 'like', '%'.$locations[0].'%');
unset($locations[0]); //remove first item as it is processed
foreach($locations as $location) {
$builder->orWhere('town', 'like', '%'.$location.'%')
->orWhere('city', 'like', '%'.$location.'%');
}
});
});
let me know if you find any problem with it. :)
I managed to figure it out late last night.
I moved the with() to the top of the query and made a callback on locations.
public static function newGetSearchResults($request)
{
return Service::with(['types', 'contacts', 'locations.datetimes', 'locations' => function($query) use($request) {
$ran = false;
foreach($request->locations as $location)
{
if(!$ran)
{
$query->Where('town', 'like', '%'.$location.'%')
->orWhere('city', 'like', '%'.$location.'%');
}
else
{
$query->orWhere('town', 'like', '%'.$location.'%')
->orWhere('city', 'like', '%'.$location.'%');
}
$ran = true;
}
}])
->whereHas('conditions', function ($query) use ($request){
$query->whereIn('id', $request->conditions);
})->get();
}

Laravel eloquent search on fields of related model

I have an eloquent models as,
User : users(id, username, password, email, status)
Profile : profiles(id, user_id, first_name, last_name, gender, dob)
In the controller logic, I am eagerly loading the Profile model.
I can do this,
$user = User::with('Profile')->get();
or
$user = User::with('Profile')->where('status', '1')->get();
but how to do something like,
$user = User::with('Profile')->where('status', '1')->where('gender', 'Male')->get();
That's where whereHas comes in handy:
$user = User::with('Profile')->where('status', 1)->whereHas('Profile', function($q){
$q->where('gender', 'Male');
})->get();
Basically it adds the condition that the user needs to have a profile with gender = Male
If you want to search multiple columns in relation model.
$searchText = 'test text';
Product::with('owner')->where(function($query) use ($searchText)
{
$query->where('product_name', 'LIKE', '%' . $searchText . '%');
$columns = ['product_code', 'place_location', 'remark'];
foreach ($columns as $column ) {
$query->orWhere($column, 'LIKE', '%' . $searchText . '%');
}
$query->orWhereHas('owner', function($q) use ($searchText) {
$q->where(function($q) use ($searchText) {
$q->where('name', 'LIKE', '%' . $searchText . '%');
$q->orWhere('company_name', 'LIKE', '%' . $searchText . '%');
});
});
});
Let's say you've multiple relations
and you want to search records based on multiple relational columns value
User::with('associate')
->where('name', 'like', '%' . $input . '%')
->orWhere(function ($query) use ($input) {
$query->whereHas('associate', function ($q) use ($input) {
$q->where('first_name', 'like', '%' . $input . '%');
});
})
->orWhere(function ($query) use ($input) {
$query->with('roles')->whereHas('roles', function ($q) use ($input) {
$q->where('display_name', 'like', '%' . $input . '%');
});
})
->get();
Suppose, Your search input field name is q.
function name(Request $request){
$query = User::select();
if($request->q && $request->q !=''){
// if you search
$keyword = $request->q;
$query = User::with('Profile')->whereHas('Profile', function($q) use
($keyword){
$q->where('gender', 'like', "%{$keyword}%" );
});
}
$query->latest('id')->paginate();
}

Resources