Validació bàsica

Com validar dades a Laravel amb el mètode validate(), regles integrades i gestió d'errors.

Com funciona la validació#

El flux de validació a Laravel segueix un patró senzill: reps les dades de la petició, les valides contra un conjunt de regles, i si passen la validació les processos. Si la validació falla, Laravel redirigeix automàticament l'usuari al formulari anterior amb els errors i els valors que havia introduït.

Validació al controlador#

La manera més directa de validar és amb el mètode validate() de l'objecte Request. Rep un array on les claus són els noms dels camps i els valors són les regles de validació:

public function store(Request $request)
{
    $validated = $request->validate([
        'title' => 'required|string|max:255',
        'email' => 'required|email|unique:users',
        'body' => 'required|min:10',
        'category' => 'required|in:tech,laravel,php',
        'published_at' => 'nullable|date|after:today',
    ]);
 
    // $validated conté NOMÉS els camps que han passat la validació
    Article::create($validated);
 
    return redirect()->route('articles.index')
        ->with('success', 'Article creat correctament!');
}

El mètode validate() retorna un array amb només els camps validats. Si la validació falla, Laravel llança una excepció ValidationException que automàticament redirigeix l'usuari al formulari anterior amb els errors a la sessió. No cal gestionar manualment la redirecció en cas d'error.

Les regles es poden separar amb el caràcter | (pipe) o passar com un array, cosa que és més llegible quan hi ha moltes regles:

$validated = $request->validate([
    'title' => ['required', 'string', 'max:255'],
    'email' => ['required', 'email', 'unique:users,email'],
    'password' => ['required', 'string', 'min:8', 'confirmed'],
    'tags' => ['nullable', 'array'],
    'tags.*' => ['string', 'max:50'],
]);

La notació amb array és especialment necessària quan utilitzes regles que contenen comes o closures.

Regles de validació integrades#

Laravel inclou desenes de regles de validació que cobreixen la majoria de situacions. Aquí tens les més habituals organitzades per categoria:

Presència i tipus#

$request->validate([
    'name' => 'required',              // Camp obligatori
    'bio' => 'nullable',               // Pot ser null
    'nickname' => 'sometimes',         // Valida només si el camp és present
    'title' => 'required|string',      // Ha de ser text
    'age' => 'required|integer',       // Ha de ser un enter
    'price' => 'required|numeric',     // Ha de ser numèric (enters o decimals)
    'active' => 'required|boolean',    // Ha de ser true/false, 1/0, "1"/"0"
    'data' => 'required|array',        // Ha de ser un array
    'config' => 'required|json',       // Ha de ser JSON vàlid
]);

Strings i longitud#

$request->validate([
    'name' => 'required|string|min:2|max:100',     // Entre 2 i 100 caràcters
    'slug' => 'required|alpha_dash',                 // Lletres, números, guions
    'code' => 'required|alpha_num',                  // Només lletres i números
    'username' => 'required|regex:/^[a-z0-9_]+$/',   // Regex personalitzat
    'url' => 'required|url',                          // URL vàlida
    'ip' => 'required|ip',                            // Adreça IP vàlida
    'uuid' => 'required|uuid',                        // UUID vàlid
]);

Números i rangs#

$request->validate([
    'age' => 'required|integer|between:18,120',      // Entre 18 i 120
    'quantity' => 'required|integer|min:1|max:99',    // Mínim 1, màxim 99
    'price' => 'required|numeric|decimal:0,2',        // Fins a 2 decimals
    'rating' => 'required|integer|in:1,2,3,4,5',     // Valors específics
    'discount' => 'required|numeric|gt:0|lt:100',     // Major que 0, menor que 100
]);

Dates#

$request->validate([
    'birthday' => 'required|date',                          // Data vàlida
    'starts_at' => 'required|date|after:today',             // Després d'avui
    'ends_at' => 'required|date|after:starts_at',           // Després de starts_at
    'published_at' => 'nullable|date|before:tomorrow',      // Abans de demà
    'event_date' => 'required|date_format:Y-m-d',           // Format específic
    'meeting_at' => 'required|date|after_or_equal:today',   // Avui o després
]);

Base de dades#

use Illuminate\Validation\Rule;
 
$request->validate([
    // Únic a la taula users, columna email
    'email' => 'required|email|unique:users,email',
 
    // Únic ignorant el registre actual (per a updates)
    'email' => ['required', 'email', Rule::unique('users')->ignore($user->id)],
 
    // Ha d'existir a la taula
    'category_id' => 'required|exists:categories,id',
 
    // Existeix amb condicions addicionals
    'category_id' => ['required', Rule::exists('categories', 'id')->where('active', true)],
]);

Fitxers#

$request->validate([
    'avatar' => 'required|image|max:2048',               // Imatge, màxim 2MB
    'document' => 'required|file|mimes:pdf,doc,docx',    // Tipus específics
    'photo' => 'required|image|dimensions:min_width=100,min_height=100',
    'attachment' => 'required|file|max:10240',            // Qualsevol fitxer, 10MB
]);

Confirmació i coincidència#

$request->validate([
    // Requereix un camp password_confirmation que coincideixi
    'password' => 'required|string|min:8|confirmed',
 
    // Ha de coincidir amb la contrasenya actual
    'current_password' => 'required|current_password',
 
    // Igual a un altre camp
    'email_repeat' => 'required|same:email',
 
    // Diferent d'un altre camp
    'new_password' => 'required|different:current_password',
]);

