Vai al contenuto principale

Cosa sono le regole di validazione personalizzate

Laravel offre molte regole di validazione integrate, ma a volte serve una logica di validazione specifica dell’applicazione. Le regole personalizzate permettono di definire logica riutilizzabile come classe o closure, e usarla al pari delle regole standard. Ci sono due modi principali per definire una regola personalizzata.
  • Oggetti Rule — molto riutilizzabili e facili da testare
  • Closure — indicate per regole semplici usate una sola volta

Oggetti Rule

Generare la classe della regola

Con il comando Artisan make:rule generi una nuova classe di regola. La classe viene creata in app/Rules.
php artisan make:rule Uppercase

Implementare l’interfaccia ValidationRule

Nella classe generata implementi il metodo validate. Il metodo, in caso di fallimento, invoca la closure $fail.
<?php

namespace App\Rules;

use Closure;
use Illuminate\Contracts\Validation\ValidationRule;

class Uppercase implements ValidationRule
{
    /**
     * Esegue la regola di validazione
     */
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        if (strtoupper($value) !== $value) {
            $fail('The :attribute must be uppercase.');
        }
    }
}
Nel messaggio passato a $fail puoi usare il placeholder :attribute. Laravel lo sostituisce con il nome del campo.

Applicare l’oggetto Rule

Passi un’istanza dell’oggetto Rule nell’array di validazione.
use App\Rules\Uppercase;

$request->validate([
    'name' => ['required', 'string', new Uppercase],
]);
Puoi usarla allo stesso modo nel metodo rules() di una form request.
public function rules(): array
{
    return [
        'name' => ['required', 'string', new Uppercase],
    ];
}

Messaggi di errore tramite chiavi di traduzione

Al posto di hardcodare il messaggio, puoi usare una chiave di traduzione.
public function validate(string $attribute, mixed $value, Closure $fail): void
{
    if (strtoupper($value) !== $value) {
        $fail('validation.uppercase')->translate();
    }
}
Aggiungi il messaggio al file di traduzione lang/it/validation.php.
return [
    'uppercase' => 'Il campo :attribute deve essere in maiuscolo.',
    // ...
];

Aggiungere più messaggi di errore

Per riportare più errori sullo stesso campo, chiami $fail più volte.
public function validate(string $attribute, mixed $value, Closure $fail): void
{
    if (! is_string($value)) {
        $fail('The :attribute must be a string.');
        return;
    }

    if (strlen($value) < 8) {
        $fail('The :attribute must be at least 8 characters.');
    }

    if (! preg_match('/[A-Z]/', $value)) {
        $fail('The :attribute must contain at least one uppercase letter.');
    }
}

Regole basate su closure

Le regole semplici usate una sola volta nell’applicazione possono essere definite come closure, senza creare una classe.
use Illuminate\Support\Facades\Validator;
use Closure;

$validator = Validator::make($request->all(), [
    'title' => [
        'required',
        'max:255',
        function (string $attribute, mixed $value, Closure $fail) {
            if ($value === 'foo') {
                $fail("The {$attribute} is invalid.");
            }
        },
    ],
]);
Le regole con closure sono comode perché inline, ma sono difficili da riusare e testare in isolamento. Per logica usata in più punti conviene estrarla in un oggetto Rule.

Regole implicite (eseguite anche con valore vuoto)

Di default, se il campo è vuoto o assente, le regole personalizzate non vengono eseguite. Se vuoi eseguirle anche sui valori vuoti, genera la classe con l’opzione --implicit.
php artisan make:rule Uppercase --implicit
La classe generata implementa l’interfaccia ImplicitRule. Questa interfaccia non aggiunge metodi: funziona come segnale per Laravel.
<?php

namespace App\Rules;

use Closure;
use Illuminate\Contracts\Validation\ImplicitRule;
use Illuminate\Contracts\Validation\ValidationRule;

class RequiredIfJapanese implements ValidationRule, ImplicitRule
{
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        if (empty($value)) {
            $fail('The :attribute is required.');
            return;
        }

        if (! preg_match('/^[\p{Hiragana}\p{Katakana}\p{Han}ー]+$/u', $value)) {
            $fail('The :attribute must contain only Japanese characters.');
        }
    }
}
ImplicitRule segnala a Laravel che “l’attributo è obbligatorio”. Se far fallire davvero la validazione con valore vuoto o meno dipende dall’implementazione di validate.

Accesso ai dati

DataAwareRule — accedere a tutti i dati del form

Se vuoi validare in base al valore di altri campi, implementa l’interfaccia DataAwareRule. Il metodo setData viene chiamato automaticamente prima dell’esecuzione della regola.
<?php

