Building Web Apps with Laravel Module 7 – Validation, form requests and file uploads
Validation rules and error messages
Write Laravel 13 rule lists for text, numbers, dates, choices and nested rows, read validated data instead of all(), and translate every message.
What you will learn
- Validate text, numbers, dates, choices and nested rows with one rule list
- Apply conditional rules such as sometimes, required_if and exclude_if
- Read validated input with validated() and safe() instead of all()
- Replace a message and an attribute name, and translate both into Hindi
Before you start
What you’ll learn · Before you start
What you will learn
- Validate text, numbers, dates, choices and nested rows with one rule list
- Apply conditional rules such as sometimes, required_if and exclude_if
- Read validated input with validated() and safe() instead of all()
- Replace a message and an attribute name, and translate both into Hindi
Before you start
On this page
An admission form posts four fields and three of them are wrong: a date as 05/01/2016, a batch called night, and seven seats where three are the limit. One rule list turns that into three messages, a redirect back to the form and the fields the parent already typed, and your controller never runs. The same payload sent by a mobile client gets a 422 with the same three messages as JSON.
This lesson writes that rule list and reads what it hands back. You will see which keys survive validation and which types they keep, how rules for dates, choices and nested rows differ from rules for text, how one field can switch another field’s rules on or remove it, why a field sometimes gets two messages and sometimes one, and how the same failures read in Hindi. Every run was recorded on Laravel 13.34.
One rule list decides what reaches your code
The shortest way to validate is to let the request do it. $request->validate([...]) applies the rules and returns the data when they pass; when a rule fails it throws, so the next line of the action is never reached. Here is the whole route for the admission form:
<?php
use Illuminate\Foundation\Http\Middleware\PreventRequestForgery;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
// The probe is a script with no browser, so it carries no forgery token; a real form route keeps PreventRequestForgery.
// The page the form sits on. It reads the errors the redirect left in the session.
Route::get('/admissions/create', function () {
$errors = session('errors');
return response(
$errors === null
? "the form, with no errors\n"
: "the form, with errors: ".json_encode($errors->getBag('default')->messages())."\n"
);
})->name('admissions.create');
// One rule list, applied by the request itself. A failure never reaches the line that saves.
Route::post('/admissions', function (Request $request) {
$validated = $request->validate([
'name' => ['required', 'string', 'max:120'],
'born_on' => ['required', 'date_format:Y-m-d'],
'batch' => ['required', 'in:morning,evening'],
'seats' => ['required', 'integer', 'between:1,3'],
]);
return response()->json(['saved' => $validated]);
})->withoutMiddleware(PreventRequestForgery::class); Follow the wrong post through it, then the same post with three fields corrected:
The drawing shows the whole scene; its 7 steps are listed below.
Every step as text
- Four fields of text. The form posts name, born_on, batch and seats. Every one of them is text, because a form body carries nothing else, and three of the four are wrong in a way no column type would catch.
- The rule list runs. The action begins with $request->validate([...]): one rule list for the four fields. The validator applies every rule of every field, not only the first field that fails.
- The answer is built for you. Three fields failed, so validate() throws a validation exception and the first line of your action never runs. Laravel turns that exception into the answer the request asked for.
- Back to the form, with help. A browser form gets 302 and a Location of the page it came from, and the messages and the fields the visitor typed are flashed into the session: errors for the three fields, old input for all four.
- The page reads them once. The page the redirect points at answered 200 and read the three messages out of the session. Flashed data lives for exactly one request, so a reload shows the form without them.
- With input that passes. Send born_on in the Y-m-d shape, batch as morning and seats as 2, and validate() returns instead of throwing. It hands back the four fields it had rules for, and nothing else the request carried.
- Your action gets the data. The action runs with that array: the keys it declared rules for, each still the text the form sent. seats is the string "2", not the number 2, because validation checks a value and never changes it.
The recorded run sends both, as a browser form and as JSON, and prints the status, the headers and what the session carries afterwards:
from session import run
# The same wrong admission form as a browser post and as a JSON post, through the real HTTP kernel.
run(
"""
php form.php
""",
app=True,
overlay=("form-probe",),
) Output
$ php form.php
== a browser form with three wrong fields
status 302
Location http://localhost/admissions/create
Content-Type text/html; charset=utf-8
flashed errors: {"default":{"format":":message","messages":{"born_on":["The born on field must match the format Y-m-d."],"batch":["The selected batch is invalid."],"seats":["The seats field must be between 1 and 3."]}}}
flashed old input: ["name","born_on","batch","seats"]
== the page the redirect points at
status 200
Content-Type text/html; charset=utf-8
body the form, with errors: {"born_on":["The born on field must match the format Y-m-d."],"batch":["The selected batch is invalid."],"seats":["The seats field must be between 1 and 3."]}
== the same payload with Accept: application/json
status 422
Content-Type application/json
body {"message":"The born on field must match the format Y-m-d. (and 2 more errors)","errors":{"born_on":["The born on field must match the format Y-m-d."],"batch":["The selected batch is invalid."],"seats":["The seats field must be between 1 a…
== input that passes every rule
status 200
Content-Type application/json
body {"saved":{"name":"Meera Nair","born_on":"2016-01-05","batch":"morning","seats":"2"}}
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 form_session.py
The browser form got a 302 with Location: http://localhost/admissions/create, the page it came from. Two things were flashed into the session: the error bag: The collection of validation messages for a request, keyed by field. For a browser form it is flashed into the session so the page can show it once. with one message for each of born_on, batch and seats, and the old input for all four fields, so the page can fill the boxes in again. The page itself answered 200 and read those three messages out of the session. The JSON request got a 422 whose body carries message and an errors object keyed by field, which is the shape every Laravel API answers with.
The last block is the interesting one. The form that passes returns {"saved":{"name":"Meera Nair","born_on":"2016-01-05","batch":"morning","seats":"2"}}. The seats value is still the string "2", although the field has the integer rule. The next section’s probe shows the other half of the same point: a key the request carried but no rule named does not appear in the data at all.
Key idea
validated data: The array a validator hands back: only the fields that had rules, holding the values the request carried, with no key the rules did not declare. is the array of the fields you wrote rules for, holding the values that arrived. A rule decides whether a value is acceptable; it never changes the value, and it never carries a key you did not declare.
That is why validated(), or safe()->only([...]) when you want a subset, is what you pass on, and $request->all() is not. Everything a caller sends is in all(), including fields you never put in the form, and handing that array to a model is the mass assignment: Creating or updating a model from a whole array of fields in one call, which is why a model declares the columns such an array is allowed to write. More in “Your first CRUD feature with Eloquent” weakness that OWASP devotes a cheat sheet to.
Checkpoint
Check: What a rule list returns
Rules for text, numbers, dates, choices and nested arrays
A validation rule: One named check applied to one field, such as digits:6 or a rule object. It reports a message and never changes the value it checked. is one named check. A field takes a list of them, in order, and every rule of every field is applied, so one post can report a problem in each field at once. The probe below runs six fields against this list:
| Field | Rules | What it refuses |
|---|---|---|
name |
required, string, max:120 |
text with no bound, or no name at all |
born_on |
required, date_format:Y-m-d, before:2020-01-01 |
a date in another shape, or too recent |
batch |
required, Rule::in(['morning', 'evening']) |
any batch that does not exist |
seats |
required, integer, between:1,3 |
text, and a count outside the limit |
items |
sometimes, array, max:5 |
more rows than the form allows |
items.*.qty |
required, integer, min:1 |
a row whose quantity is text or zero |
from session import run
# One rule list for an admission form, run against the validator itself: the messages it produces and what it keeps.
run(
"""
php rules.php
""",
app=True,
overlay=("rules-probe",),
) Output
$ php rules.php
== a PIN code of four digits
message The pin field must be 6 digits.
== a password of three letters
message The password field must be at least 8 characters.
message The password field must contain at least one number.
== six fields, four of them wrong
message The name field must not be greater than 120 characters.
message The born on field must match the format Y-m-d.
message The selected batch is invalid.
message The items.0.qty field must be an integer.
== the same input, named for a reader
message The name field must not be greater than 120 characters.
message The date of birth field must match the format Y-m-d.
message Choose the morning or the evening batch.
message The number of books field must be an integer.
== input that passes every rule
kept name string "Meera Nair"
kept born_on string "2016-01-05"
kept batch string "morning"
kept seats string "2"
kept items array [{"qty":"3"}]
== two rules fail on one field
message The pin field must be 6 digits.
message The pin field must be a number.
== the same field with bail
message The pin field must be 6 digits.
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 rules_session.py
Read the messages of the third block. A date in the wrong shape reported The born on field must match the format Y-m-d. and nothing about before, though both rules ran. For text that does not fit the declared format, before hands it to PHP’s own date parser, which reads slashes month first: 05/01/2016 became 2016-05-01, earlier than the limit, so that rule passed and only the format rule caught the slip. The batch reported The selected batch is invalid., which is the sentence every in rule uses, and the nested row reported The items.0.qty field must be an integer. — the field path with the row number in it, which is accurate and unreadable; the section on messages below replaces it.
Two rules deserve a closer look. digits:6 on 4110 answered The pin field must be 6 digits., so a PIN code needs no regular expression. Password::min(8)->letters()->numbers() on abc answered twice, once for the length and once for the missing digit, and said nothing about letters, because abc has them. A rule object like this one builds its own list of requirements, and reports every requirement the value misses, so the visitor can fix them in one attempt.
A numeric string passes integer
seats arrived as the text "2" and the integer rule accepted it, because a form body has no numbers. The rule asks whether the value is an integer as PHP reads it, which a numeric string is. The value you receive is still text.
For nested data, write rules at both levels: on the container, so its size is bounded, and on the star path, so every row is checked. items.*.qty applies to row 0, row 1 and any row a caller invents.
The block that passes also sent a referrer key, which no rule named. It is not in the five kept lines: a key with no rule is dropped rather than kept as null, and that is the difference between the validated data and the whole payload.
Rules that switch on because of another field
Most forms have fields that only matter in some cases. A conditional rule: A rule that reads another field before it applies, such as required_if, exclude_if or sometimes, so one rule list can describe a form whose fields depend on each other. reads the rest of the payload and decides. Three of them cover nearly everything: required_if demands a field when another field has a value, exclude_if removes a field in that case, and sometimes applies the rest of a field’s rules only when the key is present at all.
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- Transport asked for, no stop. The payload says transport: 1 and carries no stop. The stop field has required_if:transport,1, so the rule switches on and the form comes back with one message.
- A stop that is named. Send the same payload with stop: Kacheripady and every rule passes. validated() hands back batch, transport and stop, so the action can save the stop.
- No transport, a stop anyway. Now transport: 0 with a stop still in the payload. exclude_if:transport,0 removes the field before the other rules read it, so max:60 never runs on it.
- The dropped field is gone. validated() returns batch and transport only. A field that exclude_if removed cannot reach your action, which is the point: a stop without transport would be a row nobody reads.
- A field nobody sent. The guardian phone has sometimes, so when the payload has no such key the validator skips its rules instead of failing them. No message, and no key in the data.
- The same field, sent badly. Send guardian_phone as "98765 43210" and sometimes no longer applies: the key is there, so the regex runs and reports that the format is invalid.
The recorded run sends six payloads through one rule list and prints, for each, its message or the keys that survived; its last block adds a check after the rules:
from session import run
# Rules that depend on other fields: which rule switches on, which field is dropped, and which keys survive.
run(
"""
php conditional.php
""",
app=True,
overlay=("conditional-probe",),
) Output
$ php conditional.php
== transport yes, no stop
sent {"batch":"morning","transport":"1"}
message The stop field is required when transport is 1.
== transport yes, a stop named
sent {"batch":"morning","transport":"1","stop":"Kacheripady"}
kept ["batch","transport","stop"]
== transport no, a stop sent anyway
sent {"batch":"morning","transport":"0","stop":"Kacheripady"}
kept ["batch","transport"]
== no guardian phone sent
sent {"batch":"morning","transport":"0"}
kept ["batch","transport"]
== a guardian phone in the wrong shape
sent {"batch":"morning","transport":"0","guardian_phone":"98765 43210"}
message The guardian phone field format is invalid.
== an evening batch with a waiver note
sent {"batch":"evening","transport":"0","fee_waiver_note":"sibling"}
message The fee waiver note field is prohibited when batch is evening.
== a cross-field check added after the rules
message The morning bus does not pass Kacheripady.
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 conditional_session.py
With transport set to 1 and no stop, the answer is The stop field is required when transport is 1. With a stop named, validated() returns ["batch","transport","stop"]. With transport set to 0 and a stop sent anyway, it returns ["batch","transport"]: exclude_if removed the field before the other rules read it, so max:60 never ran and the stop cannot reach the action. That is the point of excluding rather than ignoring — a stop with no transport would be a row nobody reads, written because a caller sent an extra key.
The guardian phone shows the difference between sometimes and nullable. A payload with no guardian_phone key passes with no message and no key kept. A payload that sends 98765 43210 fails the +91 pattern, because the key is there and the regular expression runs. nullable is a third thing again: the key is present and may hold null, which is what a text box the visitor left empty arrives as, since ConvertEmptyStringsToNull ran before the rules.
The last block adds a check the rule strings cannot express. A timetable knows that the morning bus does not pass one of the stops; after() runs once the field rules have been applied and adds The morning bus does not pass Kacheripady. to the stop field, where it appears beside the rule messages.
Checkpoint
Check: Rules that depend on another field
How many messages one field gets
A field collects one message for each rule that fails, in the order the rules are listed. The last two blocks of the rules probe above sent the text abc to a pin field twice, the second time with bail at the front of its list:
Rules on pin |
Messages for abc |
|---|---|
digits:6, numeric |
The pin field must be 6 digits. and The pin field must be a number. |
bail, digits:6, numeric |
The pin field must be 6 digits. |
Without bail both rules reported. With it the field stopped at its first failure, and numeric never ran.
bail is a flag for the field, not a position in the list: it stops that field at its first failing rule wherever it is written, and it says nothing about the other fields, which keep reporting. Use it where the later messages only repeat the first in other words, such as a format check behind a type check. Leave it off where every requirement is a separate thing to fix, as for the password rule, whose two messages told the visitor both problems at once.
Put the most useful rule first
The order of the rules is the order of the messages, so put the check that says what is wrong first: a type or format rule before a range rule.
Checkpoint
Check: How many messages a field gets
Messages and field names a reader understands
Three things can be replaced, from the most specific to the least: the sentence for one rule of one field, the name of the field inside every sentence, and the default sentences of every rule in a locale file: A file of translated strings for one language, keyed by name. A key that is missing falls back to the application's fallback locale, one key at a time.. The second block of the rules probe passes a message for batch.in and attribute name: The reader-facing name of a field, used inside every message about it, so a message says "date of birth" rather than the column name. for born_on and items.*.qty, and three of the four messages change:
| Default message | With a message and attribute names |
|---|---|
The born on field must match the format Y-m-d. |
The date of birth field must match the format Y-m-d. |
The selected batch is invalid. |
Choose the morning or the evening batch. |
The items.0.qty field must be an integer. |
The number of books field must be an integer. |
The attribute name for items.*.qty is written with the star, so one entry renames the field in every row’s message. Prefer an attribute name to a replaced sentence: one entry fixes every rule of that field, while a replaced sentence has to be written again for each rule.
A fresh Laravel 13 project has no lang directory, because the defaults live in the framework. php artisan lang:publish writes them where you can edit them, and a second language is a folder beside en holding only the keys the application uses:
<?php
// lang/hi/validation.php: the messages this form needs, in Hindi, and the names the reader should see.
// php artisan lang:publish writes the full English file; a second language is a folder beside it with the
// keys the application actually uses. A key that is missing falls back to the application's fallback locale.
return [
'required' => ':attribute भरना आवश्यक है।',
'integer' => ':attribute में पूरी संख्या होनी चाहिए।',
'digits' => ':attribute में :digits अंक होने चाहिए।',
'in' => 'चुना गया :attribute मान्य नहीं है।',
'date_format' => ':attribute का प्रारूप :format होना चाहिए।',
'between' => [
'numeric' => ':attribute :min और :max के बीच होना चाहिए।',
'string' => ':attribute में :min से :max अक्षर होने चाहिए।',
],
'max' => [
'string' => ':attribute :max अक्षरों से अधिक नहीं होना चाहिए।',
],
'attributes' => [
'name' => 'नाम',
'born_on' => 'जन्म तिथि',
'batch' => 'बैच',
'seats' => 'सीटों की संख्या',
'pin' => 'पिन कोड',
'items.*.qty' => 'पुस्तकों की संख्या',
],
]; from session import run
# Publishing the language files, the Hindi file this form needs, and the same failures in both locales.
run(
"""
php artisan lang:publish
ls lang
head -12 lang/hi/validation.php
php hindi.php
""",
app=True,
overlay=("hindi-probe",),
) Output
$ php artisan lang:publish
INFO Language files published successfully.
$ ls lang
en
hi
$ head -12 lang/hi/validation.php
<?php
// lang/hi/validation.php: the messages this form needs, in Hindi, and the names the reader should see.
// php artisan lang:publish writes the full English file; a second language is a folder beside it with the
// keys the application actually uses. A key that is missing falls back to the application's fallback locale.
return [
'required' => ':attribute भरना आवश्यक है।',
'integer' => ':attribute में पूरी संख्या होनी चाहिए।',
'digits' => ':attribute में :digits अंक होने चाहिए।',
'in' => 'चुना गया :attribute मान्य नहीं है।',
'date_format' => ':attribute का प्रारूप :format होना चाहिए।',
'between' => [
$ php hindi.php
== locale en
The born on field must match the format Y-m-d.
The selected batch is invalid.
The seats field must be between 1 and 3.
The pin field must be 6 digits.
The items.0.qty field must be an integer.
== locale hi
जन्म तिथि का प्रारूप Y-m-d होना चाहिए।
चुना गया बैच मान्य नहीं है।
सीटों की संख्या 1 और 3 के बीच होना चाहिए।
पिन कोड में 6 अंक होने चाहिए।
पुस्तकों की संख्या में पूरी संख्या होनी चाहिए।
== a rule the Hindi file has no message for
The सीटों की संख्या field must be a number.
fallback locale: en
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 hindi_session.py
The Hindi block reports the same five failures in Hindi, with the field names from the attributes array: पिन कोड में 6 अंक होने चाहिए। for the PIN code, and पुस्तकों की संख्या में पूरी संख्या होनी चाहिए। for the nested quantity. Read the last block carefully. The Hindi file has an attribute name for seats but no numeric message, and the answer is The सीटों की संख्या field must be a number. — an English sentence around a Hindi name.
A half-translated validation file
Each key is looked up on its own, and a missing one falls back to app.fallback_locale, which the run printed as en. The fix is not a clever default: it is a check that fails every rule the form uses in each locale and reads the result, so a missing key is found before a parent sees half a sentence.
Checkpoint
Check: Messages and attribute names
Say what is allowed, not what is forbidden
The OWASP input validation cheat sheet’s advice is to prefer an allowlist: A list of exactly what is accepted, so anything absent from it is refused. The opposite of a denylist, and the safer default for input. to a denylist: A list of what is refused, which accepts everything nobody thought to list. Useful as a second layer, unsafe as the only check on input., to bound what you accept, and to use the framework’s validators rather than hand-written checks. Every rule in this lesson’s list follows it. Rule::in and Rule::enum name the values that exist. date_format names the one shape a date may arrive in. max:120 bounds a field that would otherwise accept as much text as the server will read. digits:6 describes a PIN code exactly.
A denylist reads reasonably and fails open. Here is the batch field with each kind of rule on its own, given five values:
Value sent for batch |
Rule::in(['morning', 'evening']) |
not_in:night |
|---|---|---|
morning |
passes | passes |
night |
refused | refused |
admin |
refused | passes |
the text NULL |
refused | passes |
| an empty array | refused | passes |
not_in:night refuses the one batch somebody thought of and accepts the rest, including whatever next year’s client sends. The difference matters most for fields that choose behaviour rather than describe a person: a role, a status, a disk, a sort column. For those, write the list of what exists and let the rule refuse everything else.
Key idea
A rule list is an allowlist of shapes: the fields that may arrive, the values each may hold and the size each may reach. A value outside it is refused, so the list alone tells a reader what the action can receive.
Two habits come from this. Give every text field a maximum, even one you think is generous, so no field is unbounded. And keep the allowlist in the rules, not in the action: a match statement in a controller refuses the same values, but no message, no form helper and no API client can see it.
Checkpoint
Check: Allowlists over denylists
Practise
Work these on your own Pathshala project, with the rules in the controller for now; the next lesson moves them into a class of their own.
- Write the admission rule list: a name, a date of birth that must be before a date you choose, a guardian phone in
+91format, a six-digit PIN code, a batch from an enum, and an optional array of siblings with at most four rows. Post it with every field wrong at once and count the messages. - Add an attribute name for the nested rows and compare the message with the one that names
siblings.0.name. - Add
bailto the phone field, post a value that fails two of its rules, and decide whether you want it there. - Publish the language files, add a
hifolder with only the keys your form uses, and fail every rule in both locales. Note any sentence that comes back mixed.
Then work through the self-check.
Practice that adapts to you
Practise this lesson
16 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up: why pass the return value of validate() to the model instead of $request->all()?
Because they are different sets of keys. validate() returns only the fields the rule list declared, holding the values that arrived, while all() returns everything the caller sent, including keys that were never in your form. A caller who adds fee_waived or role to the payload does not fail validation — an undeclared key is ignored by the rules, not rejected — so Model::create($request->all()) can write a column you never meant to expose. Passing the validated array makes the set of writable fields the same as the set of validated fields, and the model’s fillable list is then a second guard rather than the only one. When a subset is wanted, safe()->only([...]) narrows it at the call site. None of this casts anything: both arrays hold the text the request carried.
Three longer questions grow out of this lesson’s rule lists:
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Fresher to mid
A fee payment form validates four fields and then calls Payment::create($request->all()). A caller adds "waived": true to the payload and the row is written with it. Explain how, and say what you would change.
-
Fresher to mid
Parents report that the admission form refuses their date of birth. The field has date_format:Y-m-d and a before: rule, and the form posts the date typed day-first with slashes. Say what the form shows and how you would fix it.
-
Mid to senior
With the locale set to Hindi, one message comes back as an English sentence with a Hindi field name inside it. Explain the lookup that produces that, and say what check you would add before a release.
Flashcards · 6 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
What does $request->validate([...]) return, and why pass that on instead of $request->all()?
If any rule fails it throws and your action never runs; otherwise it returns only the fields that had rules. all() carries every key the caller sent, so create() with it could write a column such as fee_waived that no form offered.
-
Does a validation rule change the value it checks?
No. A rule only decides whether a value is acceptable: seats arrived as the string "2" and stayed text after passing, so casting is separate work.
-
How does the same failed validation answer a browser form and a JSON client?
A browser form gets a 302 redirect back, with the errors and the old input flashed to the session. A JSON client gets a 422 with a message and an errors object.
-
What do required_if, exclude_if, sometimes and after() each do?
required_if switches a rule on because of another field's value; exclude_if removes a field so it cannot reach your action; sometimes applies rules only when the key is present; after() adds a check that rule strings cannot express.
-
How many error messages can one field get, and what does bail change?
One message per failing rule, in the order the rules are listed. bail stops that field at its first failure.
-
How do you rename nested fields in error messages, and why might a message come back half translated?
An attribute name written with a star, such as items.*.qty, renames that field in every row's message. A locale file with a missing key falls back one key at a time, so one sentence can mix languages.
Key takeaways
$request->validate([...])either throws, so your action never runs, or returns the fields that had rules, which is the array to pass on;all()carries everything the caller sent.- A rule checks a value and never changes it:
seatsarrived as the string"2"and stayed text, so casting is separate work. - The same failures become a 302 with flashed errors and old input for a browser form, and a 422 with a
messageand anerrorsobject for a JSON client. required_ifswitches a rule on from another field,exclude_ifremoves a field so it cannot reach your action,sometimesapplies rules only when the key is present, andafter()adds a check the rule strings cannot express.- A field reports one message per failing rule in the order you listed them, and
bailstops that field at the first failure. - An attribute name written with the star renames a nested field in every row’s message, and a locale file with a missing key falls back one key at a time, which is how a sentence comes back half translated.
Terms in this lesson
- allowlist
- A list of exactly what is accepted, so anything absent from it is refused. The opposite of a denylist, and the safer default for input.
- attribute name
- The reader-facing name of a field, used inside every message about it, so a message says "date of birth" rather than the column name.
- conditional rule
- A rule that reads another field before it applies, such as required_if, exclude_if or sometimes, so one rule list can describe a form whose fields depend on each other.
- denylist
- A list of what is refused, which accepts everything nobody thought to list. Useful as a second layer, unsafe as the only check on input.
- error bag
- The collection of validation messages for a request, keyed by field. For a browser form it is flashed into the session so the page can show it once.
- locale file
- A file of translated strings for one language, keyed by name. A key that is missing falls back to the application's fallback locale, one key at a time.
- validated data
- The array a validator hands back: only the fields that had rules, holding the values the request carried, with no key the rules did not declare.
- validation rule
- One named check applied to one field, such as digits:6 or a rule object. It reports a message and never changes the value it checked.
References
- Validation (Laravel (laravel.com))
- Laravel framework v13.34.0 source, ValidatesAttributes.php (Laravel (GitHub))
- Supported Date and Time Formats (The PHP Group (php.net))
- Localization (Laravel (laravel.com))
- Input Validation Cheat Sheet (OWASP)
- Mass Assignment Cheat Sheet (OWASP)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress