Laravel Eloquent do multiple counts on a relationship - php

I want to do multiple counts on a relationship. I can do 1 using the withCount method and passing it a closure with the condition like so:
$subscribedAudits = Audit::whereHas('users', function ($query) {
$query->whereIn('user_id', [Auth::user()->id])->where('approved', 1);
})->withCount(['users' => function($query) {
$query->where('approved', 0);
}])->with(['users'])->get();
This creates a field on the result called users_count with the count of users where approved is = 0. This is the expected result.
Now I want to do a count on another field on the users table but I'm unsure how to work that into the Eloquent query. I tried the following:
$subscribedAudits = Audit::whereHas('users', function ($query) {
$query->whereIn('user_id', [Auth::user()->id])->where('approved', 1);
})->withCount(['users' => function($query) {
$query->where('approved', 0);
}])->withCount(['users' => function($query) {
$query->where('viewed', 0);
}])->with(['users'])->get();
This won't work as it can only return 1 attribute named users_count.
Is there any way around this without doing another query?

You can pass multiple counts to the withCount method and give them a custom name like so:
Model::withCount([
'relationship as count_1' => function ($query) {
$query->where('active', 1);
},
'relationship as count_2' => function ($query) {
$query->where('active', 0);
}
]);
In your case it would be something like this:
$subscribedAudits = Audit::whereHas('users', function ($query) {
$query->whereIn('user_id', [Auth::user()->id])->where('approved', 1);
})->withCount([
'users as disapproved_count' => function ($query) {
$query->where('approved', 0);
},
'users as not_viewed_count' => function ($query) {
$query->where('viewed', 0);
}
])->with(['users'])->get();

Aggregate functions sometimes treated not as expected by Eloquent. I suggest using selectRaw('count() FILTER (WHERE)') instead.

Related

Multiline Eloquent query

I'm trying to filter my products based on selected filters and possibly a search term/word. My filters have a relationship with categories, which in their turn have a relation ship with my products. My code below only works (without the if statement checking for a search term/word) when everything is chained together, but when I try to break the query into multiple lines (which I've read is possible, right?) it returns an empty array.
Here's a my code:
// Create array from selected categories/filters
$filter_ids = explode(',', $request->get('cats'));
// Query for active products
$products = Product::where('active', '=', 1);
$products->with(['categories' => function($query) use ($filter_ids) {
// Query for active categories
$query->where('active', 1)->whereHas('filters', function ($query) use ($filter_ids) {
// Query for the selected filters from the request
$query->whereIn('id', $filter_ids);
});
}]);
// Check for search term/word
if ($request->get('q')) {
$q = $request->get('q') ? urldecode($request->get('q')) : null;
$products->where('title', 'LIKE', "%{$q}%");
}
// Limit to 10 items and get results
$products->limit(10)->get();
return response()->json([
'status' => 'success',
'response' => $products
], 200);
I think you could but don't need to query all products with title first, before adding the relationships. But whats wrong here is that you must store the result of get() in a variable before adding it to your json response body:
Try to do something like:
if ($request->get('q')) {
$q = $request->get('q') ? urldecode($request->get('q')) : null;
$products->where('title', 'LIKE', "%{$q}%");
}
$products->with(['categories' => function($query) use ($filter_ids) {
// Query for active categories
$query->where('active', 1)->whereHas('filters', function ($query) use ($filter_ids) {
// Query for the selected filters from the request
$query->whereIn('id', $filter_ids);
});
}]);
$response = $products->limit(10)->get();
return response()->json([
'status' => 'success',
'response' => $response
], 200);
Lukas' answer led me to do some more debugging and eventually solving my problem, though it was not the position of the if statement checking if there's a search term/word.
The problem lies in the following line:
$products->limit(10)->get();
I needed to store the retrieved results from the get(); method in another variable, in my case:
$response = $products->limit(10)->get();
I eventually ended up with the following working code:
// Create array from selected categories/filters
$filter_ids = explode(',', $request->get('cats'));
// Query for active products
$products = Product::where('active', '=', 1);
$products->with(['categories' => function($query) use ($filter_ids) {
// Query for active categories
$query->where('active', 1)->whereHas('filters', function ($query) use ($filter_ids) {
// Query for the selected filters from the request
$query->whereIn('id', $filter_ids);
});
}]);
// Check for search term/word
if ($request->get('q')) {
$q = $request->get('q') ? urldecode($request->get('q')) : null;
$products->where('title', 'LIKE', "%{$q}%");
}
// Limit to 10 items, get results and store in '$response'
$response = products->limit(10)->get();
return response()->json([
'status' => 'success',
'response' => $response
], 200);

