Planning Update Tests0:00
Okay, now that we have a little bit of a flow going for creating CRUD methods in our controllers, I think it's going to be fairly straightforward to add the update functionality for our comments. What I've opted to do is create this new test file, and I've gone ahead and broken down this larger task into small individual to-dos. So it requires authentication, it can update a comment, it redirects to the post show page. See, by taking the time to come up with all of these cases first, while the whole thing is in your brain, you can see the small tasks that you'll actually have to work on to bring this to fruition. It's a great way to work, and whenever I'm building out an entire feature, this is usually how I tackle it. Let's start by implementing our authentication test here. We can actually steal the code from the destroy test, so let's go and grab that code there,
Adding Update Route0:47
Let's start by implementing our authentication test here. We can actually steal the code from the destroy test, so let's go and grab that code there, and we'll paste it in. We actually want to make a put request rather than a delete request. We do want to create a comment, but we're going to go to the comments.update endpoint instead, and the rest of that looks absolutely fine. Let's run the test. It obviously fails because we've not yet created the root. Starting to get into that process of repeating ourselves, but in a good way. If you get into a nice flow, then you can work effectively.
Starting to get into that process of repeating ourselves, but in a good way. If you get into a nice flow, then you can work effectively. Let's go into our web.php file. I'll copy this line. We're obviously going to make a PUT request rather than a DELETE request, but the URL will remain the same because Laravel is going to differentiate based on the HTTP verb rather than the URL in this case. We need to go to the update method, and it's going to be called comments.update. Run our test again, and now it passes. So first test passing, move on to the next.
Implementing Comment Update1:41
Run our test again, and now it passes. So first test passing, move on to the next. It can update a Comment, so here's our golden path. Let's think about how we want to format this. We'll obviously have our test closure. We're going to need a Comment to update. Let's say CommentFactory create, and I'm going to hard code the body in place seeing as that's what we'll be changing. So we'll say this is the old body, and then let's have a variable called newBody,
So we'll say this is the old body, and then let's have a variable called newBody, and we'll fill that with this is the new body. And I like to separate it as a variable because it makes it very obvious what we're actually checking for in this test, as you'll come to see in just a moment. So we want to act as the Comment User, and once we're logged in, we'll make a PUT request. The PUT request is going to go to the comments.update route, passing in the comment itself to the route,
The put request is going to go to the comments.update root, passing in the comment itself to the root, and then we'll set the body to the new body here. That should be all we need to do to actually make the request. We'll use the assertDatabaseHas helper to check that in the comments table, there is an entry for that comment. So it will be id is equal to commentId, and then I'm going to check that the body tag or the body column in that particular row has the new body content rather than the old body content.
or the body column in that particular row has the new body content rather than the old body content. Let's run this test. It's obviously going to fail, and it will tell you exactly why it failed. Found similar results, but using the old body. So nothing was updated in our code. Let's jump into the CommentController, and let's make it work. I'll just minimize this so it's out of the way for a moment,
and let's make it work. I'll just minimize this so it's out of the way for a moment, and we're looking for the update method. Where are you? Here you are. We're going to need to grab the data from the request, and I'm going to use the validate method even though we've not got to that test yet, but I'll just keep the rules very minimal, maybe just using the required rule,
but I'll just keep the rules very minimal, maybe just using the required rule, and that's because I know I'm going to be implementing validation. I don't want to have to keep changing the code while I'm building this feature out, and this is the simplest form of validation that will allow me to grab the body from the request payload. But once we've done that, well, we take the comment, and we update it using that data. Let's run the test again,
Redirecting After Update3:48
and we update it using that data. Let's run the test again, and you can see down here that it passes, meaning that with just a couple of lines of code, we've actually already got the golden path for being able to change the body of a Comment. Let's go back to our test file. The third check is that it redirects to the Post show page. We can copy the majority of the code from above here. So I'll grab this,
We can copy the majority of the code from above here. So I'll grab this, and we don't need to assert the database has anything. We just need to make the actual request, and then we'll say assertRedirect, and we're looking to go to the post.show route, passing in the comment post like so. Now, it might be tempting to leave this test as is and just run it, but you should always look at the format of your tests.
and just run it, but you should always look at the format of your tests and keep them as clean as possible because your tests are going to show your intent further down the line. So in this case, well, we're not actually looking at what the new body is, so I can remove that completely, and I could inline this here because, again, the body itself isn't important.
and I could inline this here because, again, the body itself isn't important. We're just interested that if you send a valid payload, you'll be redirected to the post show page. Let's run this test, and obviously it fails. It's going to fail because there is no redirect, so we have a 200 response. We can jump into the CommentController as we have in the past, and we'll use the to route helper once again to say,
as we have in the past, and we'll use the route helper once again to say, let's go to the post.show route. I know I'm going to be implementing that little page query parameter that we looked at in the previous episode, so I'm actually going to use the array syntax here to specify that it's the post that links to the comment post_id. Again, using post_id rather than the post relationship.
that links to the comment post ID. Again, using post ID rather than the post relationship to avoid unnecessary database queries. Let's run the test again. We have our third passing test. Time to move on. The next test will be very similar. I'm going to copy the code here, open up our testing closure, and then inside we'll paste this in.
open up our testing closure, and then inside we'll paste this in. I obviously also want to pass the page number, so let's say that this is the comment, and then we'll pass a page parameter of 2, and we'll just check that when we're redirected, well, this is the post, but we should also have a page parameter set of 2. Run the test. It's failing.
Run the test. It's failing. Obviously, the two strings are not equal. We can easily fix that by adding this page parameter here, and we'll say request query passing in the page parameter that we receive from the URL. Okay, let's run that, and now it passes. Do you see how easy it is to build all these small components and work towards a finished feature set?
Enforcing Update Authorization6:18
all these small components and work towards a finished feature set because we broke up the task into small tests at the beginning of the episode? I think it's pretty cool. It's a really nice way of working. Here's our authorization check. We want to make sure that you cannot update a Comment from another User. Again, we could probably go ahead
from another user. Again, we could probably go ahead and copy the majority of this, so let's grab this here, and we'll paste it in. We want to act as a completely different user, so we'll say userFactory::create, and we're not interested in passing the page. That's superfluous, so we'll remove it, and we want to chain on the end assertForbidden to make sure that we're not actually allowed
and we want to chain on the end assertForbidden to make sure that we're not actually allowed to take that action. Let's run it. It's going to say, nope, actually, it was absolutely fine. We already know the fix for this. Let's go back into the CommentController, and at the top here, we can say this->authorize to update the following comment. Let's jump into the policy where we have this update method,
authorize to update the following Comment. Let's jump into the policy where we have this update method, and for now, we can just say, let's make sure that the userId is equal to the comment's userId, much the same as we did for our delete check, but we're not doing any date timestamp checking. Run our test again, and it passes, so now we have authorization in place. There's one last test, which is that we have valid data.
Adding Update Validation7:31
so now we have authorization in place. There's one last test, which is that we have valid data. Well, let's think about how we did this in the previous episodes. We first of all have some form of data set that allows us to check for invalid data, so I'll use the with method for that, and we'll say null is invalid, maybe a Boolean, an integer, a float. We also want the str_repeat to make sure
maybe a boolean, an integer, a float. We also want the string repeat to make sure that you can have a maximum of 2,500 characters, so I'll set that to 2,501, and then we'll accept that false body inside the function parameters. Okay, let's copy the majority of this up here. We'll paste it down below, and then I'll replace the hard-coded body here with the body that we receive from our data set.
and then I'll replace the hard-coded body here with the body that we receive from our data set, and then we can use the assertInvalid method to check that the body is actually invalid. Run the test. Obviously, we have five tests being executed. One thing I've just noticed is that we're actually using a different User here, so we're never going to even get to validation. That's one issue that can crop up.
so we're never going to even get to validation. That's one issue that can crop up if you just copy and paste your test without checking, so be aware of that. Let's log in as the correct User, run these again. Yeah, and I expect that one of these passes because we've already in the CommentController added the required validation rule. So the other validation rule is that it's a string, and the final validation rule
So the other validation rule is that it's a string, and the final validation rule is that we have a maximum of 2,500 characters. Rerun the tests, and all five of them now pass, which means that we're actually now in a position to start refactoring our code to make sure it's as clean as can be and easy to maintain down the line. There are a couple of refactors that I want to focus on specifically.
There are a couple of refactors that I want to focus on specifically. The first thing I'm going to do is come down to our tests, and I'm actually going to run all of the CommentController tests together. So we have 24 tests for our CommentController in place, and with that at the bottom of my IDE, I can rerun that as many times as I want. I'm ready and confident that my refactors are going to be absolutely fine.
Refactoring Authorization with Resources9:29
I'm ready and confident that my refactors are going to be absolutely fine. The first thing I'd like to tackle is this authorize method that we are calling at the top of update, that we're calling at the top of destroy. See, we just have a standard resource here, and as long as you have a resource and that resource follows standard naming conventions, Laravel offers you a shortcut for authorization. Let's come up to the top of our controller,
Laravel offers you a shortcut for authorization. Let's come up to the top of our controller, and I'm going to override the constructor, and inside that constructor, I'm going to call the authorizeResource method that controllers have built in. I want to authorize the Comment resource since that's the model that we're working with, and now I can actually go back through to the update method. So here it is, and I can remove the authorize check there,
and now I can actually go back through to the update method. So here it is, and I can remove the authorize check there, and I can also remove the authorize step from destroy. Let's rerun the tests. Okay, we have some failing tests, seven in total. Let's take a look at that. You'll note that all of the failing tests are actually coming from the store test rather than the update or destroy test. That's because we never actually added
rather than the update or destroy test. That's because we never actually added any form of authorization for the store method, so it's trying to authorize it, but if we take a look at our CommentPolicy, specifically the create method, which correlates to storing the new resource, it's empty, it's not returning true. We know, at least for now, that any User can post a comment, so let's just return true here and rerun our tests,
Refactoring Routes to Resource10:49
We know, at least for now, that any User can post a Comment, so let's just return true here and rerun our tests, and now they all pass. How cool is that? The next thing I'd like to tackle is in the routes/web.php file. For our Comments, we have three routes that we've defined, the post endpoint, the delete endpoint, and the put endpoint, and if we go to our terminal, and I run php artisan route:list,
and if we go to our terminal, and I run php artisan route:list, and I'm going to use the --name flag to say that we only want to show routes that have comments in the name, you'll see that we have those three routes here. Now, if you're using, again, a resource and you're using standard Laravel conventions, you can actually simplify this as well. At the top here, let's say route:resource,
you can actually simplify this as well. At the top here, let's say root resource, and the root resource is going to be posts.comments, and that's because we have posts, and then under them is the comments that we're controlling, and this resource, well, it links to our CommentController class, so we'll add that as a second method. Now, check this out. I'm going to comment out these three lines here,
Now, check this out. I'm going to comment out these three lines here, and I'm going to rerun php artisan route:list so that we can see the routes in our application again. We have, all of a sudden, seven routes generated by that single line here. Now, if you check out this POST request here, so posts, and then the post variable, and then comments, take a look at our routes.
and then comments, take a look at our roots. So here's a POST request, posts, post variable, comments. This is exactly the same as manually defining that root here. We've saved ourselves an entire line. Let's take a look at the next one. So here's the DELETE request, and it's comments forward slash comment.
So here's the delete request, and it's comments/comment. Now, when you actually look through and take a look at the delete request generated by our Route::resource method, it's prefixed with posts, and that's because, well, we've said it should be prefixed with posts. I talked about this briefly a few episodes ago, but we want to make use of something called shallow routing.
I talked about this briefly a few episodes ago, but we want to make use of something called shallow routing so that if you already have a Comment model that you're dealing with, you don't have to include the post ID in the URL. We can add that to a root resource by using the shallow method. So let's add that. Let's rerun php to some root list again, and now you can see wherever we have a comment variable,
Let's rerun php to some root list again, and now you can see wherever we have a comment variable, we don't include the post variable as well, which means that our delete request matches what we had previously, allowing us, once again, to remove this line completely. Finally, we have our put request. Let's take one more look here. Here's the put patch request,
Let's take one more look here. Here's the put patch request, comments / commentID. It matches what we had previously, meaning once more we can remove this. Now, one thing you will note is we also have other routes that have been generated, and that makes sense because if you go to the CommentController, well, most CRUD resources do have other methods available,
because if you go to the CommentController, well, most CRUD resources do have other methods available, for example, showing an index. The thing is with comments, we're actually displaying all of them on the Post show page, so there is no index for all comments. Because of that, we don't even need this index method. In fact, we don't need the create method because you create on the Post show page. We don't need the show method that we have here.
because you create on the Post show page. We don't need the show method that we have here because, again, you see a Comment in the context of a Post show page. We don't need the edit method because there is no single view, a single page or URL for being able to edit a Comment. We're going to do that inline on the Post show page. So what we're left with in actual fact is three methods, one for destroy, one for update and one for store.
Pruning Unused Policy Methods14:20
So what we're left with in actual fact is three methods, one for destroying, one for updating and one for storing. Meaning if we go back to our web.php file, we can say that the only methods we're actually interested in are the store, update and destroy methods. And now if we run php artisan route:list once more, we only have those three routes generated. In terms of refactoring, the last thing I want to do is jump into the CommentPolicy because we can also remove some of these policy methods.
the last thing I want to do is jump into the CommentPolicy because we can also remove some of these policy methods that are no longer required. For example, viewAny, which correlates to the index is not important because we don't have an index of comments. We can remove the view method for similar reasons. And then down at the bottom of our policy, we also have restore and forceDelete, which are only important if you soft delete your models.
we also have restore and forceDelete, which are only important if you soft delete your models. Our Comment models are not soft deleted. Once you delete them, they're gone from the database, meaning we can remove these as well. And we're left once again with a very clean class that is easy to update. So by sticking to conventions and by being willing to refactor existing code, we've been able to reduce a lot of manual code
and by being willing to refactor existing code, we've been able to reduce a lot of manual code that we would have had to keep track of and update over time to a much more elegant solution that's going to serve us well in the long run. Let's go ahead and rerun the 24 tests that we have. You can see they all still pass, meaning this was a successful refactor and we haven't broken any functionality by keeping the code clean.
and we haven't broken any functionality by keeping the code clean. I think that's our back end done, so now we can focus on the front end.