Arrays i camps niuats#

Quan valides arrays o objectes niuats, utilitza la notació amb punts:

$request->validate([
    'tags' => 'required|array|min:1|max:10',
    'tags.*' => 'string|max:50',
 
    'addresses' => 'required|array',
    'addresses.*.street' => 'required|string',
    'addresses.*.city' => 'required|string',
    'addresses.*.zip' => 'required|string|size:5',
 
    'settings.notifications' => 'required|boolean',
    'settings.theme' => 'required|in:light,dark',
]);

La notació * indica "cada element de l'array". Això permet validar cada adreça, cada etiqueta o cada element de qualsevol array.

Validar manualment#

De vegades necessites més control sobre el procés de validació. Pots crear un validador manualment amb la facade Validator:

use Illuminate\Support\Facades\Validator;
 
$validator = Validator::make($request->all(), [
    'title' => 'required|max:255',
    'body' => 'required',
]);
 
if ($validator->fails()) {
    return redirect()->back()
        ->withErrors($validator)
        ->withInput();
}
 
$validated = $validator->validated();

Això és útil quan necessites executar lògica entre la validació i el processament, o quan vols gestionar els errors d'una manera diferent a la redirecció automàtica.

Afegir errors manualment#

Pots afegir errors al validador després de la validació inicial:

$validator = Validator::make($request->all(), [
    'email' => 'required|email',
]);
 
$validator->after(function ($validator) use ($request) {
    if ($this->hasTooManyAttempts($request)) {
        $validator->errors()->add(
            'email',
            'Has superat el nombre màxim d\'intents. Torna a provar-ho més tard.'
        );
    }
});
 
if ($validator->fails()) {
    // Gestionar errors
}

Mostrar errors a Blade#

Quan la validació falla, Laravel redirigeix l'usuari al formulari anterior amb els errors disponibles a la variable $errors. Aquesta variable és una instància de MessageBag i està disponible automàticament a totes les vistes:

{{-- Mostrar tots els errors --}}
@if($errors->any())
    <div class="bg-red-50 border border-red-200 rounded-lg p-4 mb-6">
        <h4 class="text-red-800 font-semibold">Hi ha errors al formulari:</h4>
        <ul class="mt-2 text-red-700 text-sm">
            @foreach($errors->all() as $error)
                <li>{{ $error }}</li>
            @endforeach
        </ul>
    </div>
@endif

Per mostrar l'error d'un camp específic al costat del camp, utilitza la directiva @error:

<form method="POST" action="/articles">
    @csrf
 
    <div class="mb-4">
        <label for="title">Títol</label>
        <input
            type="text"
            name="title"
            id="title"
            value="{{ old('title') }}"
            class="w-full border rounded px-3 py-2 @error('title') border-red-500 @enderror"
        >
        @error('title')
            <p class="text-red-500 text-sm mt-1">{{ $message }}</p>
        @enderror
    </div>
 
    <div class="mb-4">
        <label for="body">Contingut</label>
        <textarea
            name="body"
            id="body"
            class="w-full border rounded px-3 py-2 @error('body') border-red-500 @enderror"
        >{{ old('body') }}</textarea>
        @error('body')
            <p class="text-red-500 text-sm mt-1">{{ $message }}</p>
        @enderror
    </div>
 
    <button type="submit" class="bg-blue-500 text-white px-4 py-2 rounded">
        Crear article
    </button>
</form>

La funció old() retorna el valor que l'usuari havia introduït al camp abans que la validació fallés. Això evita que l'usuari hagi de tornar a escriure tot el formulari.

Validació de peticions d'API#

Quan la petició espera una resposta JSON (peticions d'API, peticions AJAX), Laravel no redirigeix sinó que retorna una resposta JSON amb codi 422 i els errors estructurats:

{
    "message": "The title field is required. (and 2 more errors)",
    "errors": {
        "title": ["The title field is required."],
        "email": ["The email field must be a valid email address."],
        "body": ["The body field must be at least 10 characters."]
    }
}

No cal fer res especial: Laravel detecta automàticament si la petició espera JSON (via la capçalera Accept: application/json) i ajusta la resposta.

Aturar a la primera fallada#

Per defecte, Laravel valida totes les regles de tots els camps i retorna tots els errors. Si vols que s'aturi a la primera regla que falla per a cada camp, utilitza bail:

$request->validate([
    'title' => 'bail|required|string|max:255',
    'email' => 'bail|required|email|unique:users',
]);

Amb bail, si title no és required, no es comproven string ni max:255. Això pot ser útil per evitar missatges d'error redundants o per estalviar consultes a la base de dades.

Named Error Bags#

Quan tens múltiples formularis a la mateixa pàgina, pots separar els errors amb named error bags per evitar confusions:

$request->validateWithBag('login', [
    'email' => 'required|email',
    'password' => 'required',
]);

A Blade, accedeix als errors del bag específic:

@error('email', 'login')
    <p class="text-red-500">{{ $message }}</p>
@enderror
 
{{-- O comprovant el bag directament --}}
@if($errors->login->any())
    {{-- Errors del formulari de login --}}
@endif