Validation
- Defining Validation Rules
- Error Messages
- Available Validation Rules
- Multilingual Rules
- Conditional Rules
#Defining Validation Rules
Let's create a simple newsletter subscription form as in our previous chapter at app\Forms\NewsletterForm.php
, but this time also add validation rules to email
field:
<?php
namespace App\Forms;
class NewsletterForm extends \Laraform
{
public $model = \App\Subscriber::class;
public function schema() {
return [
'email' => [
'type' => 'text',
'placeholder' => 'Email address',
'rules' => 'required|email'
]
];
}
public function buttons() {
return [[
'label' => 'Submit'
]];
}
}
Just like Laravel validation rules they can be separated using |
pipes or can be defined as an array:
public function schema() {
return [
'type' => 'text',
'placeholder' => 'Email address',
'rules' => ['required', 'email']
];
}
Now render the form and try to submit with empty value. You should receive an error message under the field saying The Email address field is required
. Next try submitting with a non-email value. Then fill in correctly and see that it submits the data.
What you don't see is that Laraform also validates the data on the backend. You don't see it because you can only submit valid data, but if the user manages to bypass the frontend validation it will still be secured.
#Frontend & Backend Rules
Sometimes you need to define different validation rules for frontend and backend. You can do this quite straightforwardly:
public function schema() {
return [
'type' => 'text',
'placeholder' => 'Email address',
'rules' => [
'frontend' => ['required', 'email'],
'backend' => ['required', 'email', 'unique:subscribers,email']
]
];
}
It's great because now we can check if the user is already added to our subscriber list, but only on the backend. Let's see how we can do that on the frontend.
#Async Rules
You might ask, why can't we use the same unique:subscribers,email
rule on the frontend to validate the uniqueness of the email address? The answer is simple: for security reasons we don't want to reveal actual data table and field names on the frontend. Therefore we need to make a workaround to have it secured.
Let's add the unique
to our frontend rule set using subscriber
as the first parameter:
public function schema() {
return [
'type' => 'text',
'placeholder' => 'Email address',
'rules' => [
'frontend' => ['required', 'email', 'unique:subscriber'],
'backend' => ['required', 'email', 'unique:subscribers,email']
]
];
}
The unique
rule accepts a list of parameters like unique:param1,param2,...
instead of unique:database,email
. In this case we're passing the subscriber
param to the backend so it will know what type of validation it should perform. Let's go through the next steps to see how we can make it work.
Set Endpoint For Unique Rule
The unique
validation rule is async, meaning it sends a request to an endpoint to perform validation on the backend.
First we need to define which endpoint we want to use to perform the validation. Let's add that to our configuration:
import Vue from 'vue'
import Laraform from '@laraform/laraform'
Laraform.config({
endpoints: {
validators: {
unique: '/validate/unique'
}
}
})
Vue.use(Laraform)
// ...
Create Route
Create a route that will handle the unique
validation endpoint in routes/web.php
:
Route::post('/validate/unique', 'ValidationController@unique');
Create Controller
Now let's create a controller that will perform the validation at app\Http\Controllers\ValidationController.php
:
<?php
namespace App\Http\Controllers;
use Validator;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
class ValidationController extends Controller
{
public function unique(Request $request) {
$type = $request->params[0];
switch ($type) {
case 'subscriber':
$validator = Validator::make($request->all(), [
'value' => 'unique:subscribers,email',
]);
break;
}
return response()->json(!$validator->fails());
}
}
It might look a bit cumbersome to take these many steps to make it work, but this is how we can make the process secure.
Once you have these set up you can easily add different unique checkers too:
switch ($type) {
case 'subscriber':
$validator = Validator::make($request->all(), [
'value' => 'unique:subscribers,email',
]);
break;
case 'user':
$validator = Validator::make($request->all(), [
'value' => 'unique:subscribers,email',
]);
break;
// ...
}
Now if you reload the form you should see that the email field is initiating a backend call on each character typed and will return false
and show the field error if you type an email that already exists in the database.
#Debounce
We might not want to initiate a backend check on each and every character typed, but after just a few seconds if the typing stops. To do that you can use debounce
:
public function schema() {
return [
'type' => 'text',
'placeholder' => 'Email address',
'debounce' => 1000,
'rules' => [
'frontend' => ['required', 'email', 'unique:subscriber'],
'backend' => ['required', 'email', 'unique:subscribers,email']
]
];
}
If you reaload the form and start typing you'll notice that the unique
validator only triggers if the user stops typing for 1 second.
In case you don't want every rule to have a delay you can also assign debounce
to specific rules:
public function schema() {
return [
'type' => 'text',
'placeholder' => 'Email address',
'rules' => [
'frontend' => ['required', 'email:debounce=1000', 'unique:subscriber,debounce=1000'],
'backend' => ['required', 'email', 'unique:subscribers,email']
]
];
}
#Error Messages
Errors messages as stored in Laraform's frontend locale file at @laraform/laraform/locales
directory. They will be displayed according to the form's selected locale.
#Custom Messages
The form has a messages
method which can return custom error messages for validation rules. This is how we'd overwrite required
rule's message for example:
class CustomMessagesForm extends \Laraform
{
public function messages() {
return [
'required' => 'You must fill in this field'
];
}
// ...
}
The same is true for elements, where messages
property can be defined on an element level and also accepts an object in the same format as locale or form would:
class CustomNameMessageForm extends \Laraform
{
public function schema() {
return [
'name' => [
'type' => 'text',
'label' => 'Name',
'rules' => 'required',
'messages' => [
'required' => 'We must know your name, please fill in'
]
]
];
}
}
#Available Validation Rules
Here's the list of available validator rules:
accepted
Examines if the value is true
, 'true'
, 'yes'
, 'on'
, '1'
or 1
.
active_url
Aims to validate if the value is an URL with active A or AAAA type DNS record. To use the default endpoint provided by Laraform backend set the following configuration:
Laraform.config({
endpoints: {
validators: {
active_url: '/laraform/validator/active_url'
}
}
})
You can also create your own validator endpoint. In such case return a true
if the provided string is a valid A/AAAA type DNS record and false
otherwise.
after:date
The value must be after the given date. The date
parameter can be either a concrete date like '2018-01-01', a relative date which is either 'today', 'tomorrow' or 'yesterday' or the path of an other field like 'start_date'.
Examples
// being a concrete date
'rules' => 'date|after:2018-01-01'
// being a relative date
'rules' => 'date|after:today'
// being an other field's path
'rules' => 'date|after:accomodation.checkin_date'
Date must be a valid RFC2822 or ISO format according to moment.js.
after_or_equal:date
It's the same as after rule, except that the value must be after or equal to the given date.
alpha
The value can only contain English alphabetic characters.
alpha_dash
The value can only contain numbers, English alphabetic characters, dash -
and underscore _
.
alpha_num
The value can only contain numbers and English alphabetic characters.
array
The value must be an array.
before:date
It's the same as after rule, except that the value must be before the given date.
before_or_equal:date
It's the same as after rule, except that the value must be before or equal to the given date.
between:min,max
The value must be between the size of min and max. The size is evaluated the same way as described at size rule.
boolean
The value must be some form of a boolean which is either true
, false
, 1
, 0
, '1'
or '0'
.
confirmed
The value must identical to an other field's value which has the same name ending with _confirmation
.
Example
'password' => [
'type' => 'password',
'label' => 'Password',
'rules' => 'confirmed'
],
'password_confirmation' => [
'type' => 'password',
'label' => 'Password Again'
]
date
The value must be a valid RFC2822 or ISO format according to moment.js.
date_equals:date
The value must equal the given date and must be in a valid RFC2822 or ISO format according to moment.js.
date_format:format
The value must match the given date format according to moment.js.
strotime
function is using different date/time tokens. different:field
The value must be different from a different field's value.
digits:value
The value must be numeric and have an exact length of value.
digits_between:min,max
The value must be numeric and have a length between min and max.
dimensions
The value must be an image with dimension constraints. Available constraints:
- min_width
- min_height
- max_witdth
- max_height
- height
- width
- ratio
The ratio can be a float, like 0.66
or a statement like 2/3
which represents width / height ratio.
Examples
'rules' => 'dimensions:min_width=768'
'rules' => 'dimensions:min_height=1024'
'rules' => 'dimensions:min_width=768,ratio=2/3'
distinct
The value must be a unique member of an array.
Example
'favorite_numbers' => [
'type' => 'list',
'label' => 'Favorite numbers:',
'element' => [
'type' => 'text',
'placeholder' => 'Number',
'rules' => 'distinct'
]
]
The value must be a valid email format.
exists:param1,param2,...
Aims to check if a given value exists in a database by calling the endpoint
retrieved from Configuration endpoints.validators.exists
using axios POST
request. The value is submitted as value
along with the provided params
array. If any param is identical to an element's name in the form the value of that element will be sent instead of the name of the param. It expects to have a true
or false
response, if the response is true
the field will be resolved as valid.
file
The value must be an instance of File.
filled
The value must be not empty.
gt:field
The value must be greater than the size of the given field. The size is evaluated the same way as described at size rule.
gte:field
The value must be greater than or equal to the size of the given field. The size is evaluated the same way as described at size rule.
image
The value must a file with jpeg
, png
, bmp
, gif
or svg
extension.
in:foo,bar,...
The value must be one of the provided values.
Example
'role' => [
'type' => 'select',
'label' => 'Roles',
'items' => [
'admin' => 'Admin',
'editor' => 'Editor'
],
'rules' => 'in:admin,editor'
]
in_array:anotherfield.*
The value must be present in the anotherfield array.
integer
The value must be an integer according to Number.isInteger() method.
ip
The value must be a valid IP address.
ipv4
The value must be a valid IPv4 address.
ipv6
The value must be a valid IPv6 address.
json
The value must be a valid JSON string.
lt:field
The value must be lower than the size of the given field. The size is evaluated the same way as described at size rule.
lte:field
The value must be lower than or equal to the size of the given field. The size is evaluated the same way as described at size rule.
max:value
The size of the value must be lower than or equal to max. The size is evaluated the same way as described at size rule.
mimetypes:text/plain,...
The value must be an instance of File and have one of the listed mimetype.
mimes:zip,rar,...
The value must be an instance of File and have one of the listed extensions.
min:value
The size of the value must be at least min. The size is evaluated the same way as described at size rule.
not_in:foo,bar,...
The value must not be one of the provided values.
not_regex:pattern
The value must not match the provided regex pattern.
Example
'rules' => 'not_regex:/^.+$/i'
When using pipe |
in the pattern you must define the rules as array instead of a string.
nullable
Certain rules should only execute if the input has value otherwise it should be ignored. For example if a user may optionally fill in his birthday, obviously the date format should be validated if the input is filled. This is where nullable
rule comes in. If it's present among the validation rules it instructs the validator to only execute validation rules if the field has a value.
'birthday' => [
'type' => 'date',
'label' => 'Birthday',
'rules' => 'nullable|date_format:YYYY-MM-DD'
]
numeric
The value must be numeric.
regex:pattern
The value must match the provided regex pattern.
Example
'rules' => 'regex:/^.+$/i'
When using pipe |
in the pattern you must define the rules as array instead of a string.
required
The value must not be empty. The value is considered empty as the following:
- the value is
null
- the value is
undefined
- if the value is a string it's
''
- if the value is an array it contains no items
- if the value is a File object it's
name
is empty
same:field
The value must be the same as the give field's value.
Example
'password' => [
'type' => 'password',
'label' => 'Password',
'rules' => 'same:password_again'
],
'password_again' => [
'type' => 'password',
'label' => 'Password Again'
]
size:value
The size of the value must be exactly as the given value. Size is calculated as the following:
- if the value is string then it's the length of the string
- if the value is numeric then it's the actual numeric value
- if the value is array then it's the length of the array
- if the value is File then it's the size of the file.
string
The value must be a string.
timezone
The value must be a valid timezone, eg. 'America/Los_Angeles'
.
unique:param1,param2,...
Aims to check if a given value exists in a database by calling the endpoint
retrieved from Configuration endpoints.validators.unique
using axios POST
request. The value is submitted as value
along with the provided params
array. If any param is identical to an element's name in the form the value of that element will be sent instead of the name of the param. It expects to have a true
or false
response, if the response is true
the field will be resolved as valid.
url
The value must be a valid URL format.
uuid
The value must be a valid UUID format.
#Multilingual Rules
Validation rules can be specified for different languages when using multilingual elements:
<?php
namespace App\Forms;
class TranslatableForm extends \Laraform
{
public $multilingual = true;
public $languages = [
'en' => [
'code' => 'en',
'label' => 'English'
],
'de' => [
'code' => 'de',
'label' => 'German'
]
];
public function schema() {
return [
'title' => [
'type' => 't-text',
'label' => 'Title',
'rules' => [
'en' => 'required',
]
],
];
}
}
In this case the title
element will be required in English, while can be left empty in German.
#Conditional Rules
You may add conditional rules to elements which should only trigger under certain circumstances. Let's see an example, where we'd require phone number from a user only if delivery option is selected:
'delivery' => [
'type' => 'select',
'label' => 'Do you want delivery?',
'items' => [
'yes' => 'Yes',
'no' => 'No'
]
],
'phone' => [
'type' => 'text',
'label' => 'Phone number',
'rules' => [
[
'required' => ['delivery', 'yes']
]
]
]
As you can see the rule is in an array, where the key is the rule itself, while the value is another array. This second array should contain minimum 2 values, the first is the data path of the other element and the second is the expected value.
name
input in group element called profile
it's path will be profile.name
but it's data path will be name
.Multiple Accepted Values
It's also possible to accept multiple values for the condition by defining an array
of values as the second parameter:
'delivery' => [
'type' => 'select',
'label' => 'How do you want receive your package?',
'items' => [
1 => 'Pick at the store',
2 => 'Delivery with UPS',
3 => 'Delivery with FedEx'
]
],
'phone' => [
'type' => 'text',
'label' => 'Phone number',
'rules' => [
[
'required' => ['delivery', [2,3]]
]
]
]
The values of the array can be either numbers
or strings
.
Custom Operator
You might want to use different operators to examine the condition value, which can be done by simply defining the operator as the second array element, and the comparison value third:
'deposit' => [
'type' => 'text',
'label' => 'Deposit amount'
],
'id_number' => [
'type' => 'text',
'label' => 'ID Card number',
'rules' => [
[
'required' => ['deposit', '>', 500]
]
]
]
In this example, ID Card number will only be required if the Deposit amount is higher than 500
.
Here's the list of available operators:
!=
=
>
>=
<
<=
#Custom Conditions
You can also create custom conditions for rules. In this case conditions must be defined separately for backend and frontend. To demonstrate this, let's implement our previous example with custom condition.
Create CustomConditionForm.php
First create a new backend form at app/Forms/CustomConditionForm.php
:
<?php
namespace App\Forms;
class CustomConditionForm extends \Laraform
{
public $component = 'custom-condition-form';
public function schema() {
return [
'deposit' => [
'type' => 'text',
'label' => 'Deposit amount'
],
'id_number' => [
'type' => 'text',
'label' => 'ID card number',
]
];
}
public function buttons() {
return [[
'label' => 'Submit'
]];
}
}
Create CustomConditionForm.vue
Now create a frontend form at resources/js/components/forms/CustomConditionForm.vue
:
<script>
export default {
mixins: [Laraform],
data() {
return {
schema: {
id_number: {
rules: [
{
required: function(Validator){
return this.el$('deposit').value > 500
}
}
]
}
},
buttons: [{
disabled() {
return this.form$.invalid
}
}]
}
}
}
</script>
What we are doing here is extending the form's id_number
on the frontend by adding rules
and we define custom condition for required
rule, which should only apply when the deposit amount is larger than 500. In this example we're also extending the button to become disabled if the form is invalid.
Add CustomConditionForm.vue
to app.js
Next, let's add the frontend form to our app.js
(or main JS file):
import Vue from 'vue'
import Laraform from '@laraform/laraform'
import CustomConditionForm from './components/forms/CustomConditionForm'
Vue.use(Laraform)
const app = new Vue({
el: '#app',
components: {
CustomConditionForm
}
})
After that, assign this form to the view using app('App\Forms\CustomConditionForm')
and render it with ->render()
method as we've learned in the Rendering chapter.
You should see Deposit and ID card number fields and if you enter higher amount than 500 you'll notice that ID card number will become required if you try to submit the form.
But what happens if you mistakenly enter 1000 instead of 100 to deposit, submit the form and receive an error under ID card number? Even after you change the amount to 100 the ID card number's error won't go away. Let's see what we can do against it.
Watching For Condition Change
To watch for value changes of an other element we can use the Validator's .watch()
method, which only sets our watcher once, instead of Vue's this.$watch()
which would execute each time the condition is evaluated.
Here's how we can watch the other field's value and revalidate our field upon the change of deposit:
rules: [
{
'required': function(Validator){
// By using the Validator's .watch() method we
// make sure that our watcher is only registered
// once instead of registering it each time the
// condition is checked. The data we are watching
// is relative to the root Laraform component
// so in this case we are watching its data.deposit
// which is exactly what we need here.
Validator.watch('data.deposit', () => {
// By using the Validator's validate() method
// we can simply revalidate the rule.
Validator.validate()
})
return this.el$('deposit').value > 500
}
}
]
Now if you decrease Deposit amount below 500 when ID Card number is already displaying an error, you can see that it disappears as the element gets revalidated.
We're doing great so far but what happens after the user submits the data? We need to care of backend validation of course.
Add Custom Conditional Rule To Backend
Let's extend our backend form's schema with the conditional required
rule:
public function schema() {
return [
'deposit' => [
'type' => 'text',
'label' => 'Deposit amount'
],
'id_number' => [
'type' => 'text',
'label' => 'ID card number',
'rules' => [
'backend' => [
[
'required' => function($data) {
return $data->deposit > 500;
}
]
]
]
]
];
}
That's it, the backend will required ID card number on the backend too only if the deposit amount is larger than 500.