Missatges d'error

Com personalitzar missatges d'error de validació, localitzar-los i mostrar-los als formularis.

Com funcionen els missatges d'error#

Quan la validació falla, Laravel genera un missatge d'error per cada regla que no s'ha complert. Per defecte, els missatges són en anglès i segueixen el format "The field is required" o "The must be at least characters". Pots personalitzar-los a diversos nivells: per regla concreta, per camp, globalment al fitxer de localització, o directament al Form Request.

Missatges inline#

La manera més directa de personalitzar missatges és passar-los com a segon paràmetre del mètode validate():

$request->validate([
    'title' => 'required|max:255',
    'email' => 'required|email|unique:users',
    'body' => 'required|min:10',
], [
    'title.required' => 'Has d\'escriure un títol per a l\'article.',
    'title.max' => 'El títol és massa llarg. Màxim :max caràcters.',
    'email.required' => 'Necessitem el teu correu electrònic.',
    'email.email' => 'El format del correu no és vàlid.',
    'email.unique' => 'Aquest correu ja està registrat.',
    'body.required' => 'L\'article necessita contingut.',
    'body.min' => 'El contingut és massa curt. Escriu almenys :min caràcters.',
]);

El format és camp.regla. Laravel substitueix automàticament els placeholders com :max, :min, :attribute i d'altres pels valors corresponents.

Localització#

Per a projectes multilingües o per tenir un sol fitxer central amb tots els missatges, utilitza els fitxers de localització. Crea el fitxer lang/ca/validation.php per tenir els missatges en català:

// lang/ca/validation.php
return [
    'accepted' => 'El camp :attribute ha de ser acceptat.',
    'between' => [
        'numeric' => 'El camp :attribute ha d\'estar entre :min i :max.',
        'string' => 'El camp :attribute ha de tenir entre :min i :max caràcters.',
    ],
    'confirmed' => 'La confirmació del camp :attribute no coincideix.',
    'email' => 'El camp :attribute ha de ser una adreça de correu vàlida.',
    'exists' => 'El :attribute seleccionat no existeix.',
    'image' => 'El camp :attribute ha de ser una imatge.',
    'in' => 'El :attribute seleccionat no és vàlid.',
    'max' => [
        'numeric' => 'El camp :attribute no pot ser superior a :max.',
        'string' => 'El camp :attribute no pot superar els :max caràcters.',
        'file' => 'El camp :attribute no pot pesar més de :max kilobytes.',
    ],
    'min' => [
        'numeric' => 'El camp :attribute ha de ser com a mínim :min.',
        'string' => 'El camp :attribute ha de tenir almenys :min caràcters.',
        'file' => 'El camp :attribute ha de pesar almenys :min kilobytes.',
    ],
    'numeric' => 'El camp :attribute ha de ser un número.',
    'required' => 'El camp :attribute és obligatori.',
    'string' => 'El camp :attribute ha de ser text.',
    'unique' => 'El :attribute ja existeix.',
    'url' => 'El camp :attribute ha de ser una URL vàlida.',
];

Personalitzar noms d'atributs#

Per defecte, Laravel utilitza el nom del camp als missatges (per exemple, "El camp email és obligatori"). Pots donar-los noms més llegibles a l'array attributes del mateix fitxer:

// lang/ca/validation.php
return [
    // ... regles ...
 
    'attributes' => [
        'name' => 'nom',
        'email' => 'correu electrònic',
        'password' => 'contrasenya',
        'password_confirmation' => 'confirmació de la contrasenya',
        'title' => 'títol',
        'body' => 'contingut',
        'phone' => 'telèfon',
        'address' => 'adreça',
        'city' => 'ciutat',
        'country' => 'país',
        'category_id' => 'categoria',
        'published_at' => 'data de publicació',
    ],
];

Ara els missatges dirien "El camp títol és obligatori" o "El correu electrònic ha de ser una adreça de correu vàlida".

Missatges personalitzats per camp al fitxer de localització#

Pots definir missatges específics per camp dins del fitxer de localització, a l'array custom:

// lang/ca/validation.php
return [
    // ... regles generals ...
 
    'custom' => [
        'email' => [
            'required' => 'Necessitem el teu correu per poder contactar-te.',
            'unique' => 'Ja tenim un compte amb aquest correu.',
        ],
        'password' => [
            'min' => 'La contrasenya ha de tenir com a mínim :min caràcters per seguretat.',
        ],
    ],
];

Configurar l'idioma#

Per utilitzar el fitxer de localització en català, configura l'idioma al fitxer config/app.php:

'locale' => 'ca',
'fallback_locale' => 'en',

O al fitxer .env:

APP_LOCALE=ca
APP_FALLBACK_LOCALE=en

Treballar amb l'objecte d'errors#

Quan la validació falla, els errors s'emmagatzemen a la sessió i estan disponibles a les vistes a través de la variable $errors, que és una instància de Illuminate\Support\MessageBag:

// Comprovar si hi ha errors
$errors->any();          // true si hi ha algun error
$errors->isEmpty();      // true si no hi ha errors
 
