Laravel query builder. How to make the query shorter? - laravel

I need to prepare data for datatables, and i need to count total amount of filtered data of the query, but i cant do it with just one query because of limit so i must do almost the same query but with no limit option for pagination, and it makes the amount of my code x2. Is it possible to make this code shorter and beautiful?
public function data($request)
{
$staff = auth()->user();
$role = Role::find($staff->role_id)->alias;
$isAdmin = $role == 'admin';
$columns = ['id', 'name', 'shop', 'hash', 'status'];
$limit = $request->input('length');
$start = $request->input('start');
$order = $columns[$request->input('order.0.column')];
$directions = $request->input('order.0.dir');
$searchValue = $request->input('search.value');
$filters = explode(',', $request->input('columns.7.search')['value']);
foreach ($filters as $key => $value) {
if ($value !== "") {
array_push($shops, $value);
}
}
if ($searchValue) $filters[] = $searchValue;
$nfcs = NfcMark::offset($start)->select('nfc_marks.*')
->leftJoin('shops as s', 's.id', 'nfc_marks.shop_id')
->when(!$isAdmin, function ($q) {
$shop_ids = $this->shopRepository->shopsIdsByOwner(auth()->user()->id);
return $q->whereIn('shop_id', $shop_ids);
})
->when($searchValue, function ($q) use ($filters) {
foreach ($filters as $filter) {
$q->orWhere('shops.name', 'LIKE', '%' . $filter . '%');
}
})
->limit($limit)
->orderBy($order, $directions)
->get();
$nfcs_count = NfcMark::query()
->leftJoin('shops as s', 's.id', 'nfc_marks.shop_id')
->when(!$isAdmin, function ($q) {
$shop_ids = $this->shopRepository->shopsIdsByOwner(auth()->user()->id);
return $q->whereIn('shop_id', $shop_ids);
})
->when($searchValue, function ($q) use ($filters) {
foreach ($filters as $filter) {
$q->orWhere('shops.name', 'LIKE', '%' . $filter . '%');
}
})->get();
$totalFiltered = count($nfcs_count);
$totalData = $totalFiltered;
$data = array();
if (!empty($nfcs)) {
foreach ($nfcs as $item) {
$nestedData['id'] = $item->id;
$nestedData['name'] = $item->name;
$nestedData['shop'] = $item->shop_id;
$nestedData['hash'] = $item->hash;
$nestedData['status'] = $item->status;
$data[] = $nestedData;
}
}
$json_data = array(
"draw" => intval($request->input('draw')),
"recordsTotal" => intval($totalData),
"recordsFiltered" => intval($totalFiltered),
"data" => $data
);
return json_encode($json_data);
}

You can hold the part of the query that is the same between those as a builder and run both queries from it if you feel you need both these queries:
$query = NfcMark::leftJoin('shops as s', 's.id', 'nfc_marks.shop_id')
->when(!$isAdmin, function ($q) {
$q->whereIn(
'shop_id',
$this->shopRepository->shopsIdsByOwner(auth()->user()->id)
);
})
->when($searchValue, function ($q) use ($filters) {
foreach ($filters as $filter) {
$q->orWhere('shops.name', 'LIKE', '%' . $filter . '%');
}
});
$nfcs_count = $query->get();
$nfcs = $query->select('nfc_marks.*')
->offset($start)
->limit($limit)
->orderBy($order, $directions)
->get();
You can also get the count without returning the rows and just using count() instead of get()

You can use clone to duplicate the query and then run it with different where statements.
$query1 = NfcMark::query()
->leftJoin('shops as s', 's.id', 'nfc_marks.shop_id')
->when(!$isAdmin, function ($q) {
$shop_ids = $this->shopRepository->shopsIdsByOwner(auth()->user()->id);
return $q->whereIn('shop_id', $shop_ids);
})
->when($searchValue, function ($q) use ($filters) {
foreach ($filters as $filter) {
$q->orWhere('shops.name', 'LIKE', '%' . $filter . '%');
}
})
$query2 = clone $query1;
$nfcs_count = $query1->get() //or $query1->count();
$$nfcs = $query2
->limit($limit)
->orderBy($order, $directions)
->get();

Related

Way to structure code for "Search Engine"

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) => ...).

Laravel query groubBy condition

I have a dB query where I would like to groupBy() only when conditions are met without using union because of pagination.
Unfortunately groupBy() seems to only work when called on the entire query outside of the loop.
This was made for dynamic filtering from $filterArr. Depending on the array I need to select from different columns of the table.
When the $key=='pattern' I would need the distinct results from its column.
the query looks something like this
select `col_1`, `col_2`, `col_3`
from `mytable`
where (`color` LIKE ? or `pattern` LIKE ? or `style` LIKE ?)
group by `col_2` //<< i need this only for 'pattern' above and not the entire query
Heres the model:
// $filterArr example
// Array ( [color] => grey [pattern] => stripe )
$query = DB::table('mytable');
$query = $query->select(array('col_1', 'col_2', 'col_3'), DB::raw('count(*) as total'));
$query = $query->where(function($query) use ($filterArr){
$ii = 0;
foreach ($filterArr as $key => $value) {
if ($key=='color'){
$column = 'color';
}else if ($key=='style'){
$column = 'style';
}else if ($key=='pattern'){
$column = 'pattern';
$query = $query->groupBy('col_2'); // << !! does not work
}
if($ii==0){
$query = $query->where($column, 'LIKE', '%'.$value.'%');
}
else{
$query = $query->orWhere($column, 'LIKE', '%'.$value.'%');
}
$ii++;
}
});
$query = $query->orderBy('col_2', 'asc')->simplePaginate(30);
I think you can simplify your code a bit:
$query = DB::table('mytable');
$query = $query->select(array('col_1', 'col_2', 'col_3'), DB::raw('count(*) as total'));
$query = $query->where(
collect($filterArr)
->only(['color','style','pattern'])
->map(function ($value, $key) {
return [ $key, 'like', '%'.$value.'%', 'OR' ];
})->all()
)->when(array_key_exists('pattern', $filterArr), function ($query) {
return $query->groupBy('col_2');
});
$query = $query->orderBy('col_2', 'asc')->simplePaginate(30);

Search Function with Multiple Conditions in Laravel

I tried to make a search function based on 4 requests; categories, location, keyword, and sorting. The search function with 3 requests (location, categories, and sorting) works well and has correct output.
But the problem is, when my website sent request with keywords data, the output results are only the result from keywords request only, when I try to sent a location / categories requests, the output does not change (only the output of the keyword request only)
Here is the code I write:
public function getBy(Request $request) {
$categories = $request->all()['categories'];
$location = $request->all()['location'];
$sorting = $request->all()['sorting'];
$keyword = $request->all()['keyword'];
//--- sorting code ---
//keyword request
if ($keyword === null) {
$jobs = Job::query();
} else {
$jobs = Job::query()->where('job_title', 'LIKE', "%{$keyword}%")
->orWhere('title', 'LIKE', "%{$keyword}%")
->orWhere('description', 'LIKE', "%{$keyword}%")
->orWhereHas('users', function ($q) use ($keyword) {
$q->where('company_name', 'LIKE', "%{$keyword}%");
})->orWhereHas('categories', function ($q) use ($keyword) {
$q->where('category', 'LIKE', "%{$keyword}%");
})->orWhereJsonContains('tags', $keyword);
}
//location and categories request
if ($location === [] && $categories === []) {
$jobs;
} elseif ($location === []) {
$jobs->whereHas('categories', function ($q) use ($categories) {
$q->whereIn('slugs', $categories);
});
} elseif ($categories === []) {
$jobs->whereHas('location', function ($q) use ($location) {
$q->whereIn('city_name', $location);
});
} else {
$jobs->whereHas('categories', function ($q) use ($categories) {
$q->whereIn('slugs', $categories);
})->whereHas('location', function ($q) use ($location) {
$q->whereIn('city_name', $location);
});
}
//return as JSON
}
The main issue with your code is your locations and categories search query were not setting $jobs again. It should look like this:
$jobs = $jobs->whereHas('categories', function ($q) use ($categories) {
$q->whereIn('slugs', $categories);
});
This will set the $jobs variable to include the new conditions of the query.
I would also suggest not checking if $locations or $categories are empty, as they if statements for each are dealing with the condition already. Here is some refactored code:
public function getBy(Request $request) {
$sorting = $request->all()['sorting'];
//--- sorting code ---
//keyword request
if (! $keywords = request('keywords')) {
$jobs = Job::query();
} else {
$jobs = Job::query()->where('job_title', 'LIKE', "%{$keyword}%")
->orWhere('title', 'LIKE', "%{$keyword}%")
->orWhere('description', 'LIKE', "%{$keyword}%")
->orWhereHas('users', function ($q) use ($keyword) {
$q->where('company_name', 'LIKE', "%{$keyword}%");
})->orWhereHas('categories', function ($q) use ($keyword) {
$q->where('category', 'LIKE', "%{$keyword}%");
})->orWhereJsonContains('tags', $keyword);
}
foreach (['categories', 'locations'] as $field) {
if ($request->filled($field)) {
$jobs = $jobs->whereHas($field, function ($q) use ($field) {
$q->whereIn('slugs', request($field));
});
}
}
return $jobs->get()->toArray();
}
Here we loop through the categories and locations request data and see if the data is ->filled() (is defined and not empty). If so, we run the query.
Try this
public function getBy(Request $request) {
$categories = $request->categories;
$location = $request->location;
$sorting = $request->sorting;
$keyword = $request->keyword;
//--- sorting code ---
$jobs = Job::query();
//keyword request
if ($keyword !== null) {
$jobs = $jobs->where('job_title', 'LIKE', "%{$keyword}%")
->orWhere('title', 'LIKE', "%{$keyword}%")
->orWhere('description', 'LIKE', "%{$keyword}%")
->orWhereHas('users', function ($q) use ($keyword) {
$q->where('company_name', 'LIKE', "%{$keyword}%");
})->orWhereHas('categories', function ($q) use ($keyword) {
$q->where('category', 'LIKE', "%{$keyword}%");
})->orWhereJsonContains('tags', $keyword);
}
//location and categories request
if ($location === []) {
$jobs = $jobs->whereHas('categories', function ($q) use ($categories) {
$q->whereIn('slugs', $categories);
});
} elseif ($categories === []) {
$jobs = $jobs->whereHas('location', function ($q) use ($location) {
$q->whereIn('city_name', $location);
});
} else {
$jobs = $jobs->whereHas('categories', function ($q) use ($categories) {
$q->whereIn('slugs', $categories);
})->whereHas('location', function ($q) use ($location) {
$q->whereIn('city_name', $location);
});
}
return $jobs->get();
}

How to use Laravel Eloquent with LIKE statement in array list?

I have some conditions in array like
$category = Input::get('category');
$cuisine = Input::get('cuisine');
$veg = Input::get('veg');
$trending = Input::get('trending');
$time = Input::get('time');
if($category) $conditions['category'] = $category;
if($cuisine) $conditions['cuisine'] = $cuisine;
if($veg) $conditions['veg'] = $veg;
if($trending) $conditions['trending'] = $trending;
How can I make
$list = Data::where($conditions)->where('cuisine','LIKE','%'.$cuisine.'%')->get();
Is it possible to enter LIKE % in this statement
if($cuisine) $conditions['cuisine'] = $cuisine;
The problem is that if I want to add this where('cuisine','LIKE','%'.$cuisine.'%') several areas it needs to be updated. and in some cases, if cuisine is not present everything cannot be fetched
I want to perform LIKE statement for only cuisine data.
Sure, you can do that by creating an array with this format:
[['column1', 'like', '%' . $filter1 . '%'], ['column2', 'like', '%' . $filter2 . '%']]
For example:
$fields = ['category', 'cuisine', 'veg', 'trending', 'time'];
foreach ($fields as $field) {
if ($request->get($field)) {
$conditions[] = [$field, 'like', '%' . $request->get($field) . '%'];
}
}
$list = Data::where($conditions)->get();
Another example from the docs:
You may also pass an array of conditions to the where function:
$users = DB::table('users')->where([
['status', '=', '1'],
['subscribed', '<>', '1'],
])->get();
https://laravel.com/docs/5.5/queries#where-clauses
Update
You've just updated your question and said you want to use like only for $cuisine. In this case, you can use a closure:
->where(function($q) use($request) {
if ($request->cuisine) {
$q->where('cuisine', 'like', '%' . $request->cuisine . '%');
}
})
Or you could use when():
->when($request->cuisine, function ($q) use ($cuisine) {
return $q->where('cuisine', 'like', '%' . $request->cuisine . '%');
})
Well, you can do it in parts:
$query = Data::where($conditions);
if($cuisine) {
$query->where('cuisine','LIKE','%'.$cuisine.'%');
}
$list = $query->get();
You can do it like,
$query = DB::table('data');
$category = Input::get('category');
$cuisine = Input::get('cuisine');
$veg = Input::get('veg');
$trending = Input::get('trending');
$time = Input::get('time');
if($category) {
$query->where('category','LIKE','%'.$category.'%');
}
if($cuisine) {
$query->where('cuisine','LIKE','%'.$cuisine.'%');
}
if($veg) {
$query->where('veg','LIKE','%'.$veg.'%');
}
if($trending) {
$query->where('trending','LIKE','%'.$trending.'%');
}
if($time) {
$query->where('time','LIKE','%'.$time.'%');
}
$list = $query->get();
I hope you will understand.
Why not just assign as blank value as default as it will pass in LIKE for all cases
$conditions['cuisine']= (isset($cuisine)&&$cuisine)) ? $cuisine : '';
Well, you have to assign query to some variable:
$query = Data::where($conditions);
if($cuisine) {
$query = $query->where('cuisine','LIKE','%'.$cuisine.'%');
}
$list = $query->get();

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();

Resources