Is it possible to combine whereHas with 'or' queries? - mysql

I'm trying to implement a filter system where, among other attributes and relationships, items are categorized. However, the challenge appears when combining OR queries with other filters using the regular and clause. The result grabs rows which I do not want and adds the or when the condition for that fails, thus polluting the final results with unwanted data.
<?php
class ProductSearch {
public $builder;
private $smartBuild; // this is the property I'm using to disable the alternation when other search parameters are present to avoid polluting their result
function __construct( Builder $builder) {
$this->builder = $builder;
}
public function applyFilterToQuery(array $filters) {
$pollutants = ['subcategory', 'subcategory2', 'category'];
$this->smartBuild = empty(array_diff( array_keys($filters), $pollutants)); // [ui=>9, mm=>4], [mm]
foreach ($filters as $filterName => $value) {
// dd($filters, $filterName );
if (method_exists($this, $filterName) && !empty($value) )
$this->$filterName( $value);
}
return $this;
}
public function location( $value) {
$this->builder = $this->builder
->whereHas('store2', function($store) use ($value) {
$store->where('state', $value);
});
}
public function subcategory( $value) {
$name = Subcategories::where('id', $value)->pluck('name');
$this->builder = $this->builder->where('subcat_id', $value);
if ($name->isNotEmpty() && $this->smartBuild) {
$names = preg_split('/\W\s+/', $name[0]);
if (!$names) $names = $name;
foreach ($names as $value)
$this->builder = $this->builder->orWhere('name', 'like', "%$value%");
}
}
}
You may observe from the above that making a request for categories searches products matching the category name. But on attempting to combine that alternate match with legitimate AND queries (in location for instance, the result tends to include matching locations OR matching names.
The desired result is ((matching name OR matching category) AND matching location). Is this possible?

I had similar situation like this few days ago about User and Posts.
Needed a list of posts which user has commented or participated in and which user owns.
So I did following on User model
//Get user created post or if user has participated in the post
$queryString->where(function ($query) use ($user_id) {
return $query->whereHas('participants', function ($q) use ($user_id) {
$q->where('user_id', $user_id);
})->orWhere('id', $user_id);
});
Hope this helps.

Related

Laravel Eloquent JSON Contains, how to provide WhereIn logic (one of array values)

there is my code:
protected function room_count($room_count)
{
$query = $this->builder
->whereJsonContains('rent_requests.rooms_count', $room_count);
return $query;
}
There is my filter function. $room_count is array, and for example can be [2,4].
rent_requests.rooms_count is JSON array in MySQL, and it can be for example [2,3].
I need to filter this, to get this advert showed, but whereJSONContains expects that there will be 2 and 4, not 2 or 4.
Is there any tricks to make this function work correctly ? Something like json contains whereIn ?)
Sorry for my english, im really stuck on this, please help me :)
MySQL and PostgreSQL support whereJsonContains with multiple values:
So if you are using either of the database then you can pass $room_count as the second parameter
// Room Count Filter
protected function room_count($room_count)
{
return $this->builder
->where(function($query) use($room_count){
$query->whereJsonContains('rent_requests.rooms_count', $room_count[0]);
for($i = 1; $i < count($room_count); $i++) {
$query->orWhereJsonContains('rent_requests.rooms_count', $room_count[$i]);
}
return $query;
});
}
Laravel docs: https://laravel.com/docs/8.x/queries#json-where-clauses
but #donkarnash function is slightly changed, here is the final version with this example for those who face the same problem
// Room Count Filter
protected function room_count($room_count)
{
return $this->builder
->where(function($query) use($room_count) {
$query->whereJsonContains('rent_requests.rooms_count', $room_count[0]);
info (count($room_count));
for($i = 1; $i <= count($room_count) - 1; $i++) {
$query->orWhereJsonContains('rent_requests.rooms_count', $room_count[$i]);
}
return $query;
});
}

List filtering of users, returns empty object