namespace App\Rules;

use Closure;
use Illuminate\Contracts\Validation\DataAwareRule;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Support\Facades\DB;

class UniqueForTenant implements DataAwareRule, ValidationRule
{
    /**
     * Tutti i dati sotto validazione
     *
     * @var array<string, mixed>
     */
    protected array $data = [];

    public function __construct(
        protected string $table,
        protected string $column = 'value',
    ) {}

    /**
     * Imposta i dati di validazione
     *
     * @param  array<string, mixed>  $data
     */
    public function setData(array $data): static
    {
        $this->data = $data;

        return $this;
    }

    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        $tenantId = $this->data['tenant_id'] ?? null;

        $exists = DB::table($this->table)
            ->where('tenant_id', $tenantId)
            ->where($this->column, $value)
            ->exists();

        if ($exists) {
            $fail('The :attribute has already been taken for this tenant.');
        }
    }
}
Esempio d’uso:
$request->validate([
    'tenant_id' => 'required|integer',
    'email' => ['required', 'email', new UniqueForTenant('users', 'email')],
]);

ValidatorAwareRule — accedere all’istanza del Validator

Per accedere a tutte le informazioni del Validator (regole già fallite, messaggi personalizzati, ecc.) implementa l’interfaccia ValidatorAwareRule.
<?php

namespace App\Rules;

use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Contracts\Validation\ValidatorAwareRule;
use Illuminate\Validation\Validator;

class ConditionalFormat implements ValidationRule, ValidatorAwareRule
{
    protected Validator $validator;

    public function setValidator(Validator $validator): static
    {
        $this->validator = $validator;

        return $this;
    }

    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        // salta se un altro campo ha già fallito la validazione
        if ($this->validator->errors()->has('type')) {
            return;
        }

        $type = $this->validator->getData()['type'] ?? null;

        if ($type === 'phone' && ! preg_match('/^\+?[0-9\-\s]+$/', $value)) {
            $fail('The :attribute must be a valid phone number.');
        }

        if ($type === 'email' && ! filter_var($value, FILTER_VALIDATE_EMAIL)) {
            $fail('The :attribute must be a valid email address.');
        }
    }
}

Casi d’uso pratici

Verifica di caratteri giapponesi

Regola che verifica il tipo di caratteri (mezza/piena larghezza).
php artisan make:rule JapaneseOnly
<?php

namespace App\Rules;

use Closure;
use Illuminate\Contracts\Validation\ValidationRule;

class JapaneseOnly implements ValidationRule
{
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        // consente solo hiragana, katakana, kanji e prolungamento
        if (! preg_match('/^[\p{Hiragana}\p{Katakana}\p{Han}ー\s]+$/u', $value)) {
            $fail('Il campo :attribute deve contenere solo caratteri giapponesi (hiragana, katakana, kanji).');
        }
    }
}
$request->validate([
    'name_kana' => ['required', 'string', new JapaneseOnly],
]);

Validazione del formato di un numero di telefono

Regola per verificare il formato dei numeri di telefono giapponesi.
php artisan make:rule JapanesePhone
<?php

namespace App\Rules;

use Closure;
use Illuminate\Contracts\Validation\ValidationRule;

class JapanesePhone implements ValidationRule
{
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        // accetta con o senza trattini, compreso il formato internazionale
        $normalized = preg_replace('/[\s\-\(\)]/', '', $value);

        $patterns = [
            '/^0\d{9,10}$/',        // numeri fissi o cellulari generici
            '/^\+81\d{9,10}$/',     // formato internazionale
        ];

        foreach ($patterns as $pattern) {
            if (preg_match($pattern, $normalized)) {
                return;
            }
        }

        $fail('Il campo :attribute deve essere un numero di telefono valido.');
    }
}

Vincolo di unicità per tenant

Vincolo di unicità nell’ambito del tenant, tipico delle applicazioni multi-tenant.
1

Creare la classe della regola

php artisan make:rule TenantUnique
<?php

namespace App\Rules;

use Closure;
use Illuminate\Contracts\Validation\DataAwareRule;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Support\Facades\DB;

class TenantUnique implements DataAwareRule, ValidationRule
{
    protected array $data = [];

    public function __construct(
        protected string $table,
        protected string $column,
        protected ?int $ignoreId = null,
    ) {}

    public function setData(array $data): static
    {
        $this->data = $data;

        return $this;
    }

    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        $tenantId = $this->data['tenant_id'] ?? null;

        $query = DB::table($this->table)
            ->where('tenant_id', $tenantId)
            ->where($this->column, $value);

