My users need to be able to query the database with up 5 different parameters. I think the best way to handle this is with query scopes. Then just chain together the query scopes. But I cannot figure out how to do this based on an unknown number (0-5) of search parameters.
I have it working with one specific search parameter.
$thirtyDaysAgo = Carbon::now()->subDays(30)->toDateString();
$orders = Order::DateRange($thirtyDaysAgo)->get();
return view('orders/browse', compact('orders'));
Any help would be appreciated.
Edit: More info
Parameters are posted to the page from a form:
$input = Input::all();
dd($input);
yields
array:7 [▼
"_token" => "MX4gVmbON56f9Aa88kgn2Re68GoDrtDeR6phEJ30"
"orderType" => "1"
"orderNumber" => "1"
"timePeriod" => "0"
"orderStatus" => "0"
"sku" => ""
"customer" => "0"
]
Edit: Adding query scopes
public function scopeDateRange($query, $range){
return $query->where('created_at', '>=', $range);
}
public function scopeOrderNumber($query, $orderNumber){
return $query->whereOrderNumber($orderNumber);
}
public function scopeCustomer($query, $customer){
return $query->whereCustomerId($customer);
}
public function scopeStatus($query, $status){
if($status == 'active'){
return $query->where('orderStatus_id', '!=', 15)->where('orderStatus_id', '!=', 10);
}elseif($status == 'complete'){
return $query->whereOrderStatusId(15);
}elseif($status == 'cancelled'){
return $query->whereOrderStatusId(10);
}
}
By the looks of it, you are going to want to just check to see if your parameters are empty, and if so, you can just return the query and not perform the scope check:
public function scopeDateRange($query, $range){
if (!empty($range)) {
return $query->where('created_at', '>=', $range);
}
else {
return $query;
}
}
Then, you can just chain them all together and the scope functions will sort out whether or not to filter the query all by themselves.
$orders = Order::dateRange($range)->orderNumber($orderNumber)->customer($customer)->status($status)->get();
Yes you can, just loop the user input fields, for example:
// You have this right now, so until get is called you can chain
$thirtyDaysAgo = Carbon::now()->subDays(30)->toDateString();
// Remove the get call
$order = Order::DateRange($thirtyDaysAgo); // Or use Order::query()
// Loop other fields (exclude fields which are not required in query)
foreach(Request::except(['_token', 'other_field_name']) as $field => $value)
{
// Make sure $field (form fields) match with database filed names
$order->where($field, $value);
}
$result = $order->get(); // Run the query and get the result
This is an idea and you may need to tweak to make it fit according to your need. Try it by yourelf or post most relevant information. This is not using scopeMethods but you can do it to get what you are up to.
Related
I do a specific relation query all over the application, where I only need the User's subscriptions that have active column set to true.
And I have a scope method in User model, which applies said filter, to avoid copy/paste, like:
public function scopeWithActiveSubscriptions($query)
{
$query->with([
'subscriptions' => function ($query) {
$query->where('active', true);
},
]);
}
Now sometimes I want to eager-load the plan of each subscription, too.
For that I tried something like:
$user = User::where('id', 1)
->withActiveSubscriptions()
->with('subscriptions.plan')
->first();
$subscriptionList = $user->subscriptions;
But query results to all subscriptions,
in other words, ignores the ->where('active', true) part (of scope method).
How can I make this work correctly?
A quick solution would be modifying the scopeWithActiveSubscriptions method to allow it to accept another optional parameter that tells it which additional relations should also be included and thus you don't loose your filtering.
public function scopeWithActiveSubscriptions($query, array $with = [])
{
// just merges hard coded subscription filtering with the supplied relations from $with parameter
$query->with(array_merge([
'subscriptions' => function ($query) {
$query->where('active', true);
}
], $with));
}
Now you can tell that scope which nested relations you want to include and you no longer need to call with to include them by yourself.
$user = User::where('id', 1)
->withActiveSubscriptions(['subscriptions.plan'])
// ->with('subscriptions.plan') // no longer needed as we're telling the scope to do that for us
->first();
$subscriptionList = $user->subscriptions;
With that you can pass custom relations to the scope something like (am improvising here just for demo purposes)
$user = User::where('id', 1)
->withActiveSubscriptions([
'subscriptions.plan' => fn($q) => $q->where('plans.type', 'GOLD')
])->first();
Learn more about Laravel's Eloquent Scopes.
Hope i have pushed you further.
Seems Laravel does not have yet any chainable (Builder-style) solution (for asked situation), and we ended up editing the scope filter.
Into something like:
public function scopeWithPendingSubscriptions(Builder $query, $subRelations = null)
{
$query->with([
'subscriptions' => function (HasMany $query) use ($subRelations) {
$query->where('active', '=', true);
if ($subRelations) {
$query->with($subRelations);
}
},
]);
}
Which allows me to do query like:
// ...
->withActiveSubscriptions('plan');
Instead of my old (not working) code, which was:
// ...
->withActiveSubscriptions()
->with('subscriptions.plan');
Note that even passing nested-filters is now possible, like:
// ...
->withActiveSubscriptions(['plan' => function ($query) {
$query->where('name');
}]);
(Basically same as Laravel's ->with(...) method.)
Controller
public function detail(Peserta $peserta)
{
// get konfirmasi_id
$konfirmasi = KonfirmasiPembayaran::where('email',$peserta->email)->select('id')->get();
$payments = BankSettlement::whereIn('konfirmasi_id',array($konfirmasi->id))->get();
// dd($payments);
$tagihan = Tagihan::where([['peserta_id', $peserta->id],['type', 3]])->first();
return view('data.peserta.detail', ['data' => $peserta, 'payments' => $payments,'tagihan' => $tagihan]);
}
I want to display data from BankSettlement based on konfirmasi_id. Here I try to use WhereIn Query like this, but still error "Property [id] does not exist on this collection instance.".
$konfirmasi has data like the image above.
What is the correct way to display data from BankSettlement based on konfirmasi_id ? Thankyou
Try this changes:
$konfirmasi = KonfirmasiPembayaran::where('email',$peserta->email)->pluck('id')->toArray();
$payments = BankSettlement::whereIn('konfirmasi_id',$konfirmasi)->get();
This is the wrong way to change a collection to array.
$payments=BankSettlement::whereIn('konfirmasi_id',array($konfirmasi->id))->get();
You should do this
public function detail(Peserta $peserta)
{
// get konfirmasi_id
$konfirmasi = KonfirmasiPembayaran::where('email',$peserta->email)
->select('id')
->get()
->pluck('id')
->toArray(); //This will return an array of ids
$payments = BankSettlement::whereIn('konfirmasi_id',$konfirmasi)->get();
// dd($payments);
$tagihan = Tagihan::where([['peserta_id', $peserta->id],['type', 3]])->first();
return view('data.peserta.detail', ['data' => $peserta, 'payments' => $payments,'tagihan' => $tagihan]);
}
Edit:
Read Laravel Collections|Pluck
If you do not have to reuse the result of $konfirmasi then it would be better to use subquery. Writing a subquery is optimized way. if you write two different query then there will be two seperate database connection request.
Laravel subquery
$konfirmasi = KonfirmasiPembayaran::where('email',$peserta->email)->select('id');
$payments = BankSettlement::whereIn('konfirmasi_id', $konfirmasi )->get();
I'm currently trying to get data from the DB with a Laravel controller through a model Maintain. Is there a way to query the DB based on the request data made by an axios post from the front end, this includes both variables sub and zone, I have the ability to get the data from using sub OR zone but not both, is there a way to construct a multi where query if $request contains the necessary variables and a standard request if they are null or ""?
public function all(Request $request){
$query = [];
if($request->sub != ""){
array_push($query, ['subsystem', '=', $request->sub]);
}
if($request->zone != ""){
array_push($query, ['zone', '=', $request->zone]);
}
if(count($query) > 0){
return Maintain::all()->where($query);
}
else{
return Maintain::all();
}
}
Currently This returns with an error ErrorException: array_key_exists(): The first argument should be either a string or an integer in file but I've been using the Laravel reference and it doesn't seem to be working. I used Postman to get the readout of $query and it contains the following:
[
['sub', '=', 'Subsystem 1'],
['zone', '=', 'Zone 1']
]
Any help would be appreciated.
Try like this
public function all(Request $request){
$result = Maintain::when($request->zone, function ($q) use($request){
$q->where('zone', $request->zone);
})
->when($request->sub, function ($qw) use($request){
$qw->where('subsystem', $request->sub);
})
->get();
return($result);
}
when() method look like if-else
Edited: Let we know if you get an error: Happy coding
I am trying to implement a filtering method for some products.
This is the route:
Route::get('/TVs/{type?}/{producer?}', 'Product\AllProducts#getTVs')->where(['type' => '4kTV|curved|lcd|oled|plasma'], ['producer'=>'Samsung'])->name('TVs');
And this is the controller function:
public function getTVs($type = null, $producer = null)
{
$products = DB::table('products')->paginate(16);
if($type!=null) {
$products = Product::where('type', $type)->paginate(16);
}
if($producer!=null) {
$products = Product::where('description','like', '%'.$producer.'%')->paginate(16);
}
return view('product.TVs', ['products' => $products]);
}
If I select the type, the page refreshes and shows the results. Then if i enter the producer, again it works. How can i make the route in such a way, that the order of the optional parameters does not matter and i can filter the results no matter the order ?
Chain your queries; right now, you're running 3 queries, with ->paginate() being a closure and triggering a DB call. Try this:
$baseQuery = DB::table("products");
if($type){
$baseQuery->where("type", "=", $type);
}
if($producer){
$baseQuery->where("description", "like", "%".$producer."%");
}
$products = $baseQuery->paginate(16);
return view("products.TVs"->with(["products" => $products]);
As you can see, we add ->where clauses as required based on the input, and only run a single ->paginate() right before the return. Not this is additive searching, so it's WHERE ... AND ... and not WHERE ... OR ...; extra logic would be required for that.
I've got the below function, which attempts to match users on specific whitelisted fields, which works brilliantly, for small amounts of data, but in our production environment, we can have > 1 million user records, and Eloquent is (understandably) slow when creating models in: $query->get() at the end. I asked a question this morning about how to speed this up and the accepted answer was brilliant and worked a treat, the only problem now, is that the resulting query being sent to DB::select($query->toSql()... has lost all of the required extra relational information I need. So is there any way (keeping as much of the current function as possible), to add joins to DB::select so that I can maintain speed and not lose the relations, or will it require a complete re-write?
The recipients query should include relations for tags, contact details, contact preferences etc, but the resulting sql from $query->toSql() has no joins and only references the one table.
public function runForResultSet()
{
$params = [];
// Need to ensure that when criteria is empty - we don't run
if (count($this->segmentCriteria) <= 0) {
return;
}
$query = Recipient::with('recipientTags', 'contactDetails', 'contactPreferences', 'recipientTags.tagGroups');
foreach ($this->segmentCriteria as $criteria) {
$parts = explode('.', $criteria['field']);
$fieldObject = SegmentTableWhiteListFields::where('field', '=', $parts[1])->get();
foreach ($fieldObject as $whiteList) {
$params[0] = [$criteria->value];
$dateArgs = ((strtoupper($parts[1]) == "AGE" ? false : DatabaseHelper::processValue($criteria)));
if ($dateArgs != false) {
$query->whereRaw(
DatabaseHelper::generateOperationAsString(
$parts[1],
$criteria,
true
),
[$dateArgs['prepared_date']]
);
} else {
// Need to check for empty value as laravel's whereRaw will not run if the provided
// params are null/empty - In which case we need to use whereRaw without params.
if (!empty($criteria->value)) {
$query->whereRaw(
\DatabaseHelper::generateOperationAsString(
$parts[1],
$criteria
),
$params[0]
);
} else {
$query->whereRaw(
\DatabaseHelper::generateOperationAsString(
$parts[1],
$criteria
)
);
}
}
}
}
// Include any tag criteria
foreach ($this->segmentRecipientTagGroupCriteria as $criteria) {
$startTagLoopTime = microtime(true);
switch (strtoupper($criteria->operator)) {
// IF NULL check for no matching tags based on the tag group
case "IS NULL":
$query->whereHas(
'recipientTags',
function ($subQuery) use ($criteria) {
$subQuery->where('recipient_tag_group_id', $criteria->recipient_tag_group_id);
},
'=',
0
);
break;
// IF NOT NULL check for at least 1 matching tag based on the tag group
case "IS NOT NULL":
$query->whereHas(
'recipientTags',
function ($subQuery) use ($criteria) {
$subQuery->where('recipient_tag_group_id', $criteria->recipient_tag_group_id);
},
'>=',
1
);
break;
default:
$query->whereHas(
'recipientTags',
function ($subQuery) use ($criteria) {
$dateArgs = (DatabaseHelper::processValue($criteria));
$subQuery->where('recipient_tag_group_id', $criteria->recipient_tag_group_id);
if ($dateArgs != false) {
$subQuery->whereRaw(
DatabaseHelper::generateOperationAsString(
'name',
$criteria,
true
),
[$dateArgs['prepared_date']]
);
} else {
// Need to check for empty value as laravel's whereRaw will not run if the provided
// params are null/empty - In which case we need to use whereRaw without params.
if (!empty($criteria->value)) {
$subQuery->whereRaw(
\DatabaseHelper::generateOperationAsString(
'name',
$criteria
),
[$criteria->value]
);
} else {
$subQuery->whereRaw(
\DatabaseHelper::generateOperationAsString(
'name',
$criteria
)
);
}
}
},
'>=',
1
);
}
}
//$collection = $query->get(); // slow when dealing with > 25k rows
$collection = DB::select($query->toSql(), $query->getBindings()); // fast but loses joins / relations
// return the response
return \ApiResponse::respond($collection);
}
By lost relational information do you mean relations eagerly loaded the name of which you passed to with()?
This information was not lost, as it was never in the query. When you load relations like that, Eloquent runs separate SQL queries to fetch related objects for the objects from your main result set.
If you want columns from those relations to be in your result set, you need to explicitely add joins to your query. You can find information about how to do this in the documentation: https://laravel.com/docs/5.1/queries#joins