I have two list-filtering inputs, if used individually they work perfectly but when used together it returns an empty object.
the filters contain one simple search Input field, and one select.
they trigger an API call to the server which when used both looks like this
http://127.0.0.1:8000/api/user?&search=jom&type=admin
the method which this call trigger looks like this
public function index(Request $request) {
$search = $request->input('search');
$type = $request->input('type');
$user = User::select('*');
$this->checkSearch($user, $search); // check for search
$this->filterUserType($user, $type); // filter user type
...
the method checkSearch looks like this
private function checkSearch(&$query, $search) {
if (!isset($query)) {
return $query;
}
if (!is_null($search)) {
$searchTerms = $this->stringToArray($search, ' ');
$query = $query->where(function ($query) use ($searchTerms) {
for ($i = 0, $max = count($searchTerms); $i < $max; $i++) {
$term = str_replace('_', '\_', mb_strtolower('%' . $searchTerms[$i] . '%'));
$query->whereRaw("(Lower(name) LIKE ?)", [$term, $term])
->orWhereRaw("(Lower(bio) LIKE ?)", [$term, $term]);
}
});
}
}
and the filterUserType like this
private function filterUserType(&$query, $type) {
if (!isset($query)) { return $query; }
if (!is_null($type)) {
$query = $query->where( 'type', $type);
}
}
I've tried to check the where on the filterUserType method, to orWhere but this just returns values of both not combined.
I triggered a break on the raw query and it appeared like this
select * from `users` where ((Lower(name) LIKE %jom%) or (Lower(bio) LIKE %jom%)) and `type` = %jom%)
When I switched the methods I got the right results.
Switching from
$this->checkSearch($user, $search); // check for search
$this->filterUserType($user, $type); // filter user type
to
$this->filterUserType($user, $type); // filter user type
$this->checkSearch($user, $search); // check for search
Still don't know why but it worked

How to improve performance of multiple count queries in a laravel view

I'm working on a marketing application that allows users to message their contacts. When a message is sent, a new "processed_message" database entry is created. There is a list view that displays all campaigns and the number of messages sent, blocked and failed for each campaign. My problem is that this list view takes way too long to load after there are > 50 campaigns with lots of messages.
Currently each campaign has 3 computed attributes (messages_sent, messages_failed and messages_blocked) that are all in the Campaign model's "appends" array. Each attribute queries the count of processed_messages of the given type for the given campaign.
namespace App;
class Campaign
{
protected $appends = [
'messages_sent',
'messages_blocked',
'messages_failed'
];
/**
* #relationship
*/
public function processed_messages()
{
return $this->hasMany(ProcessedMessage::class);
}
public function getMessagesSentAttribute()
{
return $this->processed_messages()->where('status', 'sent')->count();
}
public function getMessagesFailedAttribute()
{
return $this->processed_messages()->where('status', 'failed')->count();
}
public function getMessagesBlockedAttribute()
{
return $this->processed_messages()->where('status', 'blocked')->count();
}
}
I also tried to query all of the messages at once or in chunks to reduce the number of queries but getting all of the processed_messages for a campaing at once will overflow memory and the chunking method is way too slow since it has to use offset. I considered using eager loading the campaigns with processed_messages but that would obviously use way too much memory as well.
namespace App\Http\Controllers;
class CampaignController extends Controller
{
public function index()
{
$start = now();
$campaigns = Campaign::where('user_id', Auth::user()->id)->orderBy('updated_at', 'desc')->get();
$ids = $campaigns->map(function($camp) {
return $camp->id;
});
$statistics = ProcessedMessage::whereIn('campaign_id', $ids)->select(['campaign_id', 'status'])->get();
foreach($statistics->groupBy('campaign_id') as $group) {
foreach($group->groupBy('status') as $messages) {
$status = $messages->first()->status;
$attr = "messages_$status";
$campaign = $campaigns->firstWhere('id', $messages->first()->campaign_id);
$campaign->getStatistics()->$attr = $status;
}
}
return view('campaign.index', [
'campaigns' => $campaigns
]);
}
}
My main goal is to reduce the current page load time considerably (which can take anywhere from 30 seconds to 5 minutes when there are a bunch of campaigns).
You could use the withCount method to count all the objects without loading the relation.
Reference:
If you want to count the number of results from a relationship without actually loading them you may use the withCount method, which will place a {relation}_count column on your resulting models.
In your controller you could do this:
$count = Campaign::withCount(['processed_messages' => function ($query) {
$query->where('content', 'sent');
}])->get();
You could do multiple counts in the same relationship too:
$campaigns = Campaign::withCount([
'processed_messages',
'processed_messages as sent_message_count' => function ($query) {
$query->where('content', 'sent');
}],
'processed_messages as failed_message_count' => function ($query) {
$query->where('status', 'failed');
}],
'processed_messages as blocked_message_count' => function ($query) {
$query->where('status', 'blocked');
}])->get();
You can access the count with this:
echo $campaigns[0]->sent_message_count
Docs