        if ($this->ignoreId !== null) {
            $query->where('id', '!=', $this->ignoreId);
        }

        if ($query->exists()) {
            $fail('Il campo :attribute è già in uso.');
        }
    }
}
2

Usarla in una form request

<?php

namespace App\Http\Requests;

use App\Rules\TenantUnique;
use Illuminate\Foundation\Http\FormRequest;

class CreateProjectRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'tenant_id' => 'required|integer|exists:tenants,id',
            'name' => [
                'required',
                'string',
                'max:100',
                new TenantUnique('projects', 'name'),
            ],
        ];
    }

    public function authorize(): bool
    {
        return true;
    }
}
3

In update: escludere l'ID

Per aggiornare un record esistente, escludi il proprio ID dal controllo di duplicazione.
class UpdateProjectRequest extends FormRequest
{
    public function rules(): array
    {
        $projectId = $this->route('project');

        return [
            'tenant_id' => 'required|integer|exists:tenants,id',
            'name' => [
                'required',
                'string',
                'max:100',
                new TenantUnique('projects', 'name', ignoreId: $projectId),
            ],
        ];
    }
}

Registrazione delle regole nel service provider

Aggiungere regole con Validator::extend()

Con Validator::extend() puoi usare la regola personalizzata in forma di stringa ('rule_name'). La registri nel metodo boot() di AppServiceProvider.
<?php

namespace App\Providers;

use Illuminate\Support\Facades\Validator;
use Illuminate\Support\ServiceProvider;
use Illuminate\Validation\Validator as ValidatorInstance;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Validator::extend('japanese_only', function (string $attribute, mixed $value, array $parameters, ValidatorInstance $validator): bool {
            return preg_match('/^[\p{Hiragana}\p{Katakana}\p{Han}ー\s]+$/u', $value) === 1;
        });

        Validator::replacer('japanese_only', function (string $message, string $attribute): string {
            return str_replace(':attribute', $attribute, 'Il campo :attribute deve essere in giapponese.');
        });
    }
}
Le regole registrate con Validator::extend() si indicano come stringa.
$request->validate([
    'name_kana' => 'required|japanese_only',
]);
Validator::extend() è la modalità di registrazione più vecchia. Per nuovi sviluppi consigliamo l’uso di oggetti Rule che implementano ValidationRule.

Aggiungere come metodo statico sulla classe Rule

Aggiungendo una macro alla facade Rule puoi offrire un’API fluent come Rule::myRule().
use Illuminate\Validation\Rule;

Rule::macro('tenantUnique', function (string $table, string $column, ?int $ignoreId = null) {
    return new \App\Rules\TenantUnique($table, $column, $ignoreId);
});
// esempio d'uso
$request->validate([
    'name' => ['required', Rule::tenantUnique('projects', 'name')],
]);

Dettagli dell’implementazione interna

Vediamo come Illuminate\Validation\Validator invoca le regole personalizzate. Nel Validator, validateAttribute() elabora ogni campo. Se la regola implementa ValidationRule, viene invocato il metodo validateUsingCustomRule().
// versione semplificata di Illuminate\Validation\Validator::validateUsingCustomRule()
protected function validateUsingCustomRule($attribute, $value, $rule)
{
    // per DataAwareRule inietta tutti i dati
    if ($rule instanceof DataAwareRule) {
        $rule->setData($this->getData());
    }

    // per ValidatorAwareRule inietta il Validator stesso
    if ($rule instanceof ValidatorAwareRule) {
        $rule->setValidator($this);
    }

    // chiama validate()
    $rule->validate($attribute, $value, function ($message, $translate = false) use ($attribute, $rule) {
        // closure $fail — aggiunge un messaggio d'errore
        $this->errors()->add($attribute, $this->makeReplacements(
            $message,
            $attribute,
            get_class($rule),
            [], // sostituzioni di placeholder aggiuntivi (es: ['min' => '8'])
        ));
    });
}
La gestione di ImplicitRule avviene tramite il metodo isImplicit(): se il valore è vuoto, la regola viene comunque eseguita.
// decide se eseguire la regola quando il valore è vuoto
protected function isImplicit($rule): bool
{
    return $rule instanceof ImplicitRule
        || in_array($rule, $this->implicitRules);
}
Puoi implementare contemporaneamente DataAwareRule e ValidatorAwareRule. In una classe che implementa entrambe le interfacce vengono eseguite entrambe le iniezioni.

Pagine correlate

Validazione (introduzione)

Rivedi le modalità di validazione standard in controller e form request.
Ultima modifica il 13 luglio 2026