Laravel sort child relationship collection - php

I have the following 3 tables:
Movie
- id
Series
- id
- status_id
- movie_id
Status
- id
- order_number
this is the code on the controller:
$movie = Movie::where('slug', $slug)->with(['series'])->first();
this is the code for view:
#foreach ($movie->series as $series)
{{ $series->name}}
#endforeach
how to sort $movie->series based on status->order_number? if it can be written on the model, so every order is only written once the controller?
is there a code that I can use for example like this:
$movie->series->sortBy('status.order_number');

Yes, but you will need to join status with series:
$movie = Movie::where('slug', $slug)->with([
'series' => function ($query) {
// Subquery on `series` table
return $query
// select whatever you'll need
->select('series.*')
// join with status
->join('status', 'series.status_id', '=', 'status.id')
// order by order number
->orderBy('status.order_number')
// * you can drop this if you select all the fields you need and use those
->with('status');
},
])->first();
Edit this ^ method will sort on SQL level, but you could also do this with collections:
#foreach ($movie->series->sortBy('status.order_number') as $series)
{{ $series->name}}
#endforeach
In that case also add .status to your with to avoid n + 1 problem: ->with(['series.status'])
The reason your attempt didn't work is because ->sortBy(..) doesn't mutate the collection, it just returns a new sorted one. This would work:
$movie->series = $movie->series->sortBy('status.order_number');

Related

Laravel orderBy with child relationship and parent at the same time

I have a list with messages. Its possible to reply to these messages (parent - child). I do not show child-messages in the list.
How can I always display the newest parent-message on top. Newest means that either the parent OR one of the childern has the newest timestamp.
Here is my eloquent query:
Message::withCount(['childMessages as latest_child_message' => function($query) {
$query->select(DB::raw('max(created_at)'));
}])
->orderByDesc('latest_child_message')
->orderByDesc('created_at')
->get();
Both orderBy should somehow be combined. Otherwise either the parent or the child sort will be prioritised.
In the context it's not possible to sort the collection after the DB-query.
edit 1:
Since "ee" is the latest response (child), the "bb" message should be at the bottom of the list.
edit 2:
The query will be used in a function returning a query
public static function getEloquentQuery(): Builder {
$query = parent::getEloquentQuery();
return $query->doTheMagicHere();
}
edit 3
This would be a working query.. but it's very slow
SELECT
id,
comment,
(SELECT MAX(cc.id) FROM comments cc WHERE cc.commentable_id = c.id) AS child_id
FROM
comments c
WHERE
commentable_type NOT LIKE '%Comment%'
ORDER BY CASE WHEN child_id IS NULL
THEN id
ELSE child_id
END DESC
;
In the withCount closure you must set conditions.
Use this:
Message::with('childMessages')->get()->sortByDesc(function ($parent, $key) {
$child = $parent->childMessages()->orderBy('created_at', 'desc')->first();
return $child ? $child->created_at : $parent->created_at;
});
the orderBy way you need is a bit complicated. it's better to use sortByDesc method and sort data on collection.
I hope this works.

Get all the items NOT included in a giving order using Eloquent

I have the following tables:
orders:
- id
- date
item_order:
- order_id
- item_id
items:
- id
- desc
- price
Using Eloquent, how can I get all the items NOT included in a giving order (say, the order with id = 6)?
I'm trying to do relationships & subqueries, but without luck.
Thanks in advance.
Your question is not clear enough, Using Eloquent, how can I get all the items NOT included in a giving order (say, the order with id = 6)?, this sentence is unclear.
In other hand based on your explanation, below example gonna return list of orders with their items, except something you don't want:
$id = 6;
// Get list of orders with itemOrder.
Order::with('itemOrder' => function ($query) use $($id) {
//make sure query don't return list of order items belong to order id = 6.
$query->where('order_id', '!=,' $id)
})->get();
You will get list of orders with item_order but you will not get item_order for order with id of 6.
If this is not what you've asked please clarify and update your question for better explanation.
Based on your comment : Your solution gives me the ORDERS, but I want all the ITEMS, except those of the order with id = 6., you can simply replace query with specified model and relations
$id = 6;
Item::with('itemOrder' => function ($query) use $($id) {
$query->where('order_id', '!=,' $id)
})->get();
// Or,
// This one only return items with itemOrders and of course returned itemsOrder
// will be affected by query inside callback function
Item::whereHas('itemOrder' => function ($query) use $($id) {
$query->where('order_id', '!=,' $id)
})->get();

