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

TDD comment creation0:00

Alright friends, I think it's time we stop looking at paginated index pages and show pages and we start thinking about how we actually create new content as a User for our forum. One of the most fundamental parts, aspects, features of any forum is being able to post a Comment. And that's a great starting place because it's quite a basic form, it's quite a basic set of requirements in order to get it up and running. We could approach this UI first, so we could build the frontend and then create a backend to fit those needs. Or, and what we'll choose to do in this episode, we could use TDD to build the backend first and then in the next episode we can come and add the frontend on top. So let's jump in, create a test that will allow us to post a Comment on our forum. We'll need to create ourselves a new directory because we're now dealing not with Posts, but with Comments. So CommentController, and I'll stick with the cruddy keywords like index and show. In this case we are storing a new Comment, so this will be our store test, test.php. We'll open our php tags and then underneath we can create our first golden path test, that is the test that will work in the perfect circumstances.

In this case we are storing a new Comment, so this will be our storeTest.php. We'll open our php tags and then underneath we can create our first golden path test, that is the test that will work in the perfect circumstances. This is the ideal user path. So it can store a Comment, what will we need? Let's do our world building first. Well, you can only store a Comment if you are logged in as a User, if you have an account. So let's create a User, which will be User::factory()->create(). And then let's also create a Post because you have to have a Post to store a Comment on. So $post is equal to Post::factory()->create(). We'll need to log in as this User and we can use the actingAs helper to do this, passing the $user object that we want to log in as.

So $post is equal to PostFactory::create(). We'll need to log in as this $user and we can use the actingAs helper to do this, passing the $user object that we want to log in as. And then we can chain on a method to actually make the request. What kind of request is it? Well, it's going to be a POST request because we'll need to pass data in, in order to specify the body of our comment. The root name is going to be nested under posts, so let's say posts.comments.store. And because it's nested under posts, we'll pass the relevant $post here so that it can be a variable. The ID of the $post will be a variable in the root itself. The second parameter that we pass to POST is our array of data. And of course, in this case, the only thing we have to pass is the content of our comment.

The second parameter that we pass to Post is our array of data. And of course, in this case, the only thing we have to pass is the content of our Comment. This is a Comment will work absolutely fine for our test. Now, how can we validate after taking the golden path here that there is actually a Comment in our database? Well, there is a helper for that. So assertDatabaseHas is going to give us that exact functionality. We'll pass in the Comment fully qualified class name. And then once we've done that, we can pass an associative array of data that we'd expect to see. And each column, each key represents a column in our database. So we'd expect a body saying this is a comment.

And each column, each key represents a column in our database. So we'd expect a body saying this is a comment. But we'd also expect a post ID equal to the post ID at the top of our test. And we'd expect a user ID equal to the user at the top of our test. See? Now, with this in place, if we could get this to pass, then we'd be pretty sure that we had a root that created a comment in our database. Let's run this test. And obviously, it fails saying that the root is not defined. I'm going to leave the UI here at the bottom so that we can easily rerun this test. And then we can jump into our web root to start building out this functionality.

Define route and controller3:34

I'm going to leave the UI here at the bottom so that we can easily rerun this test. And then we can jump into our web root to start building out this functionality. So I'll leave a little bit of spacing, seeing as we're now dealing with comments. And let's say rootPost. We're going to post, obviously, underneath the post directory. It's a nested root. So we'll also need to pass the post variable and then comments, like so. And after that, we can point it to the controller. This controller doesn't exist yet, but let's go along with it. So CommentController::class, and we're looking for the store method.

This controller doesn't exist yet, but let's go along with it. So CommentController::class, and we're looking for the store method. And the name of this route is going to be posts.comments.store. Okay. Obviously, we're going to need to create this controller before anything else. So in order for artisan to work correctly, I'll need to just comment this line out for one moment. Jump into our console, and I can run php artisan make:controller. It's going to be called the CommentController. We'll make it a resource controller, and, of course, it links to the Comment model. Brilliant.

We'll make it a resource controller, and, of course, it links to the Comment model. Brilliant. With that in place, we can uncomment this line, and we can import the CommentController class. And if we jump into the store method, well, here we are. Now, by default, artisan isn't going to give you any insights when it comes to nested resource controllers. But because we know this is nested under Post, we can actually inject the Post directly at the top of the method here. Let's rerun our test.

Implement store logic4:53

we can actually inject the Post directly at the top of the method here. Let's rerun our test. And now we have a very different message. So this time it's failing, asserting that there was actually a new Comment in the database with the data that we've provided. The table is, in fact, empty. So let's create the most basic implementation. We could say something like Comment::create, and we could pass everything that's been given in the request. And tell you what, instead of create, let's use make.

and we could pass everything that's been given in the request. And tell you what, instead of create, let's use make. We'll save it to a variable. And then we could say comment User associate with the request User. And on the same lines, we could say comment Post, and let's associate that with the post that's been injected. And then finally, we call comment save. Right, let's run this. All right, it looks like we have the exact same error as before, but note we also had an exception that occurred during the request,

Fix mass assignment5:42

All right, it looks like we have the exact same error as before, but note we also had an exception that occurred during the request, and that was a mass assignment exception. What is mass assignment? Well, if you use Laravel and you have used Laravel for some time, you'll know exactly what this is. Laravel, by default, out of the box, it's going to prevent you from accidentally assigning bad data to your Eloquent models. So take a look at what we've actually done. We've grabbed all of the request data,

So take a look at what we've actually done. We've grabbed all of the request data, and we've created a Comment model from that request data. But the User could have passed anything in that request. For example, they could have set the id column to 1,000. They could have set the created_at column to something back in 1990. They could have set the post_id column, right? There's nothing stopping them doing that. It just so happens that we associate a Post after, so thankfully we'd have saved ourselves from that particular security threat.

It just so happens that we associate a Post after, so thankfully we'd have saved ourselves from that particular security threat. But as you think about it, you realize, yeah, this is a big security hole. Something very bad could happen here. We've given the User too much control. Thankfully, Laravel is going to say, oh, no you don't. I'm going to throw a mass assignment exception unless you specifically say that I'm allowed to fill a certain property. And obviously the property that we've passed in from the request front end is body.

unless you specifically say that I'm allowed to fill a certain property. And obviously the property that we've passed in from the request front end is body. So we need to say to the Comment, you are allowed to mass assign the body column when creating an instance. Let's jump into this Comment, and there is a property that we can override on Comment called fillable, which is an array of column names. And we just want to add body here. So we'll add body, we'll jump back, and we'll rerun this test. And now look, we have a passing test.

So we'll add body, we'll jump back, and we'll rerun this test. And now look, we have a passing test. So that's what mass assignment is. And there are multiple ways around this. This is just one of the ways using that fillable array. In fact, we probably will touch on one of the other options that I prefer to use a little later down the line. But for now, if you come across mass assignment, make sure you add the specific columns that you want to mass assign into the model's fillable property.

make sure you add the specific columns that you want to mass assign into the model's fillable property. But be aware that by doing that, what you're saying is, no matter where this data comes from, it's allowed to be assigned directly to a model in my database. Always good to know about these little security threats that pop up, and thankfully Laravel is very good at protecting us from them. So that's our golden path set up. If we jump back into our store test, we now need to create another test that will specify

Add redirect after store8:08

If we jump back into our store test, we now need to create another test that will specify that after the comment has been created, we're actually taken to the correct place. Because currently, nothing happens, which by default will return a 200 response, but the user is going to see nothing in the browser. So we need to redirect to the correct route. So let's go ahead and create a second test. It redirects to the post show page.

So let's go ahead and create a second test. It redirects to the post show page. That would make sense, right? You create a comment, you get redirected back to the post show page. So we'll open up our test closure, and we can copy the first part of this test. So we'll grab this here, and we'll drop it in. And we could inline this variable, because we're no longer going to be using that data. We'll make the post request,

because we're no longer going to be using that data. We'll make the post request, and then we want to chain on a second method called assertRedirect, and we'll pass the route of post.show, and then the post that we want to redirect to. And this is our second test. So let's run it. You can see it fails, because, yes, we returned a 200, because we've not actually implemented any logic yet. So we'll go back into our CommentController.

because we've not actually implemented any logic yet. So we'll go back into our CommentController. And down here, I'm going to use the to route helper, which I really like. It's so succinct, and the DX is lovely for it. And then we'll redirect to post.show, passing the $post that we want to redirect to. And now when we rerun this test, we are once again to a green passing state. Functionality-wise, the final thing we want to do here

Validate comment body9:30

we are once again to a green passing state. Functionality-wise, the final thing we want to do here is actually validate the data that's coming from the front end. There's only one property, body. However, as is, that body could be anything. The user could send a Boolean over. The user could send an integer over or null over. They might not even include the body at all. And as it stands, well, our code allows that. It would fail with a database integrity exception,

And as it stands, well, our code allows that. It would fail with a database integrity exception, but it would allow it. So instead, it would be better that we validate this data up front and we say to the user, look, no, you have to do this specific thing to this property, to the body, before we're allowed to process that information. And that, again, will help close up lots of little security holes that might otherwise pop up in our application and also stop bad data from ever hitting our database.

that might otherwise pop up in our application and also stop bad data from ever hitting our database. Now when it comes to validating request data, there is a brilliant method called request()->validate. But before I go into creating the rules for validation, I want to create a test that will ensure that these things are set up correctly. So back in our CommentController, I'm actually going to copy the test above and paste it below so that we have a nice starting point.

I'm actually going to copy the test above and paste it below so that we have a nice starting point. And let's change this to it requires a valid body. And this is fine, this is fine. We want to change the body here to be invalid so that we can test against this. So let's set it to null because obviously we require the body to be filled. And then we can check for invalid data by saying assertInvalid

And then we can check for invalid data by saying assertInvalid and passing the key that should have a validation issue, in this case, the body. All right, let's run this test. Currently it's going to fail because there were no validation errors, so everything was normal as far as the controller was concerned, even though we know there should have been an error. Let's go to the CommentController,

even though we know there should have been an error. Let's go to the CommentController, and inside the validate method here, I'm going to validate that body. And this is an array of any validation rules that we want to check against. For required data, well, we'll use the required rule. And if we rerun this test, it now passes. So that's the fact that it's required. But what else would we want to check?

So that's the fact that it's required. But what else would we want to check? Well, we might also want to check that the body is not a number. It has to be a string. It's not a Boolean, it's not a float, all of these different data types. It has to be a string. Now, we could copy this test, and we could paste it below, and we could change the body, but we're going to have a lot of duplicate code there.

and we could change the body, but we're going to have a lot of duplicate code there for basically a single line of code change in each test. Instead, we could make use of data sets in pest.php or data providers if you're using phpUnit. So we'll use the with method here, and I'm going to pass in different scenarios that our test needs to handle. So null is one of them, but we also have maybe an integer,

So null is one of them, but we also have maybe an integer, maybe a float, maybe a Boolean. Perhaps we also have a string that's too long. So let's set a maximum of 2,500 characters for a comment. We need to create a string that's 2,501 characters, so we'll use str_repeat here, and we'll repeat the A character 2,501 times. Okay, once we have all of our data defined, we can accept one instance of that here,

Okay, once we have all of our data defined, we can accept one instance of that here, and we'll call it value, and we'll replace our hard-coded null with value. With this in place, we can run this test, and you'll note that there's not one test executed but five tests executed, one for each of the items in our data set. Now, if you want more information on data sets, how they work, you can take a look at our Pest series on Laracasts,

Now, if you want more information on data sets, how they work, you can take a look at our Pest series on Laracasts, which goes into much more detail on how to get the most out of data sets. But with our data set in place, we actually have a set of tests we can use to ensure that our validation is working correctly for our body. So let's go ahead and jump back into our ComicController and add a new validation rule, string, for example.

So let's go ahead and jump back into our ComicController and add a new validation rule, string, for example. And with that one rule in place, you'll see we go from four tests failing to four tests passing, because now all of these other data types are not allowed. Only a string is allowed. When it comes to validating the length of the string, we can use the max rule, and we need to pass the maximum length, which is 2,500,

Refactor and unguard models13:44

we can use the max rule, and we need to pass the maximum length, which is 2,500, and hopefully, yeah, there we go. We now have five out of five tests passing, and we're pretty confident that all of this is working as expected. The last thing I want to do is the refactor step, and this is a very important step when it comes to test-driven development, because it ensures your code stays clean.

