Reset and Rebuild Feature0:00
Let's now take everything we learned in the last episode, so in ConfirmablePasswordController, all of this functionality here, even down to the requirePassword middleware, and let's see if we can re-implement it from scratch. Not to keep it, but exclusively as a learning exercise, to get ourselves into the workflow and the routine of implementing a feature you can understand. Almost like a kata. When we're done, we're going to delete everything, because Laravel already provides this out of the box. All right, let's get going. We're going to start with, well, let's reset, excuse me, let's reset a few things here.
All right, let's get going. We're going to start with, well, let's reset, excuse me, let's reset a few things here. In our auth file, we're going to rewrite these routes, and then in ConfirmablePasswordController, we will get rid of all of this. We'll just keep the controller wrapper and reformat. Okay, so I think we're good here. The only thing I will keep is PasswordConfirmationTest, so that the test can guide us a bit. All right, let's get going. The Confirm Password screen can be rendered. We run it, and we get a 404.
Debug Missing Route1:03
The Confirm Password screen can be rendered. We run it, and we get a 404. Now right off the bat, notice, okay, the test failed, but it didn't really give us much information. So a little tip, you can always add the withoutExceptionHandling method to disable the feature where Laravel will swallow an exception that's thrown and convert it into the proper response. Instead, let's just let that exception ride. Now you can see, it fails immediately. There is no route that responds to a GET request to confirm password. Okay, of course not, we deleted it.
There is no route that responds to a GET request to confirm password. Okay, of course not, we deleted it. So let's re-add it, again, like a kata. So respond to confirm password, and that will hit ConfirmablePasswordController, and then an action called show. All right, let's give it a run. No show method exists. All right, let's go in there now, run it again, and now it passes. Because remember, that test is just making sure that a 200 response is thrown. So for example, if I abort, well now the test is going to fail.
Because remember, that test is just making sure that a 200 response is thrown. So for example, if I abort, well now the test is going to fail. So really it's just making sure, do we have a route set up for that? Does the endpoint run? Let's make sure no error is thrown in the process, and that's all it's doing. So let's move on to the next test. Test password can be confirmed. All right, now a 405 is being returned, but the exact same thing. So in terms of without exception handling, you might decide, let's just put this on the parent test case within a setup method, so that it applies to every single test.
So in terms of without exception handling, you might decide, let's just put this on the parent TestCase within a setUp method, so that it applies to every single test. And then conditionally, when you do want Laravel to swallow any exceptions, you can manually add with exception handling, which is the inverse. It's just up to you. For now, we're going to use it only when we need to. And in fact, you might often temporarily apply it, and then remove it when you're done with the test. All right, so let's run it again, and now we should get better feedback. There is no post method supporting the given route.
Add POST Confirmation2:56
All right, so let's run it again, and now we should get better feedback. There is no POST method supporting the given route. Okay, fair enough. We're trying to submit a POST request to this endpoint, but we only have a GET request registered. So a couple things. First, we know this is linked to authenticated Users only. But if I duplicate this, let's now listen for a POST request, and that will hit a store action. All right, run it again. And now there's no store action.
Validate and Set Session3:46
And there shouldn't be any errors in the session, because nothing went wrong. Okay, let's come on back. And we can think of a few things that need to happen here, now that we've learned the basic flow, right? We need to validate the user's email and password. So to confirm your password, let's make sure you gave us the right password. If you didn't, we have to throw a validation exception. But if you did, we know ultimately we need to store the current timestamp in the session. And that way we can say, all right, for the next three hours, you're good. We won't require that you reconfirm your password.
And that way we can say, all right, for the next three hours, you're good. We won't require that you reconfirm your password. And then finally, we redirect you. So redirect you where you intended to go, but default to dashboard if that's no longer in the session. Next, we need to validate. So we could just do, and this isn't going to be verbatim, what the original controller had. I'm not copying it one-to-one. And that's fine.
I'm not copying it one-to-one. And that's fine. Oftentimes the code that ships with the framework is intentionally, what would be the word, generic so that it can fit every possible use case. Whereas if you're writing your own project, you maybe already know that you're only using Eloquent and not a different provider, but the framework won't know that. So it will change how you write your code. In our case, we're going to validate, and we need to give it the email. And that will just be, notice we didn't have an email field on that form. We don't need to.
And that will just be, notice we didn't have an email field on that form. We don't need to. We can just grab it directly off of the authenticated user. Next, the password will be in the request. So we could just say, request->password, and actually, yeah, on that note, we could do it like this. And that'll give us the same user, the user associated with the current request. That's how I think of it. So try to validate the user. And then we could say, isValid.
So try to validate the User. And then we could say, is valid. Let's just loosely write this, and then we'll refactor how we need to. If they are valid, then record the current timestamp, and then redirect them where they intended to go. So let's give this a run, and I think the test will pass, and it does. Great. Let's move on to the next one. TestPassword, and we can get rid of that. TestPassword is not confirmed with an invalid password.
Test password, and we can get rid of that. Test password is not confirmed with an invalid password. So we give that a run, and nope, we don't have anything in the session, but we expected to. All right, let's come on back, and we'll say, well, look, if we failed validation, or even we could rename that to say, User exists. If a User does not exist, then let's throw a ValidationException. So we could do it like this, but we could also say ValidationException with messages, and this is kind of a cool technique. So when you create your own exception classes, you can add a static method that within it
and this is kind of a cool technique. So when you create your own Exception classes, you can add a static method that within it is still going to do right here. It's still basically saying new Exception, but in this case, it wraps some extra work it has to do, where you accept messages, and those messages you provide are then added to the validation error bag. It's kind of a cool technique. So anyways, if we come on back, our error bag, we just want it to have one for password, and I'm going to hard code it. Could not find a User.
and I'm going to hard code it. Could not find a User. So we run the test, and that passes as well. So this is kind of what we get. We can maybe even inline this. Let's see what that looks like. Mm-mm. Don't like it. Instead, why don't we extract a method called, well, let's just start with userExists, like this.
Instead, why don't we extract a method called, well, let's just start with userExists, like this. I can then inline that, yeah, and I don't think we need wrappers there. Is it going to make me add that? Yeah. Run it. That test still passes. Inline this. I wish phpStorm would figure that out. There's no scenario where people ever want this, at least, I don't think.
I wish phpStorm would figure that out. There's no scenario where people ever want this, at least, I don't think. You should detect if you immediately return a variable, then you can inline it automatically. All right, get rid of that. And let's see how we're doing here. Let's run everything. And those are passing. Okay, great. So yeah, again, just be careful. These are very simple tests.
Strengthen Tests and Routes7:50
So yeah, again, just be careful. These are very simple tests. So notice, at no point do we ensure that the form actually loads. You might want to do that as part of an acceptance test, or you could even do things like, let's go into that view. You could even assert that some specific text is there. So for example, please confirm your password. We could come on back and say, okay, well, when we load that show action, yes, it should return a 200, but I also want to see this text here. So now, notice if I run this, it's going to fail, and that might be useful.
return a 200, but I also want to see this text here. So now, notice if I run this, it's going to fail, and that might be useful. Just keep in mind, be thoughtful with this, because now the text in your view is coupled to your test. So for example, if you later decide, or if your designer decides something like, now, confirmPassword before continuing, well, that innocuous change actually broke the tests like this. And actually, it would have failed either way. But real quick, if we make it pass, return view confirm password. So that will pass.
But real quick, if we make it pass, return view confirmPassword. So that will pass. No, it still didn't pass. Hmm. Okay, let's fix this, and then we'll roll on back. So here, I'm going to disable exception handling to get a better idea of what the issue is. Oh yeah. So it looks like the view is referencing this named route, but we haven't named any routes. Okay. So let's come over here, and we'll notice the endpoint is the same, right?
Okay. So let's come over here, and we'll notice the endpoint is the same, right? So for both of these, it's confirmPassword. Now you'll see in the view, where is it? Yeah, this form is going to submit a POST request to that URL. So I'll show you what I often do. You might start by saying, well, let's give this a name, password.confirm. But instead, I will often do that to the GET route. Because it's going to be the same either way, right? If I submit a GET request to this route name, it'll go to confirmPassword.
Because it's going to be the same either way, right? If I submit a GET request to this route name, it'll go to confirmPassword. And if I submit a POST request to that route name, it still goes to confirmPassword. But because it's a POST request, we will respond uniquely. All right, let's give that a run, and now that passes. Okay, so anyways, coming back to a couple minutes ago, if your UI person or your front-end person decides, where is it, decides to change, to make some kind of innocuous change, now your tests have failed. Because the view is linked to the underlying test. And sometimes that's okay.
Because the view is linked to the underlying test. And sometimes that's okay. Sometimes that's exactly what you want. And then other times, eh, not really ideal. So you'll need to measure that yourself. Nonetheless, all of our tests should be passing. Okay, so we've effectively reproduced kind of a quick reproduction of the ConfirmablePasswordController. At least everything except this portion. And that's our next step, to reproduce the middleware.
Implement Require Password Middleware10:47
At least everything except this portion. And that's our next step, to reproduce the middleware. So let's come on back to routes. And like we did in the last episode, you'll remember, just as a quick demo, it doesn't make sense to put it here, normally you would do it when you're doing an important change or a charge or a destructive action. But we're going to do it on the dashboard, just to be quick. Our final step is to manually reproduce this middleware. So I'll give you a little tip. You can use these key-based middleware, but you could also just say, require password,
So I'll give you a little tip. You can use these key-based middleware, but you could also just say, requirePassword, like this. And why don't we go ahead and create our own. Make middleware requirePasswordConfirmation. We'll even be a little more lengthy there. Okay. User's passwordConfirmation. Okay, so now, if we have a look at the handle method, and this is where we can say, well, if we need to confirm or reconfirm the User's password, then redirect, otherwise continue.
Okay, so now, if we have a look at the handle method, and this is where we can say, well, if we need to confirm or reconfirm the user's password, then redirect, otherwise continue onward, which is basically this section here. Okay. How do we check if we need to reconfirm the user's password? Well, we already learned about this in the last episode, right? We just need a timestamp that represents the last time the user confirmed their password. Then we could say, well, if the confirmThat variable, we'll have to create that, is greater than our timeout, and the timeout would be 60 seconds times 60 minutes and an hour times three hours, which is what Laravel defaults to, 10,800.
than our timeout, and the timeout would be 60 seconds times 60 minutes and an hour times three hours, which is what Laravel defaults to, 10,800. So if it's greater than that, then it's time to reconfirm. And that's where we redirect. So we do something like redirect to the given route, password.confirm. But this isn't quite right, is it? It just occurred to me in our controller, we were doing that thing where we say, okay, you now have a valid password for three hours, let's redirect you where you intended to go. In order to work with intended, we need to make a call to guest. So we could say, redirect guest, and then provide where we're going.
So now the only remaining step is this. How do we figure out when you confirmed that? This is a very common pattern you'll use all the time. You'll do something like, all right, take the current time and subtract a different timestamp that represents when one of the users did something. So that's when we might reach for the session. And a key, I think Laravel calls setPasswordConfirmedAt. And then the second parameter will be the default, and that can just be zero. So subtract the current timestamp from the timestamp when you were confirmed, and that will give you how many seconds it's been.
Oh, it didn't work. I must have forgotten something. Oh yeah, of course I did. We forgot to actually store this key in the session. That's the final step here. session, auth.password.confirmedAt will be the current time. Okay, I bet that will work now. There we go. So now no matter how many times I load my dashboard, it won't make me reconfirm for another three hours.
And again, the goal isn't to keep any of this. It's only to get ourselves in the workflow of writing code and testing code and refactoring code. That's when the real learning is done. So here's our middleware. There is the reproduced ConfirmablePasswordController. Pretty simple, but it works. And I think that's going to do it for this series. So again, if you want to improve as a developer, you need to do a lot of learning, a lot of reading, and a lot of writing.