Laravel - Displaying fields with many to many relationship according field in pivot table

I have this database structure
table users table office_user table offices
----------- ----------------- -------------
id * id * id *
full_name user_id name
office_id
joined_at
So in my project every office has many users and user can be joined to many offices in date (joined_at)
User.php model
public function offices()
{
return $this->belongsToMany('App\Office)->withPivot('joined_at');
}
Office.php model
public function users()
{
return $this->belongsToMany('App\User)->withPivot('joined_at');
}
OfficeController.php
public function show(Office $office)
{
$users = User::with(array('phones', 'offices' , function($query)
{
$query->orderBy('joined_at', 'desc');
}))->get();
return view('dashboard.offices.show', compact(['office', 'users']));
}
I need two things :
1- Get current users list for every office
2- Count of current users in every office
I already achieve this:
<h3>{{ $office->name }}</h3><span>{{ $office->users->count() }}</span>
#foreach ($office->users as $user)
<li>{{ $user->full_name }}</li>
#endforeach
But the result is not as expected it gives me all users in certain office and count of them regardless there joined date
I want the list of last joined users to this office and count of them according joined_at field in pivot table
Thank you and Sorry for my english
But the result is not as expected it gives me all users in certain office and count of them regardless there joined date
When you do $office->users->count() that is the expected behavior because you are retrieve all the associated users of every office at any time, so given that you returned all this users, the count() executed in the collection will count all of them.
Your pivot attribute is just a timestamp, so how would you reduce the number of users returned? users that joined the office today/in the last hour/in the last 15 min maybe?
If so, you can add constrains to your count() method to get the results you want.
As an example, in the following lines we are gonna constraint the associated offices that has a joined_at that belongs to today:
public function show(Office $office)
{
$users = User::with([
'phones',
'offices' => function ($offices) {
$offices->whereDate('joined_at', '>', now()->startOfDay());
},
])->get();
return view('dashboard.offices.show', compact([office, 'users']));
}
Check this section of the documentation:
Constraining Eager Loads
Sometimes you may wish to eager load a relationship, but also specify
additional query conditions for the eager loading query. Here's an
example:
$users = App\User::with(['posts' => function ($query) {
$query->where('title', 'like', '%first%');
}])->get();
In this example, Eloquent will only eager load posts where the post's
title column contains the word first. You may call other query
builder methods to further customize the eager loading operation:
$users = App\User::with(['posts' => function ($query) {
$query->orderBy('created_at', 'desc');
}])->get();

Laravel 5.7 - "orderBy" Not Sorting Results

I have three models related through hasMany relationship: Course/Lesson/Article - A Course hasMany Lessons which hasMany Articles.
I have an int column in my articles table named pos (short for position) and I want to order the articles by it.
I am using the following query in my CoursesController, but it's not sorting the articles by this pos attribute:
Code:
public function show(Course $course, Lesson $lessons, Article $articles)
{
$articles = $lesson->articles()->orderBy('pos', 'asc')->get();
return view('users.courses.show', compact('course', 'lessons', 'articles'));
}
I'm using a foreach loop in blade:
#foreach($lesson->articles as $article)
{{ $article->title }}
#endforeach
Any help would be appreciated!
Laravel debugbar shows the following result:
select * from articles where slug = 'this-is-article-one' limit 1
13.27ms\vendor\laravel\framework\src\Illuminate\Routing\ImplicitRouteBinding.php:35
select * from articles where lesson_id = 1 and pos < 2 order by pos desc limit 1 660μs\app\Http\Controllers\Users\ArticlesController.php:55
select * from articles where lesson_id = 1 and pos > 2 order by pos asc limit 1 520μs \app\Http\Controllers\Users\ArticlesController.php:59
select * from courses where courses.id = 2 limit 1 610μs view::users.articles.show:7
select * from lessons where lessons.id = 1 limit 1 530μs
view::users.articles.show:8
select * from articles where articles.lesson_id = 1 and articles.lesson_id is not null
When you call $lesson->articles again in the show view, basically you make a new DB call. To get the correct values, use the variable $articles:
#foreach($articles as $article)
{{ $article->title }}
#endforeach
If you want to continue using your lesson object in your view, use sortBy:
#foreach($lesson->articles->sortBy('pos') as $article)
{{ $article->title }}
#endforeach
I will suggest to extend you relationship with orderBy()
<?php
public function articles(){
return $this->hasMany('App\Articles')->orderBy('pos','asc');
}

Laravel 5.3 - Pull data of polymorphic table in one query?

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.

Categories