dynamic query laravel eloquent with whereHas - php

I want to create dynamic filters.
for example I want to create this code
$Contact = Contact::whereHas('User', function ($query) use ($searchString) {
$query->where('name', 'like', '%Jhone%')->orwhere('family', '<>' . 'Doe');
})->whereHas('Account', function ($query) use ($searchString) {
$query->where('account_name', '=' , 'test_account' )->orwhere('account_city', 'like', '%test_city%');
})->get();
and all of parameters is variable
name,like,%Jhone%,family,<>,Doe,.....
and I want to pass variables to function and function create above query.

I assume that the relationship functions within your Contact, User and Account models are written in camelCase and not PascalCase like your example shows.
public function getContacts(Request $request)
{
return Contact::when($request->get('username'), function ($query, $val) use ($request) {
$query->whereHas('user', function ($q) use ($val, $request) {
$q->where('name', 'like', '%'.$val.'%');
if ($request->has('familyname')) {
$q->orWhere('family', '<>', $request->get('familyname'));
}
});
})
->when($request->get('accountname'), function ($query, $val) use ($request) {
$query->whereHas('account', function ($q) use ($val, $request) {
$q->where('account_name', $val);
if ($request->has('city')) {
$q->orWhere('account_city', 'like', '%'.$request->get('city').'%');
}
});
})
->get();
}
This function will return all contacts when no GET parameters are given on the request. If a parameter for username is present, it will only return contacts where a user with the given name exists for. If furthermore a familyname parameter is present, it will give contacts with a user that has a matching username or a familyname different from the one given. The very same applies to the account, accountname and city.
In particular, there are two things interesting about this example:
The when($value, $callback) function can be used to build very dynamic queries which only execute the $callback when $value is true. If you use $request->get('something') and something is not available as parameter, the function will return null and the callback is not executed. The callback itself has the form function ($query, $value) { ... }, where $value is the variable you passed to when() as first parameter.
Using $request->has('something') inside the query builder functions to dynamically build constraints on the query is an alternative to when(). I only added it for the purpose of demonstration - in general I'd recomment sticking to one style.
If you would extend on the example, you could also build highly dynamic queries where not only the variable content like Doe for the family name is given as parameters, but also the comparator like =, <> or like. But extending further on this topic is too much for this answer and there are already tutorials about this topic available anyway.
Edit: here an example for a dynamic query with more detailed input
Expected input (slightly different than your request because yours cannot work):
$filters = [
'user' => [
['name','like','%Jhone%'],
['family','<>','Doe'],
],
'account' => [
['account_name','=','test_account'],
['account_city','like','%test_city%'],
]
];
And the function:
public function getContacts(Request $request, array $filters)
{
$query = Contact::query();
foreach ($filters as $key => $constraints) {
$query->whereHas($key, function ($q) use ($constraints) {
if (count($constraints) > 0) {
$q->where($constraints[0][0], $constraints[0][1], $constraints[0][2]);
}
for ($i = 1; $i < count($constraints); $i++) {
$q->orWhere($constraints[$i][0], $constraints[$i][1], $constraints[$i][2]);
}
});
}
return $query->get();
}
This will always use OR for multiple constraints and not AND. Using AND and OR mixed would require a lot more sophisticated system.

Related

Laravel - Group multiple where clause as orWhere

Is it possible to append multiple where clause and make them orWhere clause?
This is what I mean:
public function call()
{
$pageTypes = ["page_user", "guest_user" ...etc];
$appendQuery = [];
// in here, the query is always where making the sql query "page_type LIKE %page_user% and page_type LIKE %guest_user%"
// my written mysql query here might be written wrong but I hope you get the idea.
// I want to change the subsequent query to orWhere
foreach ($pageTypes as $i => $pageType) {
array_push($appendQuery, function ($query) use ($pageType) {
return $this->addPageTypeQuery($query, $pageType);
});
}
}
public function addPageTypeQuery($query, $pageType)
{
return $query->where('page_type', 'LIKE', $pageType);
}
Though I can manually create a function with query where・orWhere, but if there is a laravel/php way to do this, that would help.
Note I am unable to change the contents of addPageTypeQuery function. I know I can add an input parameter inside then create an ifelse/switch statement to determine what the desired return data but I cannot do so since my PM will not allow it.
Extra note I forgot I can use whereIn but currently there is no whereIn function in the current repository.php file I am working and I cannot add/edit functions because of PM. I might create my own function of whereIn clause or hack the appenedQuery and manually change the where to orWhere (Bad practice) Too complicated. Will create a whereIn instead.
orWhere() just uses where() in the background so you should be able to do it. The signature for the where method is
public function where($column, $operator = null, $value = null, $boolean = 'and')
{
...
}
orWhere is pretty similar
public function orWhere($column, $operator = null, $value = null)
{
...
return $this->where($column, $operator, $value, 'or');
}
With this in mind, I think you only need a way to pass an extra argument to your addPageTypeQuery method.
public function addPageTypeQuery($query, $pageType, $boolean = 'and')
{
return $query->where('page_type', 'LIKE', $pageType, $boolean);
}
foreach ($pageTypes as $i => $pageType) {
array_push($appendQuery, function ($query) use ($pageType) {
if (/* should be an OR query */) {
return $this->addPageTypeQuery($query, $pageType, 'or');
}
return $this->addPageTypeQuery($query, $pageType);
});
}

Dynamic query eloquent

I have these 2 linked models: jobs and job_translations. A job have many translations. So in my job model, there is :
/**
* Get the translations for the job.
*/
public function translations()
{
return $this->hasMany('App\Models\JobTranslation');
}
In my controller, I want to build a query dynamically like that :
$query = Job::query();
if ($request->has('translation')) {
$query->translations()->where('external_translation', 'ilike', '%'.$request->translation.'%');
}
$jobs = $query->paginate(10);
I have this error :
Call to undefined method Illuminate\Database\Eloquent\Builder::translations()
Is it possible to do such a dynamic query with Eloquent?
Yes, it is possible. What you are looking for is whereHas('translations', $callback) instead of translations():
$query = Job::query();
if ($request->has('translation')) {
$query->whereHas('translations', function ($query) use ($request) {
$query->where('external_translation', 'ilike', '%'.$request->translation.'%');
});
}
$jobs = $query->paginate(10);
Your query can be improved further by using when($condition, $callback) instead of an if:
$jobs = Job::query()
->when($request->translation, function ($query, $translation) {
$query->whereHas('translations', function ($query) use ($translation) {
$query->where('external_translation', 'ilike', "%{$translation}%");
});
})
->paginate(10);
the issue you should do eager loading to detected on next chained query like this:
$query = Job::with('translations')->query();

Building complex where statement in Laravel Eloquent using