when it comes to test-driven development, because it ensures your code stays clean. So once you have your tests in place, once you have a basic implementation, go back and refactor until you're happy. So I'm going to run all of these tests together, and I'll bring it down here so that I can see more of the screen, but at a moment's notice, I can rerun all seven of these tests

but at a moment's notice, I can rerun all seven of these tests to ensure that everything works after a refactor, and then we'll jump back into the CommentController, and we'll begin looking at what we might want to change. So one thing I know for sure is that validate returns an array of validated data, so I'll set that as a variable, and instead of passing everything from the request in, I'm just going to pass in validatedData to comment make.

and instead of passing everything from the request in, I'm just going to pass in validated data to comment::make. I guess I could also chain things onto make, so rather than doing this, I might actually be able to go for something a little cleaner like this, and then I could actually chain save on as well. So let's go ahead and do that so I can get rid of this here. I can actually get rid of the temporary variable.

so I can get rid of this here. I can actually get rid of the temporary variable for comment now as well, and hopefully if we rerun our tests, all seven of them are still passing. This was a successful refactor. Okay, it's looking nice. Some might argue that because the validation is so short, you could technically make that a one-liner, and you might even say that,

you could technically make that a one-liner, and you might even say that, well, why don't you just pass that directly rather than having a temporary variable? Again, that's completely up for debate, but if we run this again, we still have all seven of our tests passing. Now, seeing as we are now validating this data, and the only thing that can get through is the body property.

and the only thing that can get through is the body property. If you are absolutely sure, completely positive, that every time you're going to be storing or updating or altering data from the User you will validate, then mass assignment exceptions aren't actually much of an issue anymore. So let me preface this by saying

aren't actually much of an issue anymore. So let me preface this by saying what I'm about to do is completely optional, and if you want to stick with using the fillable property on the Comment model or on any of your models, go ahead and do that. There is nothing wrong with this approach. But for me, it gets a little tedious filling out this array each time, and because we have the tests in place,

filling out this array each time, and because we have the tests in place, because I know I'm going to be validating my controllers, I'm actually going to use an alternative approach. For that alternative approach, I'll jump into the AppServiceProvider, and in the boot method here, I'm going to grab the base Eloquent model, and I'll use the unguard method. This basically turns off

and I'll use the unguard method. This basically turns off the mass assignment exception checks, so we've completely disabled that feature inside the application code base, which means if I go back to the Comment and remove this fillable array, and then we rerun our tests again, well, everything still passes, because now we're no longer

well, everything still passes, because now we're no longer doing that mass assignment check. But it also means that I could replace this code here with something a little more succinct, because I could declare all of it inside the make method. So again, this is completely optional, and this is what refactors are for, right? You need to decide, is this what I prefer,

and this is what refactors are for, right? You need to decide, is this what I prefer, or would I prefer an alternative approach? So in this case, I'll use make, I'll spread out the validatedData, but then I'm going to set the postId to the postId, and I'm going to set the userId to the request->userId, allowing me to remove these lines here,

to the request user ID, allowing me to remove these lines here, along with the save method, and I can return to using the create method instead. Let's rerun the tests, everything still passes, and I'm pretty confident that this is feature finished, and at least to my eyes, if I remove the UI at the bottom here, this looks very clean.

if I remove the UI at the bottom here, this looks very clean. We would come into this code, and we would very easily and quickly be able to understand what's taking place, what's happening, and how to make changes as and when necessary. And again, it's completely up to you whether you have this line here, or whether you extract this and perhaps

whether you have this line here, or whether you extract this and perhaps place it a little further down like so, and then spread data out underneath. And maybe, yeah, maybe I'll keep it like this, because this is its own separate step, so it stops it getting lost in the Comment create life cycle that we have here. Let's run our tests one more time

comment create life cycle that we have here. Let's run our tests one more time to make sure everything's happy, and with that done, we have the back end constructed for our comment creation process. In the next episode, why don't we tackle the other side, the front end, so that we can go into our browser, we can post a comment from a form, and see it appear in that paginated list.

we can post a Comment from a form, and see it appear in that paginated list before our very eyes. See you there.

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