In Laravel validation, when must rules be written as an array instead of a pipe-delimited string, and what does the bail rule change?
answer
- strings are split on |
- regex patterns containing a pipe
- Password and File objects cannot stringify
- bail: stop this field at first failure
- required failure already stops the field
basics
~20 sA pipe string is split on |, so a regex containing a pipe, a closure, or a rule object without a string form such as Password::defaults() or File::types() must go in an array. bail stops running a field's remaining rules after its first failure.
solid answer
~30 s`'username' => 'required|string|max:30'` and `['required', 'string', 'max:30']` are equivalent: the parser explodes strings on `|`. Arrays become necessary when a rule cannot survive that split or has no string form: a `regex:` pattern containing `|`, a closure, a custom `ValidationRule` object, and fluent objects such as `Password::defaults()` or `File::types(['jpg', 'png'])`. Objects like `Rule::unique()`, `Rule::in()` and `Rule::enum()` do stringify, but arrays keep them readable. `bail` makes the validator stop checking one field once it has an error, so a username that fails `alpha_dash` does not also report `max`. It works per field; other fields are still validated.
code
php · 14 lines<?php
use Illuminate\Validation\Rules\Password;
$rules = [
// Pipe string: fine, nothing to split wrongly.
'display_name' => 'required|string|max:50',
// Array required: the regex contains a pipe.
'username' => ['bail', 'required', 'string', 'not_regex:/^(admin|root)$/i', 'max:30'],
// Array required: Password has no string form.
'password' => ['required', 'confirmed', Password::defaults()],
];go deeper
Know both spellings, that arrays are safer, and that bail stops a field's rules after its first error.
Explain the | split, which rule objects stringify, and why Password and File need arrays.
Order rules so cheap checks run first, rely on the built-in skip for unique/exists, and keep rule sets readable in review.
Set a codebase-wide rule style, arrays only, and shared defaults such as Password::defaults() in a provider.
## Two spellings of the same rules Laravel accepts rules for a field in two forms: - **Pipe string**: `'username' => 'required|string|alpha_dash|max:30'` - **Array**: `'username' => ['required', 'string', 'alpha_dash', 'max:30']` Internally, `ValidationRuleParser` explodes a string rule on `|`. Array entries are kept as they are (except a few fluent builders that stringify themselves). The two forms produce the same result for simple rules; arrays are the style the current docs use in most examples. ## When the array form is required 1. **Regular expressions containing a pipe.** `'regex:/^(admin|root)$/'` inside a pipe string is split at the `|`, producing a broken pattern. In an array, the whole `regex:` string stays intact. 2. **Closures.** An inline rule written as `function (string $attribute, mixed $value, Closure $fail) { ... }` can only be an array element. 3. **Custom rule objects** implementing `ValidationRule`. 4. **Fluent rule objects without a string form**, notably `Illuminate\Validation\Rules\Password` (`Password::defaults()`, `Password::min(8)->letters()`) and `Illuminate\Validation\Rules\File` (`File::types(['jpg', 'png'])->max('5mb')`). Some fluent objects **do** convert to strings — `Rule::unique()`, `Rule::exists()`, `Rule::in()` and `Rule::enum()` produce `unique:...`, `exists:...` and `in:...` — so `'required|'.Rule::in(['a', 'b'])` works. It is still harder to read and easier to break than an array. | Rule | Pipe string OK? | Why | |---|---|---| | `max:30`, `email`, `date` | yes | plain strings | | `regex:/^(a|b)$/` | **no** | the pattern's pipe is treated as a separator | | `Rule::unique('users')->ignore($user)` | works when concatenated | converts itself to a `unique:` string | | `Rule::enum(Role::class)` | works when concatenated | converts itself to an `in:` string | | `Password::defaults()` | **no** | object with no string form | | `File::types(['pdf'])` | **no** | object with no string form | | closure or `ValidationRule` class | **no** | callable or object | ## What bail does Rules for a field run **in the order written**. By default, when one fails, the validator keeps running the rest of that field's rules and collects every message. `bail` changes that for the field it is on: after the first error on that field, its remaining rules are skipped. - `'username' => ['bail', 'string', 'alpha_dash', 'max:30']` — a name like `"john smith!!"` reports only the `alpha_dash` error. - Other fields are unaffected; `bail` is per field. Stopping the **whole** validator after the first failing field is a separate switch (`stopOnFirstFailure`). ## When bail is not needed Two built-in behaviours already stop some wasted work: - If an **implicit** rule such as `required` fails, the validator stops that field's remaining rules anyway — there is nothing to check on an empty value. - `unique` and `exists` are skipped when the field already has an error, so an invalid username does not trigger a database query even without `bail`. `bail` is therefore about the ordinary rules in between — mostly for cleaner error lists and for skipping an expensive custom rule after a cheap check fails. ## Defaults for password rules `Password::defaults()` returns the application-wide password rule. Without configuration it is `Password::min(8)`; an app typically calls `Password::defaults(fn () => Password::min(12)->letters()->mixedCase()->numbers()->symbols())` in a service provider's `boot()` and then uses `['required', 'confirmed', Password::defaults()]` everywhere. Because it is an object, that list must be an array. ## Practical style guide - Prefer arrays everywhere; they never surprise you with splitting. - Put cheap, format rules first and database or custom rules last. - Add `bail` where a list of follow-on errors would confuse the user or where a later rule is costly. - Keep a shared rule set (for example the username rules) in one place, such as a static method or a custom rule class, instead of copying strings between endpoints. ## Reading an existing rule list When reviewing rules, check three things in order: 1. Does every string rule survive a split on `|`? If a parameter contains a pipe, convert to an array. 2. Is every object in an array, not concatenated into a string, unless it is one of the stringable builders? 3. Is the order sensible — presence rules (`required`, `nullable`, `sometimes`) first, then type, then format, then size, then database or custom checks — with `bail` where a failure makes the rest pointless?
- Does bail stop validation of other fields?No. `bail` only stops the remaining rules of the field it is on after that field's first failure. Every other field is still validated. Stopping all fields after the first failing one is done with `stopOnFirstFailure`, which is a validator-wide setting.
- Without bail, does a username that fails 'string' still run the unique database query?No. The validator skips `unique` and `exists` when the attribute already has an error, so the query is avoided. `bail` would additionally skip any other ordinary rules after the first failure, such as `max`.
saying these in an interview costs you the question
- Pipe strings and arrays differ in which rules run
- Password::defaults() can be appended to a pipe string
- bail stops validating every other field too
- Without bail, all rules still run after required fails
- A regex with | works fine in a pipe string