I have an Eloquent\Builder $query that I want to use additional where() calls on, where the amount of the calls is indefinite and is taken from an array $filter, example below:
$filter = [
'or:email:=:ivantalanov#tfwno.gf',
[
'or:api_token:=:abcdefghijklmnopqrstuvwxyz',
'and:login:!=:administrator',
],
];
The strings, when parsed, produce valid SQL conditions, but the problem lies into sticking them into closures where a group is present (like strings 2 and 3 in the example - the array is their 'group').
I know of Laravel's functionality that allows sticking a Closure function into $query->where() to achieve what I want, but the problem I'm facing is actually building those complex closures. I have to iterate through every string in the group and pass it into the closure generated like so (where $item is the result of parsing a condition string):
$closure = function ($query) use ($item)
{
call_user_func_array(
[$query, $item['function']], [$item['field'], $item['operator'], $item['values']]
);
};
Now the obvious problem with this is while it makes simple closures easily, passing more than one condition is plain impossible.
My question is, what could I use to prepare a complex statement to be executed on a query inside a closure?
Okay, I think I figured it out.
This is the method that will return the end result.
public function parse_filter(Builder &$query, array $filter)
{
$groups = $this->_prepare_groups($filter);
return $this->_parse_groups($query, $groups);
}
These methods will parse the initial array into something more usable.
private function _prepare_groups(array $filter)
{
foreach ($filter as $key => $item) {
if (is_array($item)) {
$groups[] = $this->_prepare_groups($item);
}
if (is_string($item)) {
$simple_filter = $this->_parse_simple_filter($item);
$groups[] = $simple_filter;
$simple_filter = null;
}
}
return $groups;
}
private function _parse_simple_filter(string $filter)
{
$filter_data = explode(':', $filter);
$simple_filter['function'] = $filter_data[0] === 'and' ? 'where' : 'orWhere';
$simple_filter['field'] = $filter_data[1];
$simple_filter['operator'] = $filter_data[2];
$simple_filter['values'] = $filter_data[3];
return $simple_filter;
}
And here is where the most of the magic happens. Closures are recursive calls to this method, as you can see.
private function _parse_groups(Builder &$query, array $groups)
{
foreach ($groups as $operator => $group) {
if (!array_key_exists('function', $group)) {
$closure = function ($query) use ($group)
{
$this->_parse_groups($query, $group);
};
$query->where($closure);
} else {
$query->{$group['function']}($group['field'], $group['operator'], $group['values']);
}
}
return $query;
}
Using this, you can modify an Eloquent\Builder object however you like with deeply nested filters that are declared dynamically (for example, received within a GET/POST request).
I guess this will help for you:
In model create a scope:
public static function scopeGetResultList($query) {
return $query->where(function ($query) use ($item) {
$query->where('group_user_holder_type', '=', 1)
->orWhere('group_user_holder_type', '=', 0);
});
}
OR
Example:
public static function getSearchedUserAuto($search_key, $user_id)
{
$users = DB::table((new User)->getTable().' as U')
->select('U.*', 'CT.city_name', 'C.nicename')
->leftJoin((new Country)->getTable().' as C', 'C.country_id', '=', 'U.user_country')
->leftJoin((new City)->getTable().' as CT', 'CT.city_id', '=', 'U.user_city')
->where(function($query) use ($search_key){
$query->where('U.user_full_name', 'like', '%'.$search_key.'%')
->orWhere('U.user_email', 'like', '%'.$search_key.'%');
})
->where(function($query) use ($search_key){
$query->where('U.user_full_name', 'like', '%'.$search_key.'%')
->orWhere('U.user_email', 'like', '%'.$search_key.'%');
})
->where('U.status', '=', 1)
->where('U.user_id', '!=', $user_id)
->get();
return $users;
}
See if it is work for you.

Eloquent scope method gives different results

I'm using Laravel 5.4
Working code
$cityWithEvents = City::with(['events' => function ($q) {
$q->whereDate('start_time', Carbon::today('America/Montreal'))->orwhereBetween('start_time', [Carbon::today('America/Montreal'), Carbon::tomorrow('America/Montreal')->addHours(4)]);
}])->where('active', 1)->get()->keyBy('id');
Not working code
$cityWithEvents = City::with('todayEventsWithAfterHoursIncluded')
->where('active', 1)
->get()
->keyBy('id');
City model
public function events() {
return $this->hasManyThrough('App\Event', 'App\Venue');
}
public function todayEventsWithAfterHoursIncluded () {
return $this->events()
->whereDate('start_time', Carbon::today('America/Montreal'))
->orwhereBetween('start_time', [
Carbon::today('America/Montreal'),
Carbon::tomorrow('America/Montreal')->addHours(4)
]);
}
Questions
When trying to create a scope method the query gives me different result. I can't see why and what should I change
I've only used scopes a few times, but never within a ->with() clause. On your City model, create a new scope:
public function scopeTodayEventsWithAfterHoursIncluded($query){
return $query->with(["events" => function($subQuery){
$subQuery->whereDate('start_time', Carbon::today('America/Montreal'))->orWhereBetween('start_time', [Carbon::today('America/Montreal'), Carbon::tomorrow('America/Montreal')->addHours(4)]);
});
}
Then, on your City query, add it as a scope function:
$cityWithEvents = City->where('active', 1)
->todayEventsWithAfterHoursIncluded()
->get();
I think the way you are using it requires that your Event model has the scope on it, as you're technically calling with("events") on your base query and your scoped one.
Let me know if this changes you results.
If you do the query, you should do it like this:
$cityWithEvents = City::withTodayEventsWithAfterHoursIncluded()
->where('active', 1)
->get()
->keyBy('id');
You scope in you model should look like this:
public function scopeWithTodayEventsWithAfterHoursIncluded ($query)
{
return $query
->with(['events' => function ($q) {$q
->whereDate('start_time', Carbon::today('America/Montreal'))
->orwhereBetween('start_time', [
Carbon::today('America/Montreal'),
Carbon::tomorrow('America/Montreal')->addHours(4)
]);
}]);
}
Now it should be equal.

Search query on the basis of if post variable exists in laravel

I want to write a search query on Laravel on the basis of either "keyword" or "experience" or "location" search query should run if any of these variable exists.
I am making an Ajax call to achieve this
jobsController.php
public function homepageSearch() {
$_POST = json_decode(file_get_contents('php://input'), true);
$jobs = Jobs::latest('created_at')->search()->get();
echo $jobs;
}
Model jobs.php
class Jobs extends Model {
public function scopeSearch($query) {
$query->where('job_title', 'like', '%'.$_POST['keyword'].'%')
->orWhere('job_description', 'like', '%'.$_POST['keyword'].'%');
if(isset($_POST['keyword'])) {
$query->where('job_title', 'like', '%'.$_POST['keyword'].'%')
->orWhere('job_description', 'like', '%'.$_POST['keyword'].'%');
}
if(isset($_POST['experience'])) {
$query->where('min_exp', $_POST['experience'])
->orWhere('max_exp', $_POST['experience']);
}
if(isset($_POST['city'])) {
$query->where('job_location','like','%'.$_POST['city'].'%');
}
}
}
I want to search on the basis of either keyword or city or experience is this correct way to achieve this in laravel?
I am new to Laravel. Can you suggest me with this.
class Job extends Model {
public function scopeSearch($query, $keyword, $experience, $city) {
$query->where(function ($q) {
$q->where('job_title', 'like', '%'.$_POST['keyword'].'%')
->orWhere('job_description', 'like', '%'.$_POST['keyword'].'%');
});
if(isset($keyword)) {
$query->where(function ($q) {
$q->where('job_title', 'like', '%'.$_POST['keyword'].'%')
->orWhere('job_description', 'like', '%'.$_POST['keyword'].'%');
});
}
if(isset($experience)) {
$query->where(function($q) {
$q->where('min_exp', $_POST['experience'])
->orWhere('max_exp', $_POST['experience']);
});
}
if(isset($city)) {
$query->where('job_location','like','%'.$_POST['city'].'%');
}
return $query;
}
}
Call from your controller using the following:
Job::search($request->input('keyword'),
$request->input('experience'), $request->input('city'));
A few observations/suggestions:
Where chaining needs to be correct. When you say $query->where(..a..)->orWhere(..b..)->where(..c..)->orWhere(..d..) it will evaluate to: ((a && c) || b || d). Where you intended ((a || b) && (c || d)). This is why you need to use closures like I have above using parameter grouping
Avoid using $_POST, use the Request object instead as Laravel does quite a lot of work for you when you use $request
Avoid calling your request object from the model. It's not the model's responsibility to check request/post variables, it's your controller's responsibility to do so. Use dynamic scopes instead to segregate the responsibilities
You need to return the query object in scopes
A model is one entity. So "a job" is a model not "jobs". So I renamed the Jobs class to Job :)

Categories