Plan Nested Comments0:32
that you're responding to them. Well, wouldn't it be nice, like with Reddit or Discuss, if you could just reply to them directly? And then the other guy could do that too. And in this case, we have infinite threading. Second place is no place. You get the basic idea. How exactly do we set this up? We need some way to specify the parent for a Comment. For example, this Comment has a parent of that Comment, and that Comment has a parent of that Comment. And then finally, this root level Comment has no parent. Let me show you. Let's start from our routes file, because I have a bit of setup to show you so that we can get started making the comments nestable.
Let me show you. Let's start from our routes file, because I have a bit of setup to show you so that we can get started making the comments nestable. Okay, so when you visit a particular post, we will track that down behind the scenes, and then we will load a show view and pass through the post. And then here, very, very simple stuff. So if we come back, I have a new project. And I'm sorry, let's go to post/1. We echo out the post, and we show the comments. Now, I do have some comments in the database. So if I were to say app\Comment::all(), you'll see that, yeah, we have two in this case, each which belongs
Add Parent ID Field1:36
So if I were to say app comment all, you'll see that, yeah, we have two in this case, each which belongs to a User and a post ID. But I want you to notice that I've added this parent ID. So if you're working along, here's what your comments table may look like. A Comment belongs to a User, a Comment belongs to a Post, and also a Comment can optionally be assigned a parent ID. And that will be the ID of the Comment that this Comment is in response to. And then finally, we have the body of the Comment. And of course, for the Post table, just very simple. Title, body, and a Post belongs
And then finally, we have the body of the Comment. And of course, for the post table, just very simple. Title, body, and a Post belongs to a User. Okay, so why don't we go ahead and display these comments like this. For each post comments as comment, then we'll just use like an article here where we echo out the body of the comment. And then, I don't know, maybe the comment's username, maybe something like that. So right now, this is all going to blow up because we don't have any of these relationships. And further, that's not why I'm expecting a blow up.
Define Model Relationships2:40
So right now, this is all going to blow up because we don't have any of these relationships. And further, that's not why I'm expecting a blow up. Oop, two up signs. Anyways, there's the one we want. Invalid argument supplied. Right now, this relationship doesn't exist. So a Post consists of many Comments. So hasMany Comment class. That should change the error. Okay, now we're trying to get properties of a non-object. And that's because here, once again, there's no relationship between a Comment and its owner. So a Comment belongs to an owner.
once again, there's no relationship between a Comment and its owner. So a Comment belongs to an owner. Return this belongs to a User. Okay, and we'll set user_id as the foreign key. So if we give this a refresh, there we go. Very quickly, we set up some Comments. But now, we want to have a little reply button below it. Right? So maybe, I don't know, it could go right here. And we could say, give me a form that we'll post to for now, just comments. Or maybe post slash and then the post_id and then comments. You know, you can format that
for now, just comments. Or maybe post slash and then the post ID and then comments. You know, you can format that however you want. Okay, so we'll say reply here. Come back, refresh. We have our reply button. And maybe we also want a textarea here. And this will be for the body of your comment or your reply. Okay, it doesn't look very good at all. You can style this however you want. But I want to get back to it. At this point, we have two comments. And I can reply to each of them, but I can't leave a top level reply. And also on that note, why don't we swap this out for a list item? Just so we can see
refresh, yeah, it's starting to look a little better. But I really hate that I'm having to repeat this form. So instead, why don't we grab this and extract it to something like comments/form. Alright, let's create that. resources/views/comments/form, and I'm going to paste that within here. Okay, so if we come back, it still works. But now I can take this and replace it up here as well. And we still get the same thing. So before we get to the nesting, why don't we just start with a top level Comment? So when we fill out the comments form, it is going to post
Create Top-Level Comments5:20
get to the nesting, why don't we just start with a top level Comment? So when we fill out the comments form, it is going to post to this endpoint. So why don't we set that up? We want to listen for a post to post/posts and then comments. That's going to accept the post. And we want to do something like postComments::create. Why don't we stick with create? Where the body of the comment is the request body. So yeah, we're just grabbing that right here. Now the only other thing we would need is the userId.
request body. So yeah, we're just grabbing that right here. Now the only other thing we would need is the userId. We can hard code that. Get the authenticated user. And why don't we sign somebody in? This, of course, is temporary. In real life, you'll have a registration system. Okay, so when we hit this endpoint, and on that note, let's add the middleware of auth. You must be signed in to hit this. So when you hit this endpoint and you give us a post, we will delegate to the comments relationship right here. And we're going to say create a new one where the body equals what the user typed in and the userId.
comments relationship right here. And we're going to say create a new one where the body equals what the user typed in and the user_id is the id of the authenticated user. Okay, so if we give this a refresh, we'll do a third reply and we have undefined variable post. So let's see what we have here. Ah, yeah, sorry. Post. Give it a refresh. Mass assignment exception. Now in this case, that's because yeah, we're trying to create a Comment and mass assign those, but Laravel is protecting us against that. So we can come back here and be rebellious and set guarded to nothing, or you can set the fillable fields and be
protecting us against that. So we can come back here and be rebellious and set guarded to nothing, or you can set the fillable fields and be explicit. So we could say the fillable fields are the body and the user_id. Anyways, if we come back and give that a refresh, we didn't redirect anywhere, but if we come back, sure enough, we can see that right here. So let's just make sure that after we do this, we return back. So now I could say fourthReply. We add the comment and we redirect back. So now we can successfully do top-level comments, but now we're going to do nested comments. So how do we do that?
Post Replies With Parent7:28
So now we can successfully do top-level comments, but now we're going to do nested comments. So how do we do that? So let's do this. Let's send through the parent, and that will be the commentId here, and then within here, if parent is available, so we could say if isset(parent), then we know that we are replying to another comment. And in that case, we could just set up a hidden input called parentId. The type will be hidden and the value will be parent. So that would be the ID of the parent. And in fact, why don't we make that a little more explicit?
and then this one is replying to the parent with an ID of 1. So now, when we post that form, we will hit our web routes file. We could set the parent ID equal to request parentId, with the understanding that if one is not available, that will default to null. And you can even be explicit if that helps. But now, we would have to update Comment to make that fillable as well. I don't like too many of those IDs to be fillable. So what you could do, of course, is just say comment. We're going to build up a comment. We're going to pass through the body. ... And then we'll say, and you can just
Okay, so, if we come back and try this out, reply to the fourth reply, and we run it. Yes, it got saved to the database. Don't worry, we're not filtering or sorting anything, so we should not see it indented yet. But, if we do fetch all comments, you can see, take a look, all the parent IDs are null except for this newest one, where the parent ID is set to 4. And notice, this comment is 4, and that is exactly the one we want to respond to. So now, at this point, yeah, we basically have everything set up on the database end. All we have to do now is make sure that the comments are sorted or filtered in the proper
Group and Render Recursively10:08
have everything set up on the database end. All we have to do now is make sure that the comments are sorted or filtered in the proper order, and I'll show you how to do that. Let's start by just reviewing some output. So, let's grab the comments here, give that a refresh, and we'll get some JSON. I have the JSON, what's it called, JSON formatter extension installed that makes this look really nice. But yeah, we can just see the JSON output for everything. However, what if we grouped them according to the parent_id? So, fetch a collection of all the comments. So, we start by getting all of the comments. But then, using Illuminate\Support\Collection, that class,
of all the comments. So, we start by getting all of the comments. But then, using Illuminate\Support\Collection, that class, we're going to group them according to the parent_id. So, that's going to group all the results into little collections, like this. So, take a look. The key for each item is now the id of the parent. So, parent_id 4, any of those will be stored here. If you have a parent_id of 3, like if the user responds to, how about this one, then that would then become its own collection. However, for any items that have no parent or null, in effect, those will be grouped together as well.
collection. However, for any items that have no parent or null, in effect, those will be grouped together as well. So, that means we could do this. We could say, give me all of the root level items. So, this will be all of the Comments that do not have replies. And it would be nice if we could just rewrite that. So, for example, if we did something like this, we could say, $comments, and then say something like $comments $root = $comments that. All that we are doing in this case is just changing the key of the array. But anyways, just to make it a little more readable for you, refresh. Okay, so
is just changing the key of the array. But anyways, just to make it a little more readable for you, refresh. Okay, so grouped into those with the parentId of 4, and then grouped into those that are root level comments. Okay, so now this is starting to work like we would want. So, if I do the thing where we reply to the third reply, okay, let's bring that back, refresh. Now, you'll see that we get a new collection of all the items with a parent of 3. So, at this point, we basically have our tree structure. We can just use a little bit of recursion to filter through and display all of the results, like
So, at this point, we basically have our tree structure. We can just use a little bit of recursion to filter through and display all of the results, like this. Let's, actually, we're going to do this in a couple ways, but to start, let's just pass through the compact post and comments. Okay, but anyways, we'll go to the post page. We're going to filter through our comments, but specifically the root level comments. Okay, so did we delete that? No, we didn't. Anyways, if we come back to Chrome and give this a refresh, now, yeah, notice that you're not seeing any of the replies that we made.
you want to check to see if the Comment has replies itself. And, actually, I'll tell you a little secret. If you're only interested in one level of indentation, so, like, anyone can reply to this top level Comment. But then, you can't reply to the next one. They can't reply to the third and the fourth. You don't want infinite nesting. You just want one level of nesting. Well, if you want to do that, then you can do it pretty cleanly. You could say, on the Comment model, you could set up a relationship called replies, where you basically say that a Comment can have many of itself, like this. However, the foreign key will be the parent_id itself.
comment can have many of itself, like this. However, the foreign key will be the parent_id itself. So, now, think about it. You could get a PostComment, but then, if the very first comment has a bunch of nested replies, then you could do something like this to get a collection of that. And this is why I say it really needs to be one level of indentation deep. And that's because we can't eager load multiple levels down. So, if you tried to do that, everything would work, but you would end up with that n plus 1 problem, where you keep having to make new queries. Because, for example, the first reply could have replies of its own.
n plus 1 problem, where you keep having to make new queries. Because, for example, the first Reply could have replies of its own. And so, this is what I mean by that n plus 1 problem. But, nonetheless, for one level of indentation, that works really good. But, in our case, we want to keep the database queries down to one for the Comments. So, here's what we're going to do. We could say down here, notice, this is a single Reply. So, now, I want to say, does this Comment have replies of its own? So, we could do that like this. If isset, get all the comments, and we're going to look for any replies to the current comment. Now, this is kind of
If is set, get all the comments, and we're going to look for any replies to the current comment. Now, this is kind of confusing. Even as I said that, that sounds confusing. So, stick with me. Let me go ahead and finish this up, and then we'll talk over it. Without making this recursive, I would, yeah, I'd have to do something like this, where I would say, add an unordered list here, and then filter down, and notice I'm just kind of repeating the code, and you never actually finish it, because now, down here, you're in that same boat, where you think, well, this comment has a reply, but that reply has its own replies, so you keep having to re-indent yourself, and that's just not the way
well, this Comment has a reply, but that reply has its own replies, so you keep having to re-indent yourself, and that's just not the way you want to go about it. So, instead, let's extract some partials here, so that we can load these recursively, like this. I'm going to take this entire Comment. This entire list item represents a Comment, so I'm going to extract that. Include comments.comment, and create that, and paste that in here. Next, we're back on our Post view. All of this section right here, that is the list of comments. So, let's do comments.list,
All of this section right here, that is the list of comments. So, let's do comments.list, and create that, like so. Okay, so take a look. We fetch a list of comments, and then for there, well, right now, we're assuming that we're filtering through the root comments, but we can make that as general as possible. But anyways, right now, we filter through each root level comment, and we display the comment. This is how we render it, but then down here, this is the section where we have to say, well, if this comment has sub-comments, or replies.
return the comments here. Back to Chrome, just so you don't forget. So, what we're saying here, let's go back. So, here's our full collection of comments. That's all of this. And what we're saying is, if we have a key in this comments collection called, let's say, 3, this is the ID of the current comment. Well, do we have one? Yes. That means the comment with an ID of 3 has nested children. So, if that's the case, then display a list of those children. That's all we're doing here. So, why don't we try this? Include comments.list. But now, here's where we get to that part where we
here. So, why don't we try this? Include comments.list. But now, here's where we get to that part where we can no longer assume a collection. So, maybe I can just do this. And then, here, we can decide what that will be equal to. So, in this case, that will be equal to the root level comments. So now, our list filters through each root level. But then, here, we have sub-level comments. So, those will be comments. And then, we'll reference this sub-collection again. Okay? So, we're basically passing this collection of items to the list. The list is now going to filter through that collection.
file. Get rid of that. Refresh. And it does fail. Trying to get property of non-object. Oh, and you know what? I already know what it is. So, here, we're trying to reference all of the comments. But then, we rewrote that. So, yeah, of course, this isn't going to work. So, let's do this. This is going to filter through a collection of comments. So, now, down here, the collection will be the root level comments. But any sub-level comments will change that. So, now, comments, this variable is always going to refer to basically all comments. You could even name it that way if it helps you. Okay. Come back. Refresh. And there you go. And notice we're starting
basically all comments. You could even name it that way if it helps you. Okay. Come back. Refresh. And there you go. And notice we're starting to get our nested comments. A reply to the reply to the third reply. And now, if we come back, third reply, here's one reply to that, and then another reply to that. And that's it. It's not that complicated. And even better, if we come back to Sublime, let's go to my AppServiceProvider. I have this logQuery snippet I use. So, listen for any database query, and then we'll say logInfo. So, we will log the SQL as well as the bindings. So, now,
Eager Load to Avoid N+119:44
database query, and then we'll say log info. So, we will log the SQL as well as the bindings. So, now, those will be logged to Laravel.log. Okay. So, if I give this a refresh, right now, we're going to get a bunch here. And that's because we're not eager loading it. So, for example, select * from users where id = ?, that's because of this section right here. We're trying to reference the owner relationship, but we didn't eager load it, so Laravel has to make a new SQL query. So, what you can do is, here, you could say... So, let's try this. We have our post. Let's load the comments. So, like, right now, we would just get the single post.
So, let's try this. We have our Post. Let's load the comments. So, like, right now, we would just get the single post. But you can also manually load any relationships. Like that. And now we have a post with the comments. And we can also use this dot syntax to load the comments and then the owner relationship on the Comment model. So, that will load this relationship as well. So, now, you'll see a nested set of the comments and then the owner associated with each comment. So, now, yeah, by doing it in this way, we avoid the n plus one problem. So, at this point, yeah, we could still do this. And now,
So, now, yeah, by doing it in this way, we avoid the n+1 problem. So, at this point, yeah, we could still do this. And now, if we give this a refresh and come back to laravel.log, we've gotten rid of that n+1 problem. So, to break it down, this one is where we sign in a User. Next, select * from posts; Well, of course, that's where we fetch the Post. And that's done through the implicit model binding. So, that's fine. Next, here's where we select * from comments; That is from when we load the comments. And then, finally, select * from users; That's where we load the subset of owners. So, now, what you'll find is that no matter how many
And then, finally, select * from users. That's where we load the sub set of owners. So, now, what you'll find is that no matter how many comments we leave, another, reply here, reply there, no matter how many we do, if we come back and refresh, it's still going to be a single set of four queries. And there's no n plus one problem there. So, that's basically it. You have infinite threaded comments at this point. If you want to stick around, we'll talk about some ways to maybe organize this a bit differently. But otherwise, the basic tutorial is done. Okay. So, this is a little bit gross.
Refactor With Scopes21:52
some ways to maybe organize this a bit differently. But otherwise, the basic tutorial is done. Okay. So, this is a little bit gross. Maybe another way we could do it is like this. Maybe we could say Comment for Post and give me all of them. Okay. So, now, we're just going to reference the Comment model directly. Let's see what this looks like. So, we have for Post, and that will accept the Post. Really, this is just a query scope, right? So, scopeForPost. That accepts the QueryBuilder. And we will return query where the post_id is equal to the id of the post passed in. So, it's just a readable way to add a constraint.
return query where the post ID is equal to the ID of the post passed in. So, it's just a readable way to add a constraint. Now, that should give us all comments. Let's take a look. Refresh. And now, we have a collection of comments. And then, continuing on from that, we could even say, every time I fetch comments for a post, I know that I will always want the owner included. So, if we come back and give that a refresh, now we have comments where we have eager loaded the owner for each. But then, we could even take it further. For example, we got rid of that. We haven't done any of the grouping, though. So, wouldn't it be nice if I could just get the comments and then say thread them?
Custom Threaded Collection22:56
that. We haven't done any of the grouping, though. So, wouldn't it be nice if I could just get the comments and then say thread them? But how do we do that? Because we know this is going to be Illuminate\Support\Collection. And I want to add a threaded method on it so that I can do some additional sorting. Okay. Well, here's a cool trick you may not know. Let's get rid of all this. I like to start from scratch. Any model can define a method called newCollection. And if this exists, Laravel is going to return not an instance of Illuminate\Database\Eloquent\Collection, but instead, an instance of your collection. So, here, you could say return new. This is kind of overkill,
eloquent collection, but instead, an instance of your collection. So, here, you could say return new. This is kind of overkill, by the way, but just to show you kind of a cool trick you can use in some cases. You could new up your own collection. So, this will accept your models. And then, you'll pass those in. Now, let's create that Comment collection. And you now have your own specific collection class that can have any functionality you need on top of it. If you ever need to do any kind of duck punching where you want it to work somewhat like Laravel's implementation, but you want to change it in some way, this is a good way to go about it. Just have this extend your collection.
somewhat like Laravel's implementation, but you want to change it in some way, this is a good way to go about it. Just have this extend your Collection class. Let me import that. And now, here, our method is called threaded, right? So, here's what we're going to do. We're going to call the parent groupBy method. So, we're basically reproducing what we had here. So, we're going to group everything by the parent_id and store that here. And then, we'll say, well, if we have any results, so as long as some items were returned and we don't have zero comments overall, then in that case, here's where we can do that other little bit of housekeeping where we
returned and we don't have zero comments overall, then in that case, here's where we can do that other little bit of housekeeping where we just want the top level or the root level comments to not have a key of nothing. We want it to have a key of root. So, I can grab all of that and come back and paste them in here. And, actually, we can even call this comments. Okay, so if we have any, then we're going to change this key to root and then unset the old key. And then, finally, we're done making our modifications so I can return that collection. Okay, kind of cool. You may not have known about that. So, now, think about it. I can get rid of all of this
so I can return that collection. Okay, kind of cool. You may not have known about that. So, now, think about it. I can get rid of all of this junk, come back to Chrome, give this a refresh, and maybe we didn't namespace it. Nope. Anyways, come back and refresh, and now we're getting the exact same thing that we had before, but we took a bunch of that junk and isolated it behind our own custom CommentCollection class. So, now, this is fine. Or, if you don't like doing it from the Comment, like if you really like the idea of saying, Post, give me the comments, well, remember, you can always load it here. So, just continue chaining. So, like
really like the idea of saying, post, give me the comments, well, remember, you can always load it here. So, just continue chaining. So, like here, refresh, so now we get all of the comments and we have eco-loaded the owner, but it's not threaded yet, so you could call threaded on your custom collection, and now I think you're going to get exactly what we had before. Yeah. So, yeah, that's an option as well. So, if you want to do that, then this could leave entirely. All a matter of how you want to do this. Or, just one more step, because that's really long, I don't like chaining that many times, you might just say post, getComments, and just isolate it behind
because that's really long, I don't like chaining that many times, you might just say Post, getComments, and just isolate it behind your own method. So, I know we're kind of going overboard at this point. getComments, we'll take that, paste it in here, and we're going to return this comments. So, we're just taking that long query and isolating it or encapsulating it within the Post class. So, now, I can just call Post, getComments, and I'm still going to get the same thing. If I get rid of this, refresh, and that all works again. So, we've taken all of that kind of muddy code and we've isolated it behind a nice method.
refresh, and that all works again. So, we've taken all of that kind of muddy code and we've isolated it behind a nice method. And, by the way, you can do the same thing here. That's a little muddy. Post::getComments() is going to perform a query where we eagerLoad the owner, we thread the results, the CommentCollection class does that by grouping them by the parentId, it assigns the root level ones to a key of root and returns those results. Then, the show view will list the root level comments. That listPartial will filter through them. For each one, it loads a comment. That comment displays
