تماشای این درس نیاز به اشتراک حرفه‌ای دارد.

Project Goal and Setup0:00

Here's what I'm working on today. Now, since the launch of Laracast, the comment form below each video has actually used a service called Disqus, and that's because it's basically a drop-in comment solution, and it saves time, especially in the early days, so that I could focus on other things. But now I really would like to merge it and make these native. So I don't imagine we'll get to everything, but why don't you come along as I begin this process from scratch. Okay, I'm going to begin by checking out a branch. I'll just call it comments. And next, I will make a test for how about videoComments. Alright, let's

Feature Test for Comments0:32

comments. And next, I will make a test for how about video comments. Alright, let's switch over, and I'll actually use a snippet here. Okay, so what is the first thing we can do? Well, I like to begin by approaching this from the outside in. So, how about a Video may receive comments. Simple enough. Alright, so my setup. Well, I'm going to expect some kind of video, and then I want to hit an endpoint, so I'm going to simulate the submission of a form. So if I submit a POST request to

and then I want to hit an endpoint, so I'm going to simulate the submission of a form. So if I submit a POST request to how about videos, and then the video ID, and then comments, and I'm going to send through the body of the comment, at least to start. Okay, I think that's the shape it'll take. Now, we have a couple options here. So what I could do is say, well, after you add a comment, when I visit the video page, I assert that I see that comment. And this is fine, but one thing to consider is, what if I render that comment form

that I see that comment. And this is fine, but one thing to consider is, what if I render that comment form with JavaScript? Maybe it's a Vue single file component that, when mounted, makes an AJAX request to our API to fetch those comments. Well, if that's the case, this is not going to work. This is only going to work for a server-side rendered solution. Another option would be to run this code, but tweak it so that we actually open a browser that can load and render JavaScript. Anyways, things to think about, right? So often, instead, I just want to make sure that the data is available.

Routing and Controller Setup2:40

exception handling because I want to see exactly what the issue is. I don't want Laravel to swallow the exception that was thrown and then convert it into a response. So now I can see, ah, okay, there is no endpoint setup. And of course not. That's our next step. I'm going to go to my routes/web.php file. And what I've done here is, you can see I have around 200 lines worth of routing information. No need for you to see that, so I'm just going to do it down here at the bottom. We'll say listen for a POST request to /videos/video/comments. And we'll do VideoCommentsController. And we're going to store a new comment. And now, actually, a quick note on

And we'll do VideoCommentsController. And we're going to store a new Comment. And now, actually, a quick note on controller naming. Your first instinct might have been to do something like VideosController and then maybe add Comment. As a general rule of thumb, I try to avoid method names like this on a controller. I like to stick to the seven restful actions, which of course is index, show, create, store, edit, update, destroy. I think that's seven. I like to stick to those. And whenever I feel the need to create a different action, I instead create a new controller. And I think that's a pretty good practice. So in this case,

I feel the need to create a different action, I instead create a new controller. And I think that's a pretty good practice. So in this case, if I go to VideoCommentsController, notice that I can go right back to one of the seven restful actions. Okay, let's run my test. And of course it still fails, because that doesn't exist. Alright, php artisan, make me a controller. Alright, run it again. Let's see. The store method does not exist. Alright, VideoCommentsController. So we're just following the steps. I run it again. Okay, so now we've done assertCount, but we're of course returning nothing. So let's see what our next step should be.

the steps. I run it again. Okay, so now we've done assertCount, but we're of course returning nothing. So let's see what our next step should be. I'm going to switch back to our test. So we've created a Video, we've successfully hit an endpoint, but at this point we're trying to reference a comments relationship, but that doesn't exist yet. And in fact, once we hit that endpoint, we don't do anything at all. Okay, let's figure out what we want to do there. Well, we're going to accept the video in question, right? And let's make sure I import that. So at this point, if I were to dd, you'll see that we do get our instance there with all of the information. Okay, so what do I want to do? I want to add a comment

Model Method and Relationships4:48

you'll see that we do get our instance there with all of the information. Okay, so what do I want to do? I want to add a Comment to the Video. So maybe we have a method called addComment, and we can grab the comment from the body. Now we do need validation here. I'll come back to that in a moment. But I think this is what I want. Okay, but if I give it a run, it's going to blow up because there is no addComment method. Alright, so now at this point, I will take a break from the feature test and drop down and write a model test. And often you may find that the general shape of the test you write is similar, but you're testing at different levels of the system. So with that in mind, I already have a test for my Videos.

of the test you write is similar, but you're testing at different levels of the system. So with that in mind, I already have a test for my Videos. I'll go to videoTest, and here you can see the minimized tests that I already have. But we're going to do another one here, and we can say canReceiveComments. A Video can receive comments. So once again, it's going to take a very similar shape. So let's pull that in. Okay, so given I have a Video, and now we can try out that new method we want. So if I say fooComment, how can we confirm that this works? Well, I'm assuming we're going to have a new relationship called comments. Give me the comments for the video.

how can we confirm that this works? Well, I'm assuming we're going to have a new relationship called comments. Give me the comments for the video. And right now, I'm going to assert that we have exactly one comment there. So if I give that a run, it now fails. So yeah, we've dropped down a level. We're no longer going from the outside in the way a user would. We've dropped down a level to the model. And we're specifically verifying that this method works the way we expect and nothing else. Okay, let's now go to my Video model. And here at the bottom, you can ignore some of this other stuff here. But we have this new method addComment and give it a run. And now we've reached the point where

And here at the bottom, you can ignore some of this other stuff here. But we have this new method addComment and give it a run. And now we've reached the point where we're trying to count something that is effectively null. Okay, so how would we add a comment? Well, first we need to understand the relationship between a Video and Comments. And of course, that's an easy one. A Video has many Comments, and a Comment belongs to a Video. So let's set up our relationship here. This has many Comments. And then, if we're accepting the body of the Comment, it should be as simple as saying, give me the comments relationship and create a new one where the body

body of the comment, it should be as simple as saying, give me the comments relationship and create a new one where the body is equal to what you gave us. Alright, so that will make a SQL query and it will attach the video ID foreign key in the process. So if we give it a run now, we're on to the next step. We don't have a Comment model. php artisan make:model Comment. And run it again. Now I get a MassAssignmentException. And that's because I'm trying to mass assign the fields here. We can either go to our Comment model and set the fillable fields, or I'm pretty safe already so I will just turn it off effectively. Okay, so if we give that a run,

comment model and set the fillable fields, or I'm pretty safe already so I will just turn it off effectively. Okay, so if we give that a run, now it fails. And that's because I don't have a comments table. There's our next step. php artisan make:migration create_comments_table. So now, if I visit that migration, at least to start, we know that a comment consists of a body. So let's stick with that, and then I'm going to migrate my database. So yes, I can do php artisan migrate for my local database, but on the testing end, I'm also using a MySQL database. So what I'll do there is

php artisan migrate for my local database, but on the testing end, I'm also using a MySQL database. So what I'll do there is I can set the database connection, and I have one called testing. Okay, so with that migrated, let's give it another run. Still fails though, because there is no videoId. Alright, fair enough. So it's trying to create that foreign key in that relationship, but we didn't specify it. We'll do that now. I want an unsigned integer for the videoId. And we do unsigned because it must be positive. Okay, so what I'll do here, well, let's just do a full fresh run from scratch. And now,

Threaded Comment Replies8:32

Okay, so what I'll do here, well, let's just do a full fresh run from scratch. And now, we can give it another run, and we get green. It's working. Alright, so now we know a Video can add Comments, and specifically, it does so through this method. So now that I know this works, I can jump back up a level and then test our feature. So I'll go to VideoCommentsTest, give it a run, and now that returns green as well. So with that working, I'd like to do another one now. A Comment can reply to an existing Comment. So if I switch back, notice we have standard Comments here, but we

A comment can reply to an existing comment. So if I switch back, notice we have standard comments here, but we also have a one level of threaded comments. So here's a main comment, and then here's a reply to an existing comment. So we need to allow for that as well. Okay, how about a Comment may be replied to. Okay, so once again, our setup. We're going to create a video, and then, let's tweak this for a minute, I'll just add a Comment directly here. So we'll say fooComment, and then I'd like to add or reply to that existing Comment.

add a comment directly here. So we'll say fooComment, and then I'd like to add or reply to that existing comment. So let's do it the same way. We're going to make a POST request to that video, and we'll say replying to fooComment, but here I'm also going to add the ID of the comment that this is in response to. So maybe something like replyToID will be the ID of this comment here. Okay, we may tweak this, but that's at least my first instinct. Okay, so now we're going to at the very least have two comments. So if I give that a run, hmm, it's going to fail.

instinct. Okay, so now we're going to at the very least have two comments. So if I give that a run, hmm, it's going to fail. Trying to get property id of non-object. It sounds like when we call addComment here, it's not returning the comment. So let's go there directly, and sure enough, I forgot to return. So when we call the create method, that will return to us the created comment. So now if I give it a run, that returns green, but that's not really what we're testing here. Yes, I expect two comments, but I also expect, let's see, we'll say videoComments give me the most recent one, and I'm going to expect the

difficult. You really have to stop and think for a few minutes. For now, I'm going to stick with subject here, but later we might want to change that because I don't love it. Okay, but anyways, if I get the subject of this Comment, it should be that Comment. So I could say this assertTrue, and we'll say isComment. Yeah, so a quick tip, if you ever want to assert that two models are the same, so in this case, if I get the subject of the most recent Comment, if we did everything correctly, I'm going to expect it to be that model. So we can use the is method there, and then simply assertTrue.

recent Comment, if we did everything correctly, I'm going to expect it to be that model. So we can use the is method there, and then simply assert true. Okay, but I know this is going to fail because there is no subject relationship for Comment. Okay, so it sounds like I need to drop down a level and make a test for comments. So let's do it now. php artisan make:test CommentTest. Alright. CommentTest. And then here, for my Comment model, say, how about reply to an existing Comment. Yeah, so something like, given we have a video,

say, how about reply to an existing Comment. Yeah, so something like, given we have a Video, and that Video has a Comment, so again, it's going to take a very similar shape. But don't forget, we're testing different levels of the system. And that will accept our Comment. Yeah, so now, I'm just thinking this out, I want to be able to reply, this is almost inception-like, so I want to reply to an existing Comment. And we'll say reply to foo Comment. Yeah, maybe that's what we want. Okay, so how can we assert that

reply to foo Comment. Yeah, maybe that's what we want. Okay, so how can we assert that this is correct? Well, if we get the replies, so again, don't forget, none of this exists yet. We're trying out the API. What do we want it to feel like? And once we find something we like, we'll then write the production code. So at this point, I'm going to expect there to be one reply to this specific comment. Alright, let's give that a run. And it blows up, because there is no reply method on Comment. Alright, let's go there. Give it another run. We return nothing there. Alright, so

on comment. Alright, let's go there. Give it another run. We return nothing there. Alright, so we're going to accept the body of this new reply. And I think we can do this in a couple ways. We could reference an Eloquent relationship, but also sometimes it's easier if we did something like this. Let's create a new Comment. So this will be our reply to an existing Comment. We can even call it reply if we want. But on that, we have to set what the replyToId is. Well, we are replying to the current Comment. So I could say this->id. Next,

to ID is. Well, we are replying to the current comment. So I could say this ID. Next, we need to know what the video in question is. Alright, well the video ID will be the current comment. And then it sounds like we need a video relationship. Something like that. Finally, we can say comment->save(). So yeah, if it helps, why don't we rename this to reply. Create a new reply. Set the associated comment, the parent comment. Set the video it belongs to. And then save it to the database. So here we're not using any fancy Eloquent relationship. We're just doing it directly here. And I think many times it's actually easier to

And then save it to the database. So here we're not using any fancy Eloquent relationship. We're just doing it directly here. And I think many times it's actually easier to consume. Okay, but real quick, we do need the video relationship. What is the relationship between a Comment and a Video? The Comment belongs to the Video. Okay, so let's give that a run. And it still fails. So at the moment, there is no replyToId on the comments table. Alright, and that's our next step. So here, we'll add a new one here. Assigned integer for replyToId. However, sometimes a Comment isn't replying to anyone. It's a top-level comment.

Assigned integer for replyToId. However, sometimes a Comment isn't replying to anyone. It's a top-level Comment. So I will make this nullable. Okay, so once again, I have to rerun our migrations. But a quick note, if you were using the RefreshDatabase trait, that would all happen instantly, and you wouldn't have to worry about rerunning those each time. It's just a unique thing with my system. Okay, so assertCount has to be countable. It's still failing. So I'm going to go back, give this another run. So let's see what the issue is. Yes, we can add a Comment here. And yes, we can reply to the Comment. But nonetheless,

give this another run. So let's see what the issue is. Yes, we can add a comment here. And yes, we can reply to the comment. But nonetheless, when we call assertCount, it looks like we have not yet created the replies relationship. Okay. Replies, and what is the relationship between a comment and its replies? It can have many replies. But those replies are still comments themselves. So it has many of itself. Like I said, kind of inception-like. So let's give that another run. It's still failing. And that's because right now it's trying to find all comments using this convention of comment ID, but we

run. It's still failing. And that's because right now it's trying to find all comments using this convention of commentId, but we know that's not right. It's called replyToId. So let's give that another run, and finally, we're back to green. Okay, cool. So now we know we can add comments to a Video, and we can also make replies to existing comments. So if you want to see this in the wild, let's play around. Let's quickly whip up a Video. And then I will add a comment to that Video. Hello there. So if I say videoComments, I now have a

And then I will add a comment to that video. Hello there. So if I say video comments, I now have a collection of one item. And you'll notice this comment isn't replying to anything. So the replyToId column is null. Let's grab that comment, though. So we'll say give me the very first comment there, and it's that one. And now I'm going to reply to it. I am replying to you. Okay, so now let's do this video. Give me a fresh instance, and then give me the comment so we don't deal with the Eloquent cached version. Okay, so now I do have two comments, but this one is a direct reply.

the comment so we don't deal with the ego-loaded cached version. Okay, so now I do have two comments, but this one is a direct reply to the comment with an ID of one. So that means if I were to say videoComments first, and then give me the replies to that comment, we'll get a collection of one item. But then also what we need to do is we haven't done this yet, have we? Here's the Comment model. No. So if I have an existing comment, I want to get the parent. And we called it subject. I still haven't decided which one is correct. Maybe I'm going to switch back to parent. I'm still not sure. But let's go to CommentTest. And actually,

which one is correct. Maybe I'm going to switch back to parent. I'm still not sure. But let's go to CommentTest. And actually, I just noticed this is in my features directory, but I don't think of this as a feature. I think of it as a standard model test or a unit test. So I'll move that later. But anyways, a Comment can reply to an existing Comment, but also it knows its parent. That's kind of weird, but that's okay for now. So if we have a Video, and we have a Comment, and then we reply to that Comment, and in fact I'll need to make sure that returns the new reply.

and then we reply to that Comment, and in fact I'll need to make sure that returns the new reply. But anyways, if I have that reply, I want to be able to access its parent Comment. And one more time, that should be assertTrue reply parent is the Comment. Alright, we'll give that a run. Trying to get property parent of a non-object. Okay, so we'll go to our Comment. And here, maybe we'll get the parent. And I think, another inception moment, a Comment instance belongs to another Comment.

we'll get the parent. And I think, another inception moment, a Comment instance belongs to another Comment. Okay, let's run that. Trying to get property parent of non-object. Hmm, you know what? Let's do this. Let's dump the reply and run it. Ah yeah, we're getting nil. So in my Comment model, the reply method must not be returning anything. And it's not. Return reply. So we run it again. Okay, I think we changed the error. Call to a member function is on null. So it sounds like when we get the reply parent, that's returning null. So it's probably going to

the error. Call to a member function is on null. So it sounds like when we get the reply parent, that's returning null. So it's probably going to be related to our relationship here. So if I want to fetch the parent, once again I need to say replyToId. Because remember, otherwise it would look for parentId. And that's not what we're looking for. We're using replyToId. Alright, so that'll be useful in situations where I have a Comment and I want to say, alright, what is this in relation to? Give me the parent comment. And then give me the video associated with it. So we're kind of wiring up all of these different relationships. And what I could even do

