Take last 3 records from child. Php/Laravel - php

Help me please.
I'm trying to write a function where I get all the categories of my forum with the 3 most recently updated Topics in the given categories.
But according to the result, take(3) filters by id (where the id is not higher than 3), and I need to get the last 3 records.
public function index()
{
$forums = Category::with(['posts' => function ($q){
return $q->take(3)->get();
}])->get();
dd($forums);
}

you should order your complete query by update_at descending, only after you can take the first 3.
$q->orderBy('update_at', 'desc')->take(3)->get();
Your Categories table seems to be a different table from posts, so when a post is created or updated you should also set update_at of its category to now.

As far as I know you can not use take() or limit() inside with();
EDIT: solution that was selected by mr.Sardov is to use package staudenmeir/eloquent-eager-limit.
Link is provided below this answer.
So for you need to do is by limit it from model relationship.
For example:
class Category extends Model {
public function posts()
{
return $this->hasMany('App\Models\Post');
}
public function limitPosts()
{
return $this->hasMany('App\Models\Post')
->limit(3);
}
public function limitLatestPosts()
{
return $this->hasMany('App\Models\Post')
->orderBy('created_at', 'desc'). // or use ->latest()
->limit(3);
}
}
And that use it like this:
Category::query()
->with(['limitPosts' => function($query) {
$query->orderBy('created_at', 'desc'); // the last records
}])
->where('id', '<=', 3) // id not higher than 3
->get();
Or
Category::query()
->with('limitLatestPosts')
->where('id', '<=', 3) // id not higher than 3
->get();
Hope this can help you out.

Related

How to group on multiple fields including nested eager columns

I have some carts, products and users.
I try to get statistics like number of carts per user. And here are my models:
user.php
class User {
public function products()
{
return $this->hasMany(Product::class, 'user_id');
}
}
cart.php
class Cart {
public function products()
{
return $this->hasMany(Product::class, 'cart_id');
}
}
product.php
class Product {
public function user()
{
return $this->belongsTo('User', 'user_id');
}
public function cart()
{
return $this->belongsTo(Cart::class, 'cart_id');
}
}
Here is my query:
$query = Cart::query()
->whereHas('products', function ($query) use ($filter) {
$query->where('whatever', $filter);
})
->join('product', 'product.cart_id', '=', 'cart.id')
->join('users', 'users.id', '=', 'product.user_id')
->groupBy('users.id')
->select('users.id as user_id')
->select('users.name')
->selectRaw('count(cart.id) as numberOfCarts')
->selectRaw('SUM(price)')
->orderBy('users.name')
->get();
What I get is number of products per user, but I want number of carts per user. when I try to group using cart.id like groupBy(['user.id', 'cart.id']), it is worse: I get several times the same user and at each time the number of products in cart. And this total gives the same as total of number of carts get previously.
I have added jointure because I don't make it work by trying grouping on nested eager relationship. So I make it simplier.
How to fix grouping to count users carts and not their products ?
The issue is that count() counts the number of records returned by your MySQL query, so if you're looking at 20 rows all with the same cart.id, it's still going to count all 20 of them.
You can reduce this to what you want by only counting unique cart.ids with distinct:
$query = Cart::query()
->whereHas('products', function ($query) use ($filter) {
$query->where('whatever', $filter);
})
->join('product', 'product.cart_id', '=', 'cart.id')
->join('users', 'users.id', '=', 'product.user_id')
->groupBy('users.id')
->select('users.id as user_id')
->select('users.name')
->selectRaw('count(distinct cart.id) as numberOfCarts') // << Changed line
->selectRaw('SUM(price)')
->orderBy('users.name')
->get();

Get last records from relationship in Laravel

I have a one to many relation between Person and Visit tables like this:
public function visits()
{
return $this->hasMany('App\Models\Visit');
}
And want to get the persons who has a sickness_id of 1 in the relation like this:
$persons = Person::whereHas('visits', function ($query) {
$query->where('sickness_id', 1);
})->get();
And it works fine but I want to search just last visit of each person.
I mean if a person has two visits, one with sickness_id of 1 and other with sickness_id of 2, do not return this person because last visit is sickness_id of 2.
you can use hasOne relation for that:
public function lastVisit()
{
return $this->hasOne('App\Models\Visit')->latest();
}
then you can load it:
$persons = Person::whereHas('lastVisit', function ($query) {
$query->where('sickness_id', 1);
})->get();
I think the above answer is gonna work. Using addSelect, this might also work:
Person::addSelect('last_visit_id', Visit::select('id')
->whereColumn('person_id', 'persons.id')
->latest()
->limit(1)
)
->where('last_visit_id', 1)
->get();
$person = modelname::query()
->where(['sickness_id' => '1' ])
->select('visits')
->orderBy('id', 'DESC')
->first();

laravel eloquent complex select inside where statement

halo, i have data and want to display it like picture below
there are two models relationship, Person and Installment.
this is Person model:
class Person extends Model
{
protected $table = 'person';
public function angsuran()
{
return $this->hasMany(Installment::class);
}
}
this is Installment model:
class Installment extends Model
{
protected $table = 'installment';
public function person()
{
return $this->belongsTo(Person::class);
}
}
and this is my controller to querying and display data
$data = Person::with('angsuran')
->whereHas('angsuran', function ($q) {
$q->whereBetween('installment_date', [\DB::raw('CURDATE()'), \DB::raw('CURDATE() + INTERVAL 7 DAY')])
->where('installment_date', '=', function () use ($q) {
$q->select('installment_date')
->where('status', 'UNPAID')
->orderBy('installment_date', 'ASC')
->first();
});
});
return $data->get();
it show error unknow colum person.id in where clause
please help. thanks.
As the comment said, you need to put $q as a parameter to the Closure.
When using subqueries, it's useful to tell the query builder which table it is supposed to query from.
I've rewritten your query. It should achieve what you're looking for. Also, changed the CURDATE to Carbon objects.
today() returns a datetime to today at 00:00:00 hours. If you need the hours, minutes and seconds, replace today() by now().
$data = Person::with('angsuran')
->whereHas('angsuran', function ($subquery1) {
$subquery1->where('installment_date', function ($subquery2) {
$subquery2->from('installment')
->select('created_at')
->where('status', 'UNPAID')
->whereBetween('installment_date', [today(), today()->addWeeks(1)])
->orderBy('installment_date')
->limit(1);
});
});
Using with and whereHas you will end up with two query even if you have limit(1) in your subQuery and the result will show all 4 installment related to the person model. also I don't think you can order on the subquery, it should be before the ->get
so here's i've rewritten your code
$callback = function($query) {
$query->whereBetween('installment_date', [today(), today()->addDays(7)])
->where('status', 'UNPAID')
->orderBy('installment_date');
};
$data = Person::whereHas('angsuran', $callback)->with(['angsuran' => $callback])->get();
or you can use query scope. please see this answer Merge 'with' and 'whereHas' in Laravel 5

Laravel withCount() returns wrong value

In my application there are users making pictures of items. The relationship structure is as follows:
Categories -> SubCategories -> Items -> Pictures -> Users
Now, there's also a shortcut relationship called itemPics between categories <--> pictures so that the number of pictures uploaded by a user can be quickly counted per category using withCount().
These are the relationships on the Category model:
public function subcategories()
{
return $this->hasMany('App\SubCategory');
}
public function items()
{
return $this->hasManyThrough('App\Item', 'App\SubCategory');
}
public function itemPics()
{
return $this->hasManyThrough('App\Item', 'App\SubCategory')
->join('pictures','items.id','=','pictures.item_id')
->select('pictures.*');
}
My problem is getting the number of pictures that a user has gathered per category. The itemPics_count column created by withCount() always has the same value as items_count, even though the number of related models for both relations given by with() are different in the JSON output.
$authorPics = Category::with(['SubCategories', 'SubCategories.items' => function ($q) use ($author_id) {
$q->with(['pictures' => function ($q) use ($author_id) {
$q->where('user_id', $author_id);
}]);
}])
->with('itemPics') /* added this to check related models in output */
->withCount(['items','itemPics'])
->get();
dd($authorPics);
This does not make sense to me. Any help is greatly appreciated.
Withcount() function is not work properly if relations include join. it works only table to table relations.
public function itemPics()
{
return $this->hasManyThrough('App\Item', 'App\SubCategory')
->select('pictures.*');
}
This solution was worked for me:
//...
public function itemPics()
{
return $this->hasManyThrough('App\Item', 'App\SubCategory');
}
Then you can do something like this:
$authorPics = Category::with(['SubCategories', 'SubCategories.items' => function ($q) use ($author_id) {
$q->with(['pictures' => function ($q) use ($author_id) {
$q->where('user_id', $author_id);
}]);
}])
->with('itemPics') /* added this to check related models in output */
->withCount(['items','itemPics' => function($query){
$query->join('pictures','items.id','=','pictures.item_id')
->select('pictures.*');
}])
->get();
dd($authorPics);
Link to more information about Laravel withCount function here https://laravel.com/docs/8.x/eloquent-relationships#counting-related-models