Trying to delete only one row in table - Laravel

I am trying to update some users info. Here is my code :
public function update(Request $request, $id)
{
$user = Auth::user();
$user_id = Auth::user()->id;
$arrRequest = $request->all();
$contact = Contact::findOrFail($id)->where('user_id', $user_id);
$validator = Validator::make($arrRequest, Contact::$rules);
$content = null;
if ($validator->fails()) {
$status = 400;
$content = $validator->errors();
} else {
$contact->update($arrRequest)->save();
$status = 200;
}
return response($content, $status);
}
The main problem is that the request is applied to all rows in the table though I'm specifying the $id of the row for the request to be applied to. I'm struggling to see where is my mistake. The second problem (that just popped up) is that when I perform the request I'm now getting a message that says : Call to a member function save() on integer. But it was working just fine earlier (except it was updating all rows..) ! And Im retrieving an object ($contact) and not just an integer...
Thanks !
You are using findOrFail() method, which returns a Model or Collection.
After that you actually convert $contact into a Builder object by appending the where() method on the findOrFail() result. findOrFail() expects either an $id or array of $ids and will return a Model or Collection, not a Builder.
If you just want to make sure that the requested id is owned by the user, you can either check that after fetching the object, or use something other than findOrFail().
$contact = Contact::findOrFail($id);
if ($contact->user_id != $user->id) {
abort(403);
}
or
$contact = Contact::where('id', $id)
->where('user_id', $user->id)
->first();
if (! $contact) {
abort(404);
}
Although I would not recommend the last one, just the $id should be enough to fetch the item you want.
The second error is caused by calling save() after update(), update() will return a boolean.
I've finally managed to make it work.. some weirds things happening though.
Here is my solution :
public function update(Request $request, $id)
{
$user = Auth::user();
$user_id = Auth::user()->id;
$arrRequest = $request->all();
$contact = Contact::where('user_id', $user_id)->find($id);
$validator = Validator::make($arrRequest, Contact::$rules);
$content = null;
if ($validator->fails()) {
$status = 400;
$content = $validator->errors();
} else {
$contact->update($arrRequest);
$contact->save();
$status = 201;
}
return response($content, $status);
}
Thanks to Robert I can understand some parts of it. I guess if I make update() first then save() (not in a row), my save method is applying to the object and not the returned boolean? and thats why it works? still learning ahah !

How to select all rows except (A and B) in Eloquent ORM using Scope?

I'm trying to figure out how to get all rows except few (A and B) in Eloquent ORM modal.
User Model
public function notifications()
{
return $this->hasMany('notification','listener_id','id');
}
Model Notification
public function scopeFriendship($query)
{
return $query->where('object_type', '=', 'Friendship Request');
}
public function scopeSent($query)
{
return $query->where('verb', '=', 'sent');
}
Here how can I get all notifications of a user except other than (Friendship and Sent) scope.
Something like:- all rows except !(Friendship AND Sent)
It is possible to use scopes in combination with eager loading. Like that:
User::with(['notifications' => function($q){
$q->friendship();
}])->get();
However we now need to invert the scope somehow. I can think of two ways to solve this.
1. Add negative scopes
public function scopeNotFriendship($query){
return $query->where('object_type', '!=', 'Friendship Request');
}
public function scopeNotSent($query){
return $query->where('verb', '!=', 'sent');
}
User::with(['notifications' => function($q){
$q->notFriendship();
$q->notSent();
}])->get();
2. Optional parameter
Or you could introduce an optional parameter to your current scopes. Something like this:
public function scopeFriendship($query, $is = true)
{
return $query->where('object_type', ($is ? '=' : '!='), 'Friendship Request');
}
public function scopeSent($query, $is = true)
{
return $query->where('verb', ($is ? '=' : '!='), 'sent');
}
This way you would only have to pass in false:
User::with(['notifications' => function($q){
$q->friendship(false);
$q->sent(false);
}])->get();
Edit
You can even gain more control by adding a second parameter for the boolean (AND or OR of the where:
public function scopeFriendship($query, $is = true, $boolean = 'and')
{
return $query->where('object_type', ($is ? '=' : '!='), 'Friendship Request', $boolean);
}
And if you wanted either scope to be true:
$q->friendship(true, 'or');
$q->sent(true, 'or');
2nd Edit
This one finally worked (from the chat)
Notification::where('listener_id', $user_id)
->where(function($q){
$q->friendship(false)
$q->sent(false, 'or')
})
->get();