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

Planning Comment Deletion0:00

Whoa, whoa, whoa. I'm sorry, okay? Please don't shoot me. Look, it's nice in any CRUD application, particularly in a forum where we have user-generated content, to allow a User to be able to delete something they've created. In fact, the D in CRUD stands for DELETE. So, why don't we go ahead and give the User the ability to delete comments that they've posted. We'll start by creating our test file under the CommentController directory. I'm going to call this DestroyTest. You could also call it DeleteTest. I'm sticking with the method names that have been generated by Laravel in the controller itself, but calling it DeleteTest would fit in line with the CRUD and HTTP verb terminology. So, choose one and stick to it for the project.

Writing Destroy Auth Test0:54

would fit in line with the CRUD and HTTP verb terminology. So, choose one and stick to it for the project. Okay, first of all, why don't we copy this authentication test that we created in the last episode. It's not going to be a POST request, it's going to be a DELETE request, and we're not going to be going to posts.comments.store, we're going to be going to posts.comments.destroy. Now, we could actually remove the POST prefix here. If we do, this is called shallow routing, and it basically means you have the minimum amount of information in the route possible. Because we have a Comment model and we have a comment ID, technically we don't need the parent post in the route as well. And I actually really like shallow routing, so it's what we're going to opt for in this instance.

parent post in the route as well. And I actually really like shallow routing, so it's what we're going to opt for in this instance. I can now remove this PostFactory because we want a CommentFactory. So, CommentFactory::create. And then, of course, we want to assert that we're redirected to the login route. Okay, let's run the test. And of course, it fails because we've not defined a comments.destroy route. So, we'll go to our web.php file. And inside our authentication group here, I'm going to duplicate this line. We change it to be DELETE. We'll get rid of this prefix, as we mentioned. And instead, we're going to add the variable for the comment ID itself. We'll go to the destroy endpoint. And the name that we set was comments.destroy, right? So we'll add that in there. Okay, let's jump into destroy. And if we rerun the test, hopefully,

Testing Deletion Behavior2:14

We'll go to the destroy endpoint. And the name that we set was comments.destroy, right? So we'll add that in there. Okay, let's jump into destroy. And if we rerun the test, hopefully, we now have a passing test. Everything has been set up correctly, with authentication at least. We now need our second test. So, we'll jump back into the destroy test file. And we can say it can delete a comment. Let's open up our test closure. And we want to do this. So that's going to be the same. Let's extract this to its own variable. So, we'll say comment equals commentFactory::create(). We also need to log in. We'll log in as the user that created the comment. So, I'll log in as comment->user, like so. Chain delete onto the end. Pass comment into the root. And then I can use a helper that Laravel provides called assertModel.

the Comment. So, I'll log in as comment User, like so. Chain delete onto the end. Pass comment into the root. And then I can use a helper that Laravel provides called assertModelMissing, passing in the model that I want to check for to ensure that, yes, Laravel has gone ahead and deleted that comment. It's no longer found in the database. If we run the test, it's going to fail because obviously, the database does contain a comment with an ID of one. So, let's go ahead and fix that. We'll jump back into the CommentController. I should be able to just say comment::delete, I think. And if I rerun the test again, now it's passing. We have our second passing test. Now, intuition tells me that because we've not returned anything here, it's actually going to return a 200 response. And in previous episodes, we have already discussed actually redirecting so that Inertia can handle things.

Adding Redirect After Delete3:42

not returned anything here, it's actually going to return a 200 response. And in previous episodes, we have already discussed actually redirecting so that Inertia can handle things correctly. So, before we jump into the front end, let's write a third test that says it redirects to the post show page. Okay. And we can more or less copy the test body above, but we'll just be changing the last little bit. So, we'll grab this here. And let's drop this onto a second line. And then we can say assertRedirect. And we want to go to the post show page. And the post is going to be the comment postID. Okay. So, that's the post we're redirecting to. We'll run this test. Obviously, it fails. And exactly as we'd expect, that's because we're actually currently returning a 200. We'll go back to our CommentController. We can make use of that route helper again. So, return to

we'd expect, that's because we're actually currently returning a 200. We'll go back to our CommentController. We can make use of that route helper again. So, return to route(post.show, passing in the comment->post_id). If you're wondering why I'm using the post_id directly instead of just loading in the Post model, well, that's because I don't want to load in the Post model. Loading the Post model requires that we go and make a database query. But that database query is completely unnecessary in its current state. So, we've just been making database queries for no good reason. Instead, I'll make direct use of the ID on the Comment model itself, saving ourselves a query and thus increasing our application performance. If I run this test once again, now it passes. And if I run all of the tests in our destroyed test file, all of them are green. Meaning that we're

Building Delete Button UI5:13

our application performance. If I run this test once again, now it passes. And if I run all of the tests in our destroyed test file, all of them are green. Meaning that we're pretty much in a good position to go ahead and create the front end. Let's do that now. Now, we already extracted this little Comment component. So, for now, we'll create everything in here. And then we can come back and refactor as needed a little later down the line. If I look at the front end itself, well, here's the little tagline. And I think our action should come underneath. So, perhaps underneath this span, we'll add a div. Maybe we'll give it a little bit of a margin top. And then inside here, we can have any actions that we require. For accessibility, I'll wrap the button in a form. And we want to catch the submit action, prevent it. And let's call the method that we'll call deleteComment.

we require. For accessibility, I'll wrap the button in a form. And we want to catch the submit action, prevent it. And let's call the method that we'll call deleteComment like. So, then we'll have a button in there that says delete. We'll start it in just a moment. But as you can see, the delete button is there. And we're ready to implement the logic. So, we'll come down here. Let's grab the props as a constant. So, const props equals defineProps like so. And then we can say const deleteComment equals a closure. That closure is going to make use of Inertia's router to perform a delete request. We're going to use the comments.destroy route that we set up, passing in props.comment.id. And I think that's actually all we need for the basics of this. Let's test it out. So, I'll jump back into the application itself. And if I click delete on the comment that we created,

I think that's actually all we need for the basics of this. Let's test it out. So, I'll jump back into the application itself. And if I click delete on the Comment that we created, sure enough, you can see that it's gone. Now, when we did click delete, we jump back to the top of the page. We've already discussed how to fix that. I can add an option to the router where I can set preserveScroll to true. And when I do that, now as I delete comments, well, we stay in the same place. And I'm able to carry on doing exactly what I was doing before. Did you know that I'm deleting other people's comments, though? That's not good, is it? If we allow that, then we're going to open up another large security hole in our application. So, rather than doing anything on the front end just yet, why don't we go ahead and actually fix the back end first? I'll head back into our destroy test.

Preventing Unauthorized Deletes7:25

in our application. So, rather than doing anything on the front end just yet, why don't we go ahead and actually fix the back end first? I'll head back into our destroy test because I'll write the test before the implementation. And we'll say it prevents deleting a Comment you did not create. We'll open our test closure. And again, you can actually reuse most of the body from a previous test, like this one, for example. But instead of acting as the Comment's user, we're going to act as a new User. So, we'll say User::factory()->create(). And now this Comment and this User are in no way related. In order to check this, I will assert that we're forbidden from being able to perform that action. Let's run the test. And yeah, as you can see, everything currently fails because the CommentController is allowing any User to delete any Comment. Let's go into the CommentController and think

test. And yeah, as you can see, everything currently fails because the CommentController is allowing any user to delete any Comment. Let's go into the CommentController and think about how we might be able to stop this from happening. We could do this using a basic check. So, I'll inject the $request at the top. And then we can say, well, if the request user_id is not equal to the comment user_id, then what we actually want to do is abort at this stage using a 403 response. And if we refresh, well, now you can see that the test passes. We prevent a user from being able to delete somebody else's Comment. Let's run all of the tests to make sure we've not broken anything else in the process. And with that done, well, when we go back to our front end and we attempt to delete someone else's Comment, we're going to get an error. So, that does give us some protection. But if

that done, well, when we go back to our front end and we attempt to delete someone else's comment, we're going to get an error. So, that does give us some protection. But if we're not the author of a comment, we shouldn't even see the delete button. So, how can we go about resolving that? Well, if we go to the Comment.vue file, we could create a computed property here. So, const canDelete; And we'll set that to computed. And inside there, what are we going to check? Well, we'll check if props.comment.user.id is equal to page.props.auth.user.id. And we'll use a question mark here just in case the user object is null. Okay. So, implementing that would be as simple, hopefully, as doing a v-if on the form here and checking for that computed property of canDelete. And now you can see, well, all of these comments have no delete button. But if we go to the form and we'll create a

form here and checking for that computer property of canDelete. And now you can see, well, all of these comments have no delete button. But if we go to the form and we'll create a helloWorld comment, this does have a delete button because we can delete that comment. But this is a very naive approach to authorization. Why? Well, if our logic for whether you can delete a comment changes, we need to update it here. We also need to go back to our CommentController and update it here as well. Now, wait a minute. Why would we want to ever update the authorization code that we've created here? Well, let's say not only do we want to check that a Comment belongs to a User, but we also want to check that they created it within the last hour. Because after an hour, well, the comment becomes part of the narrative. Other users have likely replied to that comment or based their comments on another comment. So, deleting it could really

Because after an hour, well, the comment becomes part of the narrative. Other users have likely replied to that comment or based their comments on another comment. So, deleting it could really destroy the entire narrative of the conversation. If you're dealing with something like timestamps in authorization, you really don't want to be handling it on the front end and the back end because that can cause all sorts of annoying issues and UI bugs. So, instead, it would make much more sense to handle all of it in the back end and then pass down abilities to the front end in order to show or hide different parts of the UI. Thankfully, Laravel has an amazing tool set around authorization that we can refactor our naive code to use. So, let's tackle that in the next episode.

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