laravel eloquent sort by relationship

I have 3 models
User
Channel
Reply
model relations
user have belongsToMany('App\Channel');
channel have hasMany('App\Reply', 'channel_id', 'id')->oldest();
let's say i have 2 channels
- channel-1
- channel-2
channel-2 has latest replies than channel-1
now, i want to order the user's channel by its channel's current reply.
just like some chat application.
how can i order the user's channel just like this?
channel-2
channel-1
i already tried some codes. but nothing happen
// User Model
public function channels()
{
return $this->belongsToMany('App\Channel', 'channel_user')
->withPivot('is_approved')
->with(['replies'])
->orderBy('replies.created_at'); // error
}
// also
public function channels()
{
return $this->belongsToMany('App\Channel', 'channel_user')
->withPivot('is_approved')
->with(['replies' => function($qry) {
$qry->latest();
}]);
}
// but i did not get the expected result
EDIT
also, i tried this. yes i did get the expected result but it would not load all channel if there's no reply.
public function channels()
{
return $this->belongsToMany('App\Channel')
->withPivot('is_approved')
->join('replies', 'replies.channel_id', '=', 'channels.id')
->groupBy('replies.channel_id')
->orderBy('replies.created_at', 'ASC');
}
EDIT:
According to my knowledge, eager load with method run 2nd query. That's why you can't achieve what you want with eager loading with method.
I think use join method in combination with relationship method is the solution. The following solution is fully tested and work well.
// In User Model
public function channels()
{
return $this->belongsToMany('App\Channel', 'channel_user')
->withPivot('is_approved');
}
public function sortedChannels($orderBy)
{
return $this->channels()
->join('replies', 'replies.channel_id', '=', 'channel.id')
->orderBy('replies.created_at', $orderBy)
->get();
}
Then you can call $user->sortedChannels('desc') to get the list of channels order by replies created_at attribute.
For condition like channels (which may or may not have replies), just use leftJoin method.
public function sortedChannels($orderBy)
{
return $this->channels()
->leftJoin('replies', 'channel.id', '=', 'replies.channel_id')
->orderBy('replies.created_at', $orderBy)
->get();
}
Edit:
If you want to add groupBy method to the query, you have to pay special attention to your orderBy clause. Because in Sql nature, Group By clause run first before Order By clause. See detail this problem at this stackoverflow question.
So if you add groupBy method, you have to use orderByRaw method and should be implemented like the following.
return $this->channels()
->leftJoin('replies', 'channels.id', '=', 'replies.channel_id')
->groupBy(['channels.id'])
->orderByRaw('max(replies.created_at) desc')
->get();
Inside your channel class you need to create this hasOne relation (you channel hasMany replies, but it hasOne latest reply):
public function latestReply()
{
return $this->hasOne(\App\Reply)->latest();
}
You can now get all channels ordered by latest reply like this:
Channel::with('latestReply')->get()->sortByDesc('latestReply.created_at');
To get all channels from the user ordered by latest reply you would need that method:
public function getChannelsOrderdByLatestReply()
{
return $this->channels()->with('latestReply')->get()->sortByDesc('latestReply.created_at');
}
where channels() is given by:
public function channels()
{
return $this->belongsToMany('App\Channel');
}
Firstly, you don't have to specify the name of the pivot table if you follow Laravel's naming convention so your code looks a bit cleaner:
public function channels()
{
return $this->belongsToMany('App\Channel') ...
Secondly, you'd have to call join explicitly to achieve the result in one query:
public function channels()
{
return $this->belongsToMany(Channel::class) // a bit more clean
->withPivot('is_approved')
->leftJoin('replies', 'replies.channel_id', '=', 'channels.id') // channels.id
->groupBy('replies.channel_id')
->orderBy('replies.created_at', 'desc');
}
If you have a hasOne() relationship, you can sort all the records by doing:
$results = Channel::with('reply')
->join('replies', 'channels.replay_id', '=', 'replies.id')
->orderBy('replies.created_at', 'desc')
->paginate(10);
This sorts all the channels records by the newest replies (assuming you have only one reply per channel.) This is not your case, but someone may be looking for something like this (as I was.)

Categories