Motivating Validation Refactor0:00
Hey, everybody. All right, I'm going to warn you, we have a lot to cover in this video. Some new concepts, some new techniques. So, yeah, with that in mind, let's skip the intro entirely and dive right in. Okay, so once again, we are back within the same controller that handles logging in a User. But this time, I want to point your attention to all of the form validation crap. And I call it crap not because it's bad, but just because there's so much here. I mean, let's think about it. I instantiate the form, and then I call validate. And then in the event that validation fails, we redirect back.
I instantiate the form, and then I call validate. And then in the event that validation fails, we redirect back. But of course, as we learned, we need some way to also pass the validation errors, as well as the old form data, along with that redirection. And now I want you to think about a real-life project and the number of forms that you will have. For example, for Laracast, there's a form for registration, a form for checkout, a form for login, a form for gift certificates, a form for leaving a comment below this video, a form for posting to the forum, a form for updating your profile and your account details, or adding a team member if you have a team account, which are $75 a person, by the way.
Inline Form Validation1:36
All right. So what if we took a slightly different approach? What if we took this form validation and moved it up a level like this? And then I got rid of the conditional entirely. Okay. Well, how would that change things? Well, I instantiate a form. I call validate. But then I immediately move on to authenticating the user. So, yeah, because we've removed that conditional, now if form validation fails,
Well, maybe we could tweak things a little bit. For example, maybe we could inline all of this. So what if I could just say loginForm validate? All right. That would be good. But, yeah, of course, right now that's not going to work. So let's see if we can make it work. If we change this to a static function, well, this then becomes what we would call a static constructor. It's just a static entry point to instantiate your class.
a static constructor. It's just a static entry point to instantiate your class. Okay. So if we added a constructor here, and then, yeah, I would have to accept the email address and the password. However, you may remember a number of episodes ago, I discussed an alternative approach where we instead accept an array of attributes. Why don't we consider that approach instead? Okay. So if we took that approach, the validate method would also accept an array of attributes.
Okay. So if we took that approach, the validate method would also accept an array of attributes. And then maybe all of this validation here could instead move up to the constructor, like so. So now whenever you instantiate this form, we immediately validate it and populate the errors. Okay. So now this would update to this attributes email, and then this attributes password. Cool. That's kind of a cool approach.
Cool. That's kind of a cool approach. So now when we instantiate LoginForm, we instantly validate any of the attributes that are relevant to the form. All right. So that means when I call this static constructor, the first thing we would do is instantiate the class. Okay. So now we have a dedicated instance. And now, yeah, maybe we can tweak the API a little bit, because before this validate
Throwing Validation Exceptions3:58
So now we have a dedicated instance. And now, yeah, maybe we can tweak the API a little bit, because before this validate method returned a Boolean. But we're changing it up a little bit. So maybe instead, if validation fails, an exception would be thrown. Okay. So let's see. How do we determine if the validation fails? Well, let's see. Do we have any method?
Well, let's see. Do we have any method? No. So we would have to check if we have any errors in this array. Okay. Well, again, we're going to refactor a lot of this, but we would start by saying, well, do we have any errors? And if that's true, that means validation failed. Okay. So with that in mind, maybe we should wrap that up within a helper method.
Okay. So with that in mind, maybe we should wrap that up within a helper method. Something like, I can think of a few options, like hasErrors. That would be a common approach. Another option would be something like failed. Did the validation fail? Okay. Either one would be fine with me. So yeah, we could just grab that and move it down here and then return. Okay.
So yeah, we could just grab that and move it down here and then return. Okay. So this failed method returns a Boolean that indicates whether or not the form validation failed. Okay. Nice and clear. So let's see. We could say, well, if the form, the login form validation failed, well, like I said, we might throw an exception. Okay.
we might throw an Exception. Okay. So you learned how to throw an Exception many episodes ago. Something like this. However, I'm sensing that we might need to do a little more with this Exception, and we'll need to pass through a little more as part of this Exception. So with that in mind, why don't we create a new custom Exception class? And I'm going to put it right in here as part of the core directory. But yeah, we're starting to see now we have multiple files and classes related to validation.
But yeah, we're starting to see now we have multiple files and classes related to validation. And at that point, you might consider creating a dedicated validation directory as part of your core folder. But yeah, for now, let's not do that. Okay. So this is, well, what is it? It is a FormException, FormValidationException. Why don't we stick with, how about ValidationException? All right.
Why don't we stick with, how about ValidationException? All right. Now, if we want this to be a custom exception, we need to reach for inheritance. This will extend Exception. Now, I think we've only very, very lightly touched on inheritance in this series. I promise we will talk about it more. But for the time being, just think of it like this. ValidationException is an Exception. And as part of that, it inherits behavior that comes from this parent class. And yeah, that's a way we often think of it.
And as part of that, it inherits behavior that comes from this parent class. And yeah, that's a way we often think of it. This is the child class, and whatever it extends then becomes the parent class. So now we have this ValidationException. We'll come back to it and work on it a little more. But if I switch back to the login form, now I'm not going to throw just a plain old exception. I'm going to throw a ValidationException. All right. So next, right here, what if the form instead is valid? What should we do?
So next, right here, what if the form instead is valid? What should we do? What should we return in that case? And yeah, this is where we start designing our API. How do we want this to feel? For example, do you want to return true? Do you want to return the instance itself so that you could further chain and call methods on this class? It just depends. And at the moment, I'm not sure.
It just depends. And at the moment, I'm not sure. Maybe it returns true. Maybe it returns the instance. It just depends. Okay. But for now, let's stick with the instance and then come back to our controller and take a look. All right. So now we're calling validate, but you'll notice it's squawking here because we should be passing through an array of attributes.
So now we're calling validate, but you'll notice it's squawking here because we should be passing through an array of attributes. So let's do that now. $email is $post->email, and $password is $post->password. All right. So now it seems a little redundant to declare these variables and then repeat ourselves. So maybe I can get rid of that entirely. All right. But now if we do that, well, right here, we were expecting the $email and $password. So maybe, what if we just inline assign this to an $attributes variable?
But now if we do that, well, right here, we were expecting the email and password. So maybe, what if we just inline assign this to an attributes variable? And then here, I can pull the email address out and the password out. Would that be one way to handle it? And I think the answer is yes. Okay. Next, if I come down here, well, I still wanted access to that form variable. And luckily, we did return the instance if validation is successful, which means I could save this to a variable called form. And yet, that removes the error that we see there.
which means I could save this to a variable called form. And yet, that removes the error that we see there. Okay. So now at this point, I would expect you to be thinking, all right, fine, we made these changes, but why? What is the benefit to this approach? And I'll tell you. But first, let's test this out in the browser and force the validation to fail. Okay. So with that in mind, let's go in here.
Okay. So with that in mind, let's go in here. And then just temporarily, why don't we say the password has to be at least 100 characters, just so I will instantly fail validation. All right. Let's go to login and then provide a password that will, of course, not meet that validation rule. Okay. And yeah, it looks like we actually have a few issues that we need to fix. First up, undefined property on line 14 of login form.
And yeah, it looks like we actually have a few issues that we need to fix. First up, undefined property on line 14 of login form. All right. I'm going to go to line 14. And oh, yeah, obviously, you saw this and you were waiting for me to run into it. All right. What else do we have here? If we give it a refresh. Oh, yeah. Okay.
Handling Exceptions in Controller9:52
So now the next question is, well, how do we deal with that? Well, I'm going to show you this in two steps. The first option, of course, is to simply wrap this within a try catch. So try to execute this logic, but catch a ValidationException. And then do whatever you need to. So caught it. Just to show you how this works. Now, if an exception is thrown, we will catch it and we can respond however we want. Okay. So that means we could do things like this, where if validation fails, flash the errors.
Okay. So that means we could do things like this, where if validation fails, flash the errors and flash the old data. And yeah, here I'll just grab that out. Attributes, email. And then what? Well, redirect back to the login page. Like so. Okay. So this doesn't yet handle this section right here.
Okay. So this doesn't yet handle this section right here. But again, we're just taking this one step at a time. All right. So now if we come back and reload, yeah, we get a different warning. Undefined variable form on line 14. All right. Let's go to line 14. And yeah, it seems like at this point, form is undefined. And it could be a little confusing because we defined it right up here.
And yeah, it seems like at this point, form is undefined. And it could be a little confusing because we defined it right up here. So what's the problem? Well, the problem is we said create this form variable and make it equal to whatever is returned from this validate method. But we never even got to the point where we returned. We instead threw an exception. So yeah, at that point where we handle the exception, we don't have access to that form variable. And just to prove it to you, if I die and dump form, what are we going to get?
variable. And just to prove it to you, if I die and dump form, what are we going to get? Null. Not what we want. Okay. So now it seems like we need to fetch the errors and then potentially the old form data off of the exception. How could we do that? Hmm. Let's go into our exception class.
Hmm. Let's go into our Exception class. And it's just empty at the moment. What if we added another static constructor? And this can be anything you want. It could be with errors and old data. I don't really like that. But yeah, whatever you want. This static constructor will be called throw. And that would allow me, if I switch back here into LoginForm, it would allow me to
This static constructor will be called throw. And that would allow me, if I switch back here into login form, it would allow me to change this to validation exception throw. And then when I call that method, I can pass through any extra data that I require. This would then do something like, well, very similar to what we had before. Create a new instance and then throw the instance. Okay. So now this and this implementation are functionally identical. Cool. But next, yeah, maybe I could pass through the errors as part of this.
Cool. But next, yeah, maybe I could pass through the errors as part of this. So I could send through. And remember, I can't call this because I'm in a static method. But I do have an instance variable that is effectively this. So I could say instance. And then we have an errors method, don't we? So why don't we call that method instance->errors()? And then I will accept the errors here. Okay.
And then I will accept the errors here. Okay. So why don't we assign it? We could do protected $errors and initialize that to an empty array. And then I could say, well, create a new instance, set the errors. And then while we're here, why don't we also pass through the old form data? Well, how do we get that old form data? Let's scroll up. And yeah, well, we have the attributes right there, don't we? But I don't have easy access to them.
And yeah, well, we have the attributes right there, don't we? But I don't have easy access to them. Why don't we make this publicly accessible? So I could either do this. And then I could define the attributes right here. Or in PHP 8, I can also just do it as part of this. So public array. And I don't think we've covered this because we haven't yet talked about types, which is at the very end of this series. But for now, I'm going to keep it like this, even though I am using a type.
which is at the very end of this series. But for now, I'm going to keep it like this, even though I am using a type. Think of this and what we had before as equivalent. So it is a public property. But we're also going to give it a type. And that type is an array. And this just means I expect whatever we instantiate this loginForm with to be an array. Not a number, not a string, not a function. It should be an array. And that's what a type is.
It should be an array. And that's what a type is. Okay, and we'll talk about that a little more later in the series. Okay, so now my instance has access to the attributes. And yeah, if you wanted to, that means if you wanted to take this approach like we had earlier, that would still work. But this is fine. Okay, so now if I switch back to one split, yeah, we can pass through those attributes. Instance, attributes. All right, so now if I go into ValidationException, we will accept them.
Instance, attributes. All right, so now if I go into ValidationException, we will accept them. And just to be consistent, I will call it old. But yeah, old form data, stale data, whatever you want to call it is fine with me. Okay, so now let's assign it. old equals that array. And then I can update it here. All right, so cool. Now when the login form throws an exception, it will pass through the errors and the old data as part of it.
Now when the login form throws an exception, it will pass through the errors and the old data as part of it. All right, so now I think we can finally solve our problem. If I come back to the controller, yeah, remember we had that issue before where we don't have access to the form variable from up here because an exception is thrown before we could assign to it. But yeah, now I can grab the errors and the old data off of the validation exception. But yeah, it's not as simple as just doing this. I can't say exception errors because, and you can see right here, errors is marked as protected,
I can't say exception errors because, and you can see right here, errors is marked as protected, which means it's protected from being accessed from the outside as we're doing here. So you have a couple choices. One option is just make it public. That could be fine. Option number two is to create a getter. So this would be a method that simply gets your protected value. It wraps it up and gets it. If you need to do anything before you return the errors,
It wraps it up and gets it. If you need to do anything before you return the errors, now you have a place and a hook to do that. That would be fine. Another option would be to declare it as public. But then you can also say, well, it's read-only, which means we can assign to it once, and then you can never update this value again. And often that solves the problem and the reason why you would create the getter in the first place.
Moving Try-Catch to Front Controller17:20
but why? Like, is this necessarily better than what we had before? And I'd say the answer is, well, no. But we're not done yet. What if we graduated this try-catch up a level so that I don't always have to do it as part of my controller logic? Well, once again, let's open this in a split, and then I'm going to return to public/index. So right here is where we perform our routing. It's where we load the controller, and we require this file.
So right here is where we perform our routing. It's where we load the controller, and we require this file. So why don't we perform the try-catch here, like this? Try to route to the controller, but catch, and what do we want to catch? A ValidationException. We're not going to handle that as part of our entry point file. Okay, so let's see. We would take all of this, and this would move here. And let's make sure that's imported.
This is just a simple explanation with no code.
Does that still work? Let's come back, refresh, submit it. And yeah, it all magically works the way it did before, but now this is starting to look a lot better in my mind. So I can simply call this validate method with the understanding that if validation fails, we will flash the errors, we will flash the old foreign data, and we will redirect back. Okay, but notice I didn't say redirect to the login page.
Hmm, well, let's do this. I know what the answer is, but I'm going to show you how you can figure out what the answer is. Let's die and dump the $_SERVER super global when an exception is thrown. All right, submit it. And yeah, here's everything we have. So let's look and find and search to see if I can find the previous page. All right, there's login, and yeah, it's the only reference.
to see if I can find the previous page. All right, there's login, and yeah, it's the only reference. And it looks like it has a key of HTTP_REFERER. All right, why don't we use that? All right, let's come back, get rid of this, and I will redirect to server and then HTTP_REFERER. However, I do have this Router class. So could I just tell the Router to do that? Hmm, let's see. Let's go into router.
Hmm, let's see. Let's go into router. And yeah, maybe I could add a method. Where should we put it? I don't know, maybe right here. And maybe the router can know what the previous URL is. Or what the previous URL was, okay? Well, if that's the case, I can grab this like so and return that value. Yeah, that would be one option.
and return that value. Yeah, that would be one option. Maybe the router can perform the redirect itself if that would make sense. But for now, it's just going to return a string. That would allow me to say redirect to the previous URL. Okay, so with any luck, if we come back and try this, but now the process of redirecting back to the previous form is dynamic, and we're no longer hard coding the login URI. All right, so try to route to the controller,
and we're no longer hard coding the login URI. All right, so try to route to the controller, but catch any validation exceptions that might be thrown. And here, in a single place, not every controller, but a single place, we can flash the errors and flash the old data and then redirect back. That's one way that we can solve this problem. All right, so if I come back to our controller, take a look at this. It's starting to look pretty good.
take a look at this. It's starting to look pretty good. So we begin by validating the form. And if we get to this point, that means no exception has been thrown. So the next step is to attempt to log in the User. But now, what about this? We have an issue here. So let's work through it. Let's come back and remove this minimum so that I can pass the validation and proceed to the next step.
Auth Failure Triggers Exception21:37
Let's come back and remove this minimum so that I can pass the validation and proceed to the next step. And let's just see what happens. I'm going to try to sign in Joe with a gibberish password. And yeah, it redirects back, but there's just no feedback here. Okay, well, let's think about why. If the authentication attempt failed, yes, we do append to the login form errors list, as you see here. But we've now tweaked things to the point that we only redirect back if the validation exception is thrown.
But we've now tweaked things to the point that we only redirect back if the ValidationException is thrown. Okay, so with that in mind, maybe we need a way to manually throw that exception or manually call a method that will throw the exception. Okay, hmm. All right, well, why don't we do this? Let's add a method, and we'll call it throw. And then this logic here can move within it. And then I can update instance to be this
And then this logic here can move within it. And then I can update instance to be this because I have an instance method here, not a static method. Okay, so now if it failed, I could say instance throw, like so. And then at this point, these two can be combined. I could say return instance failed. Well, in that case, throw the exception. Otherwise, throw the instance. And that is equivalent. Okay, so now we have a way to perform validation.
And that is equivalent. Okay, so now we have a way to perform validation. We have a way to manually add an error to the validation errors list. And now we have a way to throw the exception. Okay, so now let's switch back to the controller. And then right down here, if the authenticator failed, we append an email to the errors list. And let's do this. Let's return the current instance so that I can continue chaining. That would then allow me to say form->error throw.
But in this case, I'm doing the happy path before what is effectively the guard clause. So why don't we extract all of this into a variable like signedIn, like so. And then I could say, well, to reproduce what we had before, if you were signedIn, then good, let's redirect. But let's tweak it. Let's say if you weren't signedIn, so I will move this out of it. In that case, append to the form validation errors and then manually throw that exception. Otherwise, we can continue on with the happy path.
This is just a simple explanation with no code.
So tell me what you think. We validate the form. We attempt to log in the User. And then if it fails, we return back to the previous page. Otherwise, we send them on to the home page. So I want you to consider this and then what we had earlier. And to show you that, we will visit GitHub to a previous commit. And yeah, this is what we originally started out with. And I want you to compare it to what we ended up with right here. Okay, I don't want you to just compare the lines of code,
And I want you to compare it to what we ended up with right here. Okay, I don't want you to just compare the lines of code, even though that matters. I also want you to compare the clarity. Notice that with this approach, we are telling things to happen. So we're saying login form, validate, authenticator, attempt to sign them in, form, add this error. Notice that we are being good managers, so to speak. We are properly delegating. Whereas with this former approach, we weren't delegating at all.