comment. And then give me the video associated with it. So we're kind of wiring up all of these different relationships. And what I could even do here is say, how about this? A top level Comment has no parent. So I could say assertNull($reply->parent). And actually that would be Comment. Yeah, just to be crystal clear, top level Comments don't have a parent. But if you reply to an existing Comment, then the reply's parent is the parent Comment. Okay, so now, hmm, I think I'm ready to go back up to our main top level feature test.

comment. Okay, so now, hmm, I think I'm ready to go back up to our main top level feature test. So this one's working, but I think this one is still failing, right? So what is the issue? Well, we're making a POST request and we're sending through the replyToId. So it sounds like we aren't yet doing anything with that. So I'm going to go to my controller. And how about this? We'll say if we have a replyToId field, and we'll say commentId. So if that exists in the request, then we're trying to reply to an existing comment. So let's say comment

id. So if that exists in the request, then we're trying to reply to an existing comment. So let's say Comment::findOrFail by the comment_id. And then what we're going to do, and we've already worked out this API, we're going to reply to that comment using the body of the request. Otherwise, so we're going to clean all of this up in just a moment, but we're just kind of doing it as directly as we can. And then we'll refactor once we're at green. Okay, so we'll give that a run. It's still failing. What on earth? Let's see. video_comments last Oh, we changed it to parent, didn't we? I keep going back and forth.

So I'm going to go back to our controller here. We do have to do that. I'll save that. But here what am I trying to do? Well, I'm still trying to add a Comment to a Video and specifically whether or not we call comment reply or video addComment it's kind of like, who cares? I just want to add a comment to the video. I can drop down a level to decide how we do that. So with that in mind, let's open up a sidebar here, or a second pane, and I'm going to go to my Video model. And when we addComment yeah, maybe there can be the place that we store this logic. Or at least we can try it. So we would say request reply

and we'll change this variable in a minute and then add a reply to that comment otherwise. So let's see we can bring this up. Yep. And then this would be compact body. We're going to accept the body parameter up here. Next this would be body. Yeah. So have I broken anything here? Let's run it. Yep, I have. So too few arguments to addComment. One passed, but I expected two. Yeah, that's because we introduced a new one here, but it's not always necessary. It's only if you want to override it. So let's try

here. Put that on its own line and return. Are we still okay? Yes, we are. Otherwise, just call our standard create relationship. So this is another case where I can remove the else and we end up with that. Run it again. Still at green. Now another thing we might do here is create a new trait. Maybe something like Commentable. Like so. And then if I switch back I could just move these over, paste it in. Alright, run it again. Everything should blow up and it does. So at the top I can now say use

Alright, run it again. Everything should blow up and it does. So at the top I can now say use Commentable. Run it again and we're back to green. Okay, so now our controller, we're just adding a comment to a video. And if you ever want to see what specifically goes on there, now you can. You can see if there is a parentId, then we're replying to that comment. Otherwise, we're creating a fresh one from scratch. Excellent. And I think that's about all we have time for here. If you want to see how you would do validation, we'll do that real quick and then we're going to call it a day. If you wanted to say a Comment requires a body.

Adding Comment Validation25:04

you would do validation, we'll do that real quick and then we're going to call it a day. If you wanted to say a Comment requires a body attribute, then here's the basic shape you would take there. You know you're going to hit this end point and you know you need a video in question to comment on. So let's leave it like that. And we're going to leave the body blank. So if we do that, we'll assert that the session has errors, and specifically one for the body. Let's run it. It's failing. Okay. VideoCommentsController. Request. validate

Let's run it. It's failing. Okay. VideoCommentsController. Request. validate that the body is in fact required. So now, if I give it a run, and it's failing, but you do see a validation exception was thrown, which is what we expect. So we just need to make sure that, in this case, we catch that exception and it gets converted into the proper response with errors in the session. So I will be explicit that I want exception handling turned on. Okay. So now we do get green. So this is a nice and easy way to verify that certain attributes.

دوست دارید گاهی خبرهای Laracasts را ایمیل کنیم؟