I have 3 Models... Category, Post, Vote
When viewing a category, I am showing an index of all Posts in that category. So I'm doing something like foreach ($category->posts as $post) in my view.
The question I have now is how can I order the posts based on the sum of votes they have?
I have standard relationships setup, so that a post hasMany votes.
You can do it either by defining a helper relation on the Post model and sorting the collection after the relation is loaded OR simply by joining the votes and ordering in the query.
1 RELATION
// Post model
public function votesSum()
{
return $this->hasOne('Vote')->selectRaw('post_id, sum(votes) as aggregate')->groupBy('post_id');
}
// then
$category->posts->load('votesSum'); // load relation on the collection
$category->posts->sortByDesc(function ($post) {
return $post->votesSum->aggregate;
});
// access votes sum like this:
$category->posts->first()->votesSum->aggregate;
2 JOIN
$category->load(['posts' => function ($q) {
$q->leftJoin('votes', 'votes.post_id', '=', 'posts.id')
->selectRaw('posts.*, sum(votes.votes) as votesSum')
->groupBy('posts.id')
->orderBy('votesSum', 'desc');
}]);
// then access votes sum:
$category->posts->first()->votesSum;
You can use scope for that:
// Post model
public function scopeOrderByVotes($query)
{
$query->leftJoin('comments','comments.post_id','=','posts.id')
->selectRaw('posts.*,sum(comments.id) as commentsSum')
->groupBy('posts.id')
->orderBy('commentsSum','desc');
}
// then
$category = Category::with(['posts' => function ($q) {
$q->orderByVotes();
}])->whereSlug($slug)->firstOrFail();
Related
I am trying to get all the products with active prices and display them and the active price using a scope in the ProductPrices Model. My Products Model has an hasMany relation with prices:
Product.php (Model)
public function prices () {
return $this->hasMany(ProductsPrice::class);
}
My Prices Model has an scope is active that checks if the prices is active on this date:
ProductsPrices.php (Model)
public function scopeIsActive($query)
{
return $query->whereRaw(' timestampdiff(second, start_time, NOW()) >= 0')
->where(function ($query) {
$query->whereRaw(' timestampdiff(second, end_time, NOW()) <= 0')
->orWhereNull('end_time');
});
}
I tried many different ways to get products with an active price and display them both. Things I feel should work, but don't, are:
Route::get('/test', function (Request $request) {
return Product::join('products_prices', 'products.id', 'products_prices.product_id')
->prices->isActive()
->where('products.is_active', true)
->get();
});
I get the error:
Property [prices] does not exist on the Eloquent builder instance.
or test2
Route::get('/test2', function (Request $request) {
$prices = DB::table('products_prices')->select('id');
$product = Product::whereIn('id', $prices)->get();
return $product->prices()->isActive()->get();
});
I get the error:
Method Illuminate\Database\Eloquent\Collection::prices does not exist.
Why can't I access the ->prices() on my Product Model? Should I not use eloquent for this and go for the Query Builder of Laravel?
I think a combination of with and whereHas might work:
$products = Product::with(['prices' => function($query) {
$query->isActive(); // to fetch prices relation that is active
}])->whereHas('prices', function($query) {
$query->isActive(); // this one is to filter the products that has active price
})->get();
I have an orders table, an items table, and a pivot table called item_order which has two custom fields (price, quantity). The relationship between Order and Item is belongsToMany. I'm trying to return the count of all items with an id of 1, where the parent Order->status == 'received'. For the life of me, I can't figure out how to do this.
class Order extends Model
{
public function items()
{
return $this->belongsToMany(Item::class)->withPivot('price', 'quantity');
}
}
class Item extends Model
{
public function orders()
{
return $this->belongsToMany(Order::class)->withPivot('price', 'quantity');
}
}
Try this:
$total_quantity = Item::find(1) // getting the Item
->orders() // entering the relationship
->with('items') // eager loading related data
->where('status', '=', 'received') // constraining the relationship
->get() // getting the results: Collection of Order
->sum('pivot.quantity'); // sum the pivot field to return a single value.
The strategy here is to find the desired Item to then get the related Orders to this Item that has a 'received' status, to finally sum the pivot attribute in order to get a single value.
It should work.
Considering you know the id of the item, most performant way would be to query item_order table directly. I would create a pivot model for ItemOrder and define the belongsTo(Order::class) relationship and do this:
$sum = ItemOrder::where('item_id', $someItemId)
->whereHas('order', function ($q) {
return $q->where('status', 'received');
})
->sum('quantity');
Using the polymorphic relationship likeable, I have setup authors and books as likeable_type in likeable_items table.
Here are the models:
class Like extends Model {
public function likeable(){
return $this->morphTo();
}
}
class Author extends Model {
public function likes(){
return $this->morphMany('App\Like', 'likeable');
}
}
class Book extends Model {
public function likes(){
return $this->morphMany('App\Like', 'likeable');
}
}
I want to use one efficient query to pull them both in with their respective data, paginated by 10, something like this does not work (I commented the code to show what is needed in each step).
$likeableData =
DB::table('likeable_items')
// We want to fetch additional data depending on likeable_type
->select(['books.title', 'books.author_name', 'book_counts.like_count']) // when likeable_type = 'book'
->select(['authors.name', 'authors.country', 'authors.age', 'author_counts.like_count']) // when likeable_type = 'author'
->leftJoin('books', 'books.id', '=', 'likeable_items.likeable_id') // when likeable_type = 'book'
->leftJoin('book_counts', 'book_counts.book_id', '=', 'likeable_items.likeable_id') // when likeable_type = 'book'
->leftJoin('author_counts', 'author_counts.author_id', '=', 'likeable_items.likeable_id') // when likeable_type = 'author'
// We want to have distinct results, based on unique id of book/author
->distinct()
// We want to order by the highest like_count, regardlress of likeable_type
->orderBy('book_counts.like_count', 'desc') // order by highest like_count when likeable_type = 'book'
->orderBy('author_counts.like_count', 'desc') // order by highest like_count when likeable_type = 'author_counts'
// We want to paginate the mixed results
->paginate(10);
return $likeableData;
How can I get the mixed results back of the highest liked author/book by likes_count, with their respective data, paginated by 10?
UPDATE:
Here is the table schema:
Table: likeable_items
- id
- likeable_id (book_id or author_id)
- likeable_type (book or author)
Table: books
- id
- title
- author_name
Table: book_counts
- book_id
- like_count
Table: authors
- id
- name
- country
- age
Table: author_counts
- author_id
- like_count
You could do it from your Like model without the need for the counts tables with some sub selects and grouping like:
$likes = Like::select('*')
->selectSub('COUNT(*)', 'total_likes')
->with('likeable')
->whereIn('likeable_type', [Book::class, Author::class])
->groupBy('likeable_type', 'likeable_id')
->orderBy('total_likes', 'desc')
->paginate(10);
Then to access the values:
foreach($likes as $likedItem) {
$likedItem->total_likes;
if($likedItem->likeable_type === Book::class) {
// Your book logic here
$likedItem->likeable->title;
$likedItem->likeable->author_name;
} else {
// Your author logic here
$likedItem->likeable->name;
$likedItem->likeable->country;
$likedItem->likeable->age;
}
}
If you are willing to change the schema and have more than one query a possible solution would be:
Update Schema:
Table: likeable_items
- id
- likeable_id (book_id or author_id)
- likeable_type (book or author)
- like_count
Table: books
- id
- title
- author_name
Table: authors
- id
- name
- country
- age
Update Model:
class Like extends Model
{
/**
* #return bool
*/
public function isBook()
{
return ($this->likeable_type === Book::class);
}
/**
* #return bool
*/
public function isAuthor()
{
return ($this->likeable_type === Author::class);
}
}
Except you need book_counts and author_counts for a special piece of logic on your system, I would just remove them and move like_count to likeable_items.
like_count is a property common to books and authors so I think that it's reasonable to place it on likeable_items.
Querires:
$likes = Like::orderBy('like_count', 'desc')
->paginate(10);
$likeable_id = $likes->pluck('likeable_id');
$books = Book::whereIn('id', $likeable_id)->get()->groupBy('id');
$authors = Author::whereIn('id', $likeable_id)->get()->groupBy('id');
So, at this point get the best 10 is trivial.
To fetch the data instead we use pluck and then groupBy with whereIn.
About this point, please consider that if you are using likeable_items to dispatch the id for books and authors the two whereIn will return just the data you need.
Instead, if you use likeable_items just to cache books and authors ids, you will potentially have some data you don't need.
How to display the content:
#forelse ($likes as $item)
#if ($item->isBook())
$books[$item->likeable_id][0]->title;
$books[$item->likeable_id][0]->author_name;
#else
$authors[$item->likeable_id][0]->name;
$authors[$item->likeable_id][0]->country;
$authors[$item->likeable_id][0]->age;
#endif
#empty
#endforelse
Final Considerations:
Using this approach you will:
- change your schema and update your model
- you won't use morphTo,
- you do 3 queries instead of 1 but are not complex
- you can directly use paginate
- you always use id for the secondary queries (which is good for performance)
The only "dark side" as I said is that you may have some content you don't need in $books and $authors in case you are not dispatching the ids from likeable_items but I think is a reasonable trade-off.
I have the usual users, groups and group_user tables. I know the raw SQL that I want:
SELECT group_user.group_id, users.* FROM users
INNER JOIN group_user ON users.id = group_user.user_id
WHERE group_user.group_id IN
(SELECT group_id FROM group_user WHERE user_id=?)
ORDER BY group_user.group_id;
where ? is replaced current user's id.
but, I want to use Eloquent (outside of laravel) for this. I have tried using a User model with a groups method
public function groups() {
return $this->belongsToMany('\Smawt\User\Group');
}
and a Membership model with a users method
public function users($group_id) {
return $this->where('group_id', '=', $group_id)->get();
}
and then I loop through the groups and then loop through all its members. Finally, I append all the data to get one $users object at the end, to pass through to my view.
$thisMembership = new Membership;
$myGroups = $app->auth->groups;
$users = [];
foreach ($myGroups as $myGroup) {
foreach ($thisMembership->users($myGroup->id) as $myUser) {
$thisUser = $app->user->where('id', '=', $myUser->user_id)->first();
$thisUser->group_id = $myGroup->id;
array_push($users, $thisUser);
}
}
Then in my view I loop through my $users as normal. Although this method works, it will not be very efficient as I am unable to work out how to use Eager Loading with it.
Is there a simpler more 'Eloquent' way of getting an object of users who are in the same group as the current user? I don't want just want a list, or an array, as I want to use the other methods defined in my user model.
I have been able to construct the following Eloquent query, although I am not sure this is the 'best' way:
$users = User::join('group_user', 'users.id', '=', 'group_user.user_id')
->whereIn('group_user.group_id', function($query) {
$query->select('group_id')
->from('group_user')
->where('group_user.user_id', '=', $_SESSION['user_id']);
})->orderBy('group_id', 'asc')
->get();
The Eloquent way for the relationship and use of it:
Tables: users, groups
Models: User Group
Pivot Table: group_user (id, user_id, group_id)
In User Model:
public function groups()
{
// pivot table name and related field
// names are optional here in this case
return $this->belongsToMany('Group');
}
In Group Model:
public function users()
{
// pivot table name and related field
// names are optional here in this case
return $this->belongsToMany('User');
}
Use Case (Example):
$usersWithGroup = User::with('groups')->find(1); // or get()
$groupWithUsers = Group::with('users')->find(1); // or get()
For more information check Eloquent section on documentation.
Update:
If user belongsto any group
$usersWithGroup = User::has('groups')->with('groups')->find(1);
Also using if a user belongs to specific group:
$someGroup = 'general';
$usersWithGroup = User::whereHas('groups', function($q) use($someGroup) {
$q->where('group_name', $someGroup);
})
->with('groups')->find(1);
I have a main table, "help_pages" which stores id's of either articles or videos. There are two other tables: "articles" and "videos."
In the example below, $portalType is either 'reseller' or 'global' for the purposes of my application. All was well as long as I didn't have a flag on videos to be unpublished. Now I'm having trouble filtering the scope function by BOTH articles and videos that are published.
This is in my models/HelpPage.php:
public function scopeByPortalType($query, $portalType) {
return $query->where('portal_type', '=', $portalType)
->leftjoin('articles', 'content_id', '=', 'articles.id')
->where('articles.published', '=', '1');
}
what I wish I could do is just add a second
->leftJoin('videos', 'content_id', '=', videos.id')
->where('videos.published', '=', '0');
but this returns too many rows. I tried creating a temp table with a UNION for both articles and videos:
CREATE TEMPORARY TABLE IF NOT EXISTS tmp_assets AS
(SELECT id, 'Article' AS content_type FROM articles WHERE published = 1)
UNION
(SELECT id, 'Video' AS content_type FROM videos WHERE published = 1)
and then joining that, but no dice. This might be a smaller deal than I'm making it, but I'm lost now!
Laravel version 4.2
Well, it's not pefect, but it does what I need it to do:
public function scopeByPortalType($query, $portalType) {
$q = $query
->whereRaw("
(id IN (SELECT help_pages.id FROM help_pages INNER JOIN articles ON content_id=articles.id WHERE articles.published='1' AND content_type='Article')
OR
id IN (SELECT help_pages.id FROM help_pages INNER JOIN videos ON content_id=videos.id WHERE videos.published='1' AND content_type='Video'))")
->where('portal_type', '=', $portalType);
return $q;
}
I wish I could've figured out a way to do it using actual Eloquent, but every step led me down a rabbit hole with a dead end. This worked!
The Eloquent way to do this would be setting up the relationships so you could utilize eager loading.
Relationships: http://laravel.com/docs/4.2/eloquent#relationships
Eager Loading: http://laravel.com/docs/4.2/eloquent#eager-loading
Setup relations:
class HelpPage extends Eloquent {
public function articles()
{
return $this->hasMany('Article');
}
public function videos()
{
return $this->hasMany('Video');
}
}
Then you can utilize eager loading like this:
$help_pages = HelpPage::with(array('articles' => function($query)
{
$query->where('published', '=', 1);
},
'videos' => function($query)
{
$query->where('published', '=', 1);
}
))->get();
You might be able to then leverage whereHas and orWhereHas on the actual query rather than my example above.