Multiple conditional where clauses not working in Eloquent query

I have a function that is supposed to use a combination of filters set by the user to fetch information from a table (or no filters at all).
Keep in mind I'm not trying to override filters here, the query should dynamically add or remove filters depending on the ones set client-side by the user.
The filters work well individually, but when setting multiple of them, the query doesn't fetch the correct data at all. I'm still unable to figure out what the criteria for the incorrect data being fetched is.
The only filters that seem to work in combination with others are date_from and date_to.
This is the function:
public function getReport(Request $request)
{
// Convertir fechas a formato correcto
$request->date_from = date('Y-m-d', strtotime(str_replace('-', '/', $request->date_from)));
$request->date_to = date('Y-m-d', strtotime(str_replace('-', '/', $request->date_to)));
$query = Ticket::all();
$query->when(isset($request->date_from), function ($q) use ($request) {
return $q->where('date_created', '>=', $request->date_from);
});
$query->when(isset($request->date_to), function ($q) use ($request) {
return $q->where('date_created', '<=', $request->date_to);
});
$query->when(isset($request->user), function ($q) use ($request) {
return $q->where('user', $request->user);
});
$query->when(isset($request->paid), function ($q) use ($request) {
if ($request->paid === true) {
return $q->whereNotNull('paid');
} else {
return $q->whereNull('paid');
}
});
$query->when(isset($request->informed), function ($q) use ($request) {
return $q->where('informed', $request->informed);
});
$query->when(isset($request->endorsement), function ($q) use ($request) {
if ($request->endorsement === true) {
$query->where('policy', '<', 1);
} else {
$query->where('policy', '>', 1);
}
});
$query->when(isset($request->duplicate), function ($q) use ($request) {
return $q->where('duplicate', $request->duplicate);
});
$query->when(isset($request->active), function ($q) use ($request) {
return $q->where('active', $request->active);
});
return response()->json($query, $this->success_status);
}
I've tested the $request payload multiple times and there's no issues with the information coming client-side.
What am I doing wrong?
You can try given solution for you problem.
$query = Ticket::all();
if ($request->filled('user')) {
$user = $request->get('user');
$query->where(function ($query) use ($user) {
$query->where('user', $user);
});
}
Note: if it is working perfect then you can add all request parameter according.
First note:
$query = Ticket::all();
In this case $query is not Eloquent query but Laravel Collection.
Another note:
$request is not regular php object with properties, so you have to use ->has(), ->filled() instead of empty() or isset()
Your ->data_to and ->data_from are working, cause you have overwritten those values in the beginning, so they become regular object properties.
Also, I would simplify some parts of your code:
$query->when(isset($request->duplicate), function ($q) use ($request) {
return $q->where('duplicate', $request->duplicate);
});
I would use this construction instead to save some lines:
$query->where('duplicate', $request->get('duplicate', false));
Second param of ->get is default value. If it is not present in request you will get false anyway. So you don't have to care about param existence.
You can apply same logic to all other filters as well.
PS: I don't know your requirements, but this part seems suspicious to me:
$query->when(isset($request->endorsement), function ($q) use ($request) {
if ($request->endorsement === true) {
$query->where('policy', '<', 1);
} else {
$query->where('policy', '>', 1);
}
});
This code will never give 'policy' === 1;

Filtering in Laravel using regex

