Skip to content

Instantly share code, notes, and snippets.

@shanerbaner82
Last active March 29, 2020 21:32
Show Gist options
  • Save shanerbaner82/2f3764298e85957e30253b8cba7ee81c to your computer and use it in GitHub Desktop.
Save shanerbaner82/2f3764298e85957e30253b8cba7ee81c to your computer and use it in GitHub Desktop.
Who are you? The validation story.
Validation is a way to ensure that the data submitted from your front end forms or requests meets your expectations and ensures that we are storing the right data in our database. When a form or request is submitted, before we run any logic with that data we need to make sure it has meet all of our requirements. If the data does not meet our requirements, we need to tell the user there is a problem with the data they submitted so they can fix it.
### The Project
Currently I am working on a SAAS app called SavvyHouseHunting.com - it is a platform that allows real estate agents to create and share 360 videos and images, documents and messages with their clients. One of the key differences with this application is that it is a Vue SPA, leveraging Vue Router, so we aren't able to use the Blade helper `@errors` as we would in a blade view.
In one area of the site users of certain types are allowed to "invite" people to join their team, upon receiving the invitation the user needs to provide some basic user data in order to get started.
### Getting started
First, since we don't want users to accept the invitation more than once, when generating the invitation email we create a [Signed URL](https://laravel.com/docs/7.x/urls#signed-urls) like so:
```
$invitation = Invitation::create([
'to' => request('email'),
'user_id' => auth()->user()->id,
'type' => request('type')
]);
$link = URL::signedRoute('accept-invitation', ['invitation' => $invitation->id]);
Mail::to(request('email'))->send(new InvitationMail(auth()->user(), $link));
```
The email will fire off to the invited user, with the signed url, and when the user clicks the link in the email we receive that request and render our form. But to ensure that the signature is valid, we can use a simple `abort_if` method which takes a boolean, an http status code, and an optional message to send back to the user.
### More Logic
Additionally, we need to lookup the `invitation` and make sure it hasn't been accepted yet, there is an `accepted` column on the model that we mark as true later in the process. One additional caveat to allow for is in the scenario that this invited user actually already exists in our system, maybe they also belong to someone else's team. Here is the `accept` method:
```
abort_if(!request()->hasValidSignature(), 403, 'YOU CAN\'T DO THAT');
$invitation = Invitation::find(request('invitation'));
abort_if($invitation->accepted, 403, 'That invitation has already been accepted');
$from = User::find($invitation->user_id);
try {
$user = User::where('email', $invitation->to)->firstOrFail();
$this->createAssociations($user, $invitation, $from);
$invitation->accepted = true;
$invitation->save();
auth()->login($user);
return redirect('/dashboard/communication');
} catch (ModelNotFoundException $m) {
return view('invitation.accept', compact('invitation'));
}
```
You can see we are running through a number of instances to validate against. Abort if the request doesn't have a valid signature, lookup the invitation and abort if it has already been accepted, get the invitee's email and try to find them in the database, if so, it's ok, create the new associations with the inviter, mark the invitation as `accepted`, log them into the app and redirect to the dashboard, if not Laravel will throw a `ModelNotFoundException` and we can send them to our form with the `invitation` object.
Instantiating the component is straight-forward enough, we just need to pass the invitation as a prop:
```
<accept-invitation :invitation="{{$invitation}}"></accept-invitation>
```
### Handling Laravel Errors in Vue
The Vue form is a bit long, but essentially, we are passing the invitation, into the Vue component and adding the invitation object to a hidden field, and instantiate what we expect from our user:
```
props: {
invitation: {
type: Object,
required: true,
},
},
data() {
return {
user: {
first_name: '',
last_name: '',
email: '',
phone_number: '',
password: '',
password_confirmation: '',
},
errors: null,
};
},
```
The `errors` object is where we will put any errors caught from the post request. I use Tailwind for all of my styling, the div looks like this:
```
<div v-if="errors" class="bg-red-500 text-white py-2 px-4 pr-0 rounded font-bold mb-4 shadow-lg">
<div v-for="(v, k) in errors" :key="k">
<p v-for="error in v" :key="error" class="text-sm">
{{ error }}
</p>
</div>
</div>
```
And finally, the post method:
```
methods: {
submit() {
axios
.post('accept-invitation', {
user: this.user,
invitation: this.invitation
})
.then(data => {
location.href = '/dashboard/communication';
})
.catch(e => {
this.errors = e.data.errors;
});
},
},
```
Let's check out the controller method that handles this request:
```
Route::post('accept-invitation', 'InvitationController@submit');
```
### Laravel Form Request Validation
For this method, since we are passing several fields to the controller let's leverage [Laravel's Form Request](https://laravel.com/docs/7.x/validation#form-request-validation), here we are using two methods: `rules()` and `messages()` like so:
```
public function rules()
{
return [
'user.first_name' => ['required', 'string', 'max:255', 'min:2'],
'user.last_name' => ['required', 'string', 'max:255'],
'user.phone_number' => ['required', 'max:255'],
'invitation.type' => ['required', 'string', 'max:255'],
'invitation' => ['required'],
'user.password' => ['required', 'min:6', 'confirmed'],
];
}
public function messages()
{
return [
'user.first_name.required' => 'Your first name is required',
'user.first_name.min' => 'Your first name must be at least 2 characters',
'user.first_name.max' => 'Your first name cannot be more than 255 characters',
'user.first_name.string' => 'Your first name must be letters only',
'user.last_name.required' => 'Your last name is required',
'user.last_name.min' => 'Your last name must be at least 2 characters',
'user.last_name.max' => 'Your last name cannot be more than 255 characters',
'user.last_name.string' => 'Your last name must be letters only',
'user.phone_number.required' => 'Your phone number is required',
'user.phone_number.max' => 'Your phone number cannot be more than 255 characters',
'user.password.required' => 'You must create a password',
'user.password.min' => 'Your password must be at least 6 characters long',
'user.password.confirmed' => 'Your password confirmation does not match',
];
}
```
Let's test this out and see what we get if we pass a mismatched password:
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/w7im9bybc9ycih0qptvl.png)
Voila! We were able to parse Laravel Validation errors in our Vue form with ease!
### Pro Tip!
One thing that I don't see a lot of people doing is using the validated data in their controller, instead of the request data. Basically, once the data is validated you can set that to a variable that ONLY contains the data that is validated as an array.
```
$validated = $request->validated();
```
Typically, I see developers validate the request and then use the request data with the assumption that since the validation passed, the data is good. But what if you forgot to validate a field? Your validation would pass, but the data might not be any good. This approach will make your validations stronger and increase your code confidence!
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment