Regles personalitzades

Com crear regles de validació personalitzades a Laravel: closures, Rule objects, regles condicionals i la classe Rule.

Quan crear regles personalitzades?#

Les regles integrades de Laravel cobreixen la majoria de situacions, però de vegades necessites validar lògica específica del teu domini. Per exemple, comprovar que un codi de descompte és vàlid, que un nom d'usuari no conté paraules prohibides, o que un horari no es solapa amb un altre ja existent. En aquests casos, pots crear les teves pròpies regles.

Regles amb closures#

La manera més ràpida de crear una regla personalitzada és amb un closure directament a les regles de validació. El closure rep el nom de l'atribut, el valor i una funció $fail per reportar l'error:

$request->validate([
    'username' => [
        'required',
        'string',
        function (string $attribute, mixed $value, Closure $fail) {
            if (str_contains(strtolower($value), 'admin')) {
                $fail("El {$attribute} no pot contenir la paraula 'admin'.");
            }
        },
    ],
]);

Les closures són pràctiques per a regles que només s'utilitzen en un sol lloc. Si necessites reutilitzar la regla, és millor crear un Rule Object.

Un closure pot accedir a variables del context exterior amb use:

$request->validate([
    'start_time' => [
        'required',
        'date',
        function (string $attribute, mixed $value, Closure $fail) use ($request) {
            $exists = Booking::where('room_id', $request->room_id)
                ->where('start_time', '<=', $value)
                ->where('end_time', '>=', $value)
                ->exists();
 
            if ($exists) {
                $fail('Ja existeix una reserva en aquest horari.');
            }
        },
    ],
]);

Rule Objects#

Per a regles reutilitzables, crea una classe Rule amb Artisan:

php artisan make:rule Uppercase

Això genera un fitxer a app/Rules/Uppercase.php:

namespace App\Rules;
 
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
 
class Uppercase implements ValidationRule
{
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        if (strtoupper($value) !== $value) {
            $fail('El :attribute ha d\'estar completament en majúscules.');
        }
    }
}

El mètode validate() rep l'atribut, el valor i la funció $fail. Si el valor no és vàlid, crida $fail() amb el missatge d'error. El placeholder :attribute es substitueix automàticament pel nom del camp.

Per utilitzar-la:

use App\Rules\Uppercase;
 
$request->validate([
    'code' => ['required', 'string', new Uppercase],
]);

Rule Objects amb paràmetres#

Pots fer que la regla accepti paràmetres a través del constructor:

namespace App\Rules;
 
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
 
class MaxWords implements ValidationRule
{
    public function __construct(
        protected int $maxWords,
    ) {}
 
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        $wordCount = str_word_count($value);
 
        if ($wordCount > $this->maxWords) {
            $fail("El :attribute no pot superar les {$this->maxWords} paraules. En té {$wordCount}.");
        }
    }
}
$request->validate([
    'summary' => ['required', 'string', new MaxWords(50)],
    'title' => ['required', 'string', new MaxWords(10)],
]);

Regles que accedeixen a la base de dades#

namespace App\Rules;
 
use App\Models\DiscountCode;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
 
class ValidDiscountCode implements ValidationRule
{
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        $code = DiscountCode::where('code', $value)
            ->where('active', true)
            ->where('expires_at', '>', now())
            ->first();
 
        if (! $code) {
            $fail('El codi de descompte no és vàlid o ha expirat.');
            return;
        }
 
        if ($code->uses >= $code->max_uses) {
            $fail('El codi de descompte ha arribat al límit d\'usos.');
        }
    }
}

La classe Rule#

Laravel proporciona la classe Rule amb mètodes estàtics per construir regles complexes de manera fluida. Ja l'hem vist amb unique i exists, però ofereix moltes més opcions:

Unique amb condicions#

use Illuminate\Validation\Rule;
 
$request->validate([
    // Únic, ignorant el registre actual
    'email' => [
        'required',
        'email',
        Rule::unique('users', 'email')->ignore($user->id),
    ],
 
    // Únic amb condicions addicionals
    'slug' => [
        'required',
        Rule::unique('articles')
            ->where('tenant_id', $tenant->id)
            ->ignore($article->id),
    ],
]);

Exists amb condicions#

$request->validate([
    'category_id' => [
        'required',
        Rule::exists('categories', 'id')
            ->where('active', true)
            ->where('tenant_id', auth()->user()->tenant_id),
    ],
]);

In i NotIn#

$request->validate([
    'status' => [
        'required',
        Rule::in(['draft', 'published', 'archived']),
    ],
 
    'role' => [
        'required',
        Rule::notIn(['superadmin', 'root']),
    ],
]);

Prohibir camps condicionalment#

$request->validate([
    'role' => 'required|string',
 
    // Si el role és 'admin', el camp permissions és obligatori
    'permissions' => Rule::requiredIf($request->role === 'admin'),
 
    // Prohibir un camp si un altre té un valor específic
    'discount' => Rule::prohibitedIf($request->role !== 'admin'),
]);

Password#

Laravel inclou una regla especial per validar contrasenyes amb requisits configurables:

use Illuminate\Validation\Rules\Password;
 
$request->validate([
    'password' => [
        'required',
        'confirmed',
        Password::min(8)
            ->letters()         // Almenys una lletra
            ->mixedCase()       // Majúscules i minúscules
            ->numbers()         // Almenys un número
            ->symbols()         // Almenys un símbol
            ->uncompromised(),  // No aparèixer en filtracions de dades
    ],
]);

Pots definir un preset per defecte al AppServiceProvider i reutilitzar-lo:

// A AppServiceProvider::boot()
Password::defaults(function () {
    $rule = Password::min(8);
 
    return app()->isProduction()
        ? $rule->letters()->mixedCase()->numbers()->symbols()->uncompromised()
        : $rule;
});
// Ara pots utilitzar el preset
$request->validate([
    'password' => ['required', 'confirmed', Password::defaults()],
]);

File#

La regla File permet construir validacions de fitxers de manera fluida:

use Illuminate\Validation\Rules\File;
 
$request->validate([
    'avatar' => [
        'required',
        File::image()
            ->min(10)         // Mínim 10KB
            ->max(2 * 1024)   // Màxim 2MB
            ->dimensions(
                Rule::dimensions()
                    ->minWidth(100)
                    ->minHeight(100)
                    ->maxWidth(2000)
                    ->maxHeight(2000)
            ),
    ],
 
    'document' => [
        'required',
        File::types(['pdf', 'doc', 'docx'])
            ->max(10 * 1024), // Màxim 10MB
    ],
]);

Validació condicional avançada#

Per a validacions que depenen de múltiples camps o de l'estat de la petició:

use Illuminate\Support\Facades\Validator;
 
$validator = Validator::make($request->all(), [
    'type' => 'required|in:individual,company',
    'name' => 'required|string',
]);
 
// Afegir regles condicionalment
$validator->sometimes('company_name', 'required|string', function ($input) {
    return $input->type === 'company';
});
 
$validator->sometimes('nif', 'required|string|size:9', function ($input) {
    return $input->type === 'individual';
});
 
$validator->sometimes('cif', 'required|string', function ($input) {
    return $input->type === 'company';
});
 
$validated = $validator->validate();

El mètode sometimes() afegeix regles a un camp només quan el closure retorna true. Això és més flexible que els condicionals dins de rules() perquè pot accedir a tots els valors de la petició.