I'm trying to filter products based on query string. My goal is to get products from a collection if it's given, otherwise get every product. Could someone help me what's wrong with the following code?
$products = \App\Product::where([
'collection' => (request()->has('collection')) ? request('collection') : '[a-z]+',
'type' => (request()->has('type')) ? request('type') : '[a-z]+'
])->get();
PS.: I've also tried with 'regex:/[a-z]+', it's not working...
$products = \App\Product::where(['collection' => (request()->has('collection')) ? request('collection') : 'regex:/[a-z]+'])->get();
What you can do is use when eloquent clause, so your where clause for collections will be triggered only when the request('collection') exists, same logis applie to type as well.
$products = \App\Product::
when(request()->has('collection'), function ($q) {
return $q->where('collection', request('collection'));
});
->when(request()->has('type'), function ($q) {
return $q->where('type', request('type'));
})
->get();
Or another way if you have your request values assigned to a variable something like:
$collection = request('collection');
$type= request('type');
$products = \App\Product::
when(!empty($collection), function ($q) use ($collection) {
return $q->where('collection', $collection);
});
->when(!empty($type), function ($q) use ($type) {
return $q->where('type', $type);
})
->get();

reduce database query to one and avoid Call to a member function load() on null error

I have this function:
public function show($id)
{
if (count($post = Post::find($id))) {
$post = $post->load(['comments' => function ($q) {
$q->latest();
$q->with(['author' => function ($q) {
$q->select('id', 'username');
}]);
}, 'user' => function ($q) {
$q->select('id', 'username');
}]);
$this->authorize('seePost', $post);
return view('post.show', ['post' => $post]);
} else {
dd('no post');
}
}
I added the if statement as if I try to open a route to a non existent post id I get the error Call to a member function load() on null.
However now I have two queries, one looks for the Post in the DB and if it finds one then I have to load the relations with the second one. What can I do to go back to just one query with all the relations loaded and avoid the error? Any clue?
You can use Constraining Eager Loads do it like this:
https://laravel.com/docs/5.8/eloquent-relationships#constraining-eager-loads
$post = Post::with(["comments" => function ($query) {
// Order by created_at, query comment author & select id, username
$query->latest()->with(["author" => function ($q) {
$q->select("id", "username");
}]);
}, "user" => function ($query) {
// Query post author & select id,username
$query->select("id", "username");
}])
// Fetch post or throw a 404 if post is missing
->findOrFail($id);
// You can also return an empty post instance like this if post is missing
// ->findOrNew([]);
// Or return the post or null if post is missing
// ->find($id);
// Authorize
$this->authorize('seePost', $post);
return view("post.show", ["post" => $post]);
Laravel has an Eager Loading feature that would be helpfull in your case. Eager Loading allows you to autoload relations along with the same query that you use to retrieve your main model info. https://laravel.com/docs/5.8/eloquent-relationships#eager-loading
You could a below codes.
Easiest way is :
$post = Post::with('comments.author', 'user')
->find($id);
Or fine tune query with callback :
$post = Post::with(['comments' => function ($q) {
// if you use comments select, then you need to specify foreign key too
$q->select('id', 'author_id', 'details') // comment fields
->latest(); // Use chaining method
// OR use $q = $q->latest();
},
'comments.author' => function ($q) {
$q->select('id', 'username'); // author fields
},
'user' => function ($) {
$q->select('id', 'username'); // user fields
}])
->find($id);
In some cases you might need some modifications, bu in overall that should avoid you N+1 queries problem.

Selecting required fields using relations in Laravel

I have a model Meetings like this:
public function meeting_comments(){
return $this->hasMany('App\MeetingsComments', 'meeting_id', 'id');
}
public function meeting_users() {
return $this->hasMany('App\UserMeetingDetails', 'meeting_id', 'id');
}
The Controller is like this:
$res = Meetings::with('meeting_comments', 'meeting_users')
->select('')->get()->toArray();
I only need comments from meeting_comments and user_id from meeting_users.
What do I put in select to only get the required fields from meeting_comments and meeting_users ??
You do it through a closure in the with call:
$res = Meetings::with(['meeting_comments' => function($query) {
$query->select('comments', 'meeting_id');
}, 'meeting_users' => function($query) {
$query->select('user_id', 'meeting_id');
}])
->get()->toArray();
I'm taking this from memory, so the syntax may be slightly incorrect, but it should work. :)

Categories