// Obtenir errors
$errors->all();          // Array amb tots els missatges
$errors->first('title'); // Primer error del camp 'title'
$errors->get('title');   // Tots els errors del camp 'title' (array)
$errors->has('title');   // true si 'title' té errors
 
// Comptar errors
$errors->count();        // Nombre total d'errors

Mostrar errors a Blade#

Tots els errors junts#

Per mostrar un resum de tots els errors al principi del formulari:

@if($errors->any())
    <div class="bg-red-50 border border-red-200 rounded-lg p-4 mb-6">
        <div class="flex items-center gap-2 text-red-800 font-semibold">
            Hi ha {{ $errors->count() }} errors al formulari
        </div>
        <ul class="mt-2 text-sm text-red-700 list-disc list-inside">
            @foreach($errors->all() as $error)
                <li>{{ $error }}</li>
            @endforeach
        </ul>
    </div>
@endif

Errors per camp#

El patró més comú és mostrar l'error al costat de cada camp del formulari:

<form method="POST" action="{{ route('articles.store') }}">
    @csrf
 
    <div class="mb-4">
        <label for="title" class="block font-medium mb-1">Títol</label>
        <input
            type="text"
            name="title"
            id="title"
            value="{{ old('title') }}"
            class="w-full border rounded-lg px-3 py-2
                   @error('title') border-red-500 bg-red-50 @else border-gray-300 @enderror"
        >
        @error('title')
            <p class="text-red-600 text-sm mt-1">{{ $message }}</p>
        @enderror
    </div>
 
    <div class="mb-4">
        <label for="category_id" class="block font-medium mb-1">Categoria</label>
        <select
            name="category_id"
            id="category_id"
            class="w-full border rounded-lg px-3 py-2
                   @error('category_id') border-red-500 @enderror"
        >
            <option value="">Selecciona una categoria</option>
            @foreach($categories as $category)
                <option
                    value="{{ $category->id }}"
                    @selected(old('category_id') == $category->id)
                >
                    {{ $category->name }}
                </option>
            @endforeach
        </select>
        @error('category_id')
            <p class="text-red-600 text-sm mt-1">{{ $message }}</p>
        @enderror
    </div>
 
    <div class="mb-4">
        <label for="body" class="block font-medium mb-1">Contingut</label>
        <textarea
            name="body"
            id="body"
            rows="6"
            class="w-full border rounded-lg px-3 py-2
                   @error('body') border-red-500 @enderror"
        >{{ old('body') }}</textarea>
        @error('body')
            <p class="text-red-600 text-sm mt-1">{{ $message }}</p>
        @enderror
    </div>
 
    <div class="mb-4">
        <label class="flex items-center gap-2">
            <input
                type="checkbox"
                name="published"
                value="1"
                @checked(old('published'))
            >
            <span>Publicar immediatament</span>
        </label>
    </div>
 
    <button type="submit" class="bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700">
        Crear article
    </button>
</form>

Conservar valors amb old()#

La funció old() recupera el valor que l'usuari havia introduït abans que la validació fallés. Això és important per no obligar l'usuari a tornar a omplir tot el formulari:

{{-- Inputs de text --}}
<input type="text" name="title" value="{{ old('title') }}">
 
{{-- Textarea --}}
<textarea name="body">{{ old('body') }}</textarea>
 
{{-- Select --}}
<option value="tech" @selected(old('category') === 'tech')>Tecnologia</option>
 
{{-- Checkbox --}}
<input type="checkbox" name="newsletter" @checked(old('newsletter'))>
 
{{-- Radio --}}
<input type="radio" name="type" value="article" @checked(old('type') === 'article')>
 
{{-- Amb valor per defecte (per a edició) --}}
<input type="text" name="title" value="{{ old('title', $article->title) }}">

El segon paràmetre de old() és el valor per defecte, que s'utilitza quan no hi ha cap valor anterior a la sessió. Això és ideal per a formularis d'edició: mostra el valor antic si la validació ha fallat, o el valor actual del model si és la primera càrrega.

Component de camp reutilitzable#

Per evitar repetir el patró d'error a cada camp, pots crear un component Blade reutilitzable:

{{-- resources/views/components/form/input.blade.php --}}
@props(['name', 'label', 'type' => 'text', 'value' => ''])
 
<div class="mb-4">
    <label for="{{ $name }}" class="block font-medium mb-1">{{ $label }}</label>
    <input
        type="{{ $type }}"
        name="{{ $name }}"
        id="{{ $name }}"
        value="{{ old($name, $value) }}"
        {{ $attributes->merge([
            'class' => 'w-full border rounded-lg px-3 py-2 ' .
                       ($errors->has($name) ? 'border-red-500 bg-red-50' : 'border-gray-300')
        ]) }}
    >
    @error($name)
        <p class="text-red-600 text-sm mt-1">{{ $message }}</p>
    @enderror
</div>

Ara pots utilitzar-lo de manera concisa:

<x-form.input name="title" label="Títol" />
<x-form.input name="email" label="Correu electrònic" type="email" />
<x-form.input name="title" label="Títol" :value="